/*
* Copyright 2001-2008 Geert Bevin <gbevin[remove] at uwyn dot com>
* Licensed under the Apache License, Version 2.0 (the "License")
*
* Parts are Copyright 1999-2004 Mort Bay Consulting Pty. Ltd.
* ------------------------------------------------------------------------
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http:*www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* $Id: MockResponse.java 3918 2008-04-14 17:35:35Z gbevin $
*/
package com.uwyn.rife.test;
import com.uwyn.rife.engine.*;
import java.io.*;
import java.util.*;
import com.uwyn.rife.cmf.loader.xhtml.Jdk14Loader;
import com.uwyn.rife.engine.exceptions.EngineException;
import com.uwyn.rife.template.Template;
import com.uwyn.rife.test.MockHeaders;
import com.uwyn.rife.test.exceptions.InvalidXmlException;
import com.uwyn.rife.tools.ExceptionUtils;
import com.uwyn.rife.tools.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Provides a {@link Response} implementation that is suitable for testing a
* web application outside of a servlet container.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @version $Revision: 3918 $
* @since 1.1
*/
public class MockResponse extends AbstractResponse
{
private final static String HEADER_CONTENT_TYPE = "Content-Type";
private final static String HEADER_CONTENT_LANGUAGE = "Content-Language";
private final static String HEADER_CONTENT_LENGTH = "Content-Length";
private final static String HEADER_LOCATION = "Location";
private final static int SC_200_OK = 200;
private final static int SC_302_MOVED_TEMPORARILY = 302;
private final static Pattern STRIP_XHTML_XMLNS = Pattern.compile("html\\s+xmlns=\"[^\"]*\"");
private MockConversation mMockConversation;
private MockHeaders mHeaders = new MockHeaders();
private HashMap<String, Cookie> mNewCookies = new HashMap<String, Cookie>();
private String mContentType;
private String mCharacterEncoding;
private int mStatus = SC_200_OK;
private String mReason;
private Locale mLocale;
private ByteArrayOutputStream mMockOutputStream = new ByteArrayOutputStream();
private PrintWriter mMockWriter;
private Template mTemplate;
private Map<String, MockResponse> mEmbeddedResponses = new LinkedHashMap<String, MockResponse>();
MockResponse(MockConversation conversation, Request request)
{
this(conversation, request, false);
}
private MockResponse(MockConversation conversation, Request request, boolean embedded)
{
super(request, embedded);
mMockConversation = conversation;
}
MockConversation getMockConversation()
{
return mMockConversation;
}
/**
* Retrieves the {@link ElementInfo} of the element that was last
* processed with this response.
*
* @return the <code>ElementInfo</code> of the last element
* @see #getLastElement
* @see #getLastElementId
* @since 1.1
*/
public ElementInfo getLastElementInfo()
{
ElementSupport element = getLastElement();
if (null == element)
{
return null;
}
return element.getElementInfo();
}
/**
* Retrieves the identifier of the element that was last processed with
* this response.
*
* @return the identifier of the last element
* @see #getLastElement
* @see #getLastElementInfo
* @since 1.1
*/
public String getLastElementId()
{
ElementInfo element_info = getLastElementInfo();
if (null == element_info)
{
return null;
}
return element_info.getId();
}
/**
* Retrieves the an array of all the bytes that have been written to this
* reponse.
*
* @return an array of bytes with the response content
* @see #getText
* @see #getTemplate
* @see #getParsedHtml
* @since 1.1
*/
public byte[] getBytes()
{
return mMockOutputStream.toByteArray();
}
/**
* Retrieves the content of this reponse as text.
*
* @return the response content as text
* @see #getBytes
* @see #getTemplate
* @see #getParsedHtml
* @since 1.1
*/
public String getText()
{
String charset = mCharacterEncoding;
if (null == charset)
{
charset = StringUtils.ENCODING_ISO_8859_1;
}
try
{
return new String(getBytes(), charset);
}
catch (UnsupportedEncodingException e)
{
return ExceptionUtils.getExceptionStackTrace(e);
}
}
/**
* Retrieves the template instance that was printed to the response.
*
* @return the template instance that was printed to the response; or
* <p><code>null</code> of no template was printed to the response
* @see #getBytes
* @see #getText
* @see #getParsedHtml
* @since 1.1
*/
public Template getTemplate()
{
return mTemplate;
}
/**
* Retrieves the content of this reponse as parsed HTML.
*
* @exception IOException when exception occured during the retrieval on
* the response content
* @exception SAXException when exception occured during the parsing of
* the content as HTML
* @return the response content as parsed HTML
* @see #getBytes
* @see #getText
* @see #getTemplate
* @since 1.1
*/
public ParsedHtml getParsedHtml()
throws IOException, SAXException
{
return ParsedHtml.parse(this);
}
public String getContentType()
{
return getHeader(HEADER_CONTENT_TYPE);
}
/**
* Evaluate an XPath expression in the context of the response text and
* return the result as a list of DOM nodes.
* <p>More information about XPath can be found in the <a
* href="http://www.w3.org/TR/xpath">original specification</a> or in this
* <a href="http://zvon.org/xxl/XMLTutorial/General/book.html">tutorial</a>.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>NodeList</code>
* @see #xpathNode(String)
* @see #xpathString(String)
* @see #xpathBoolean(String)
* @see #xpathNumber(String)
* @since 1.1
*/
public NodeList xpathNodeSet(String expression)
throws XPathExpressionException
{
return (NodeList)xpath(expression, XPathConstants.NODESET);
}
/**
* Evaluate an XPath expression in the context of the response text and
* return the result as a DOM node.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String)
* @see #xpathString(String)
* @see #xpathBoolean(String)
* @see #xpathNumber(String)
* @since 1.1
*/
public Node xpathNode(String expression)
throws XPathExpressionException
{
return (Node)xpath(expression, XPathConstants.NODE);
}
/**
* Evaluate an XPath expression in the context of the response text and
* return the result as a string.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String)
* @see #xpathNode(String)
* @see #xpathBoolean(String)
* @see #xpathNumber(String)
* @since 1.1
*/
public String xpathString(String expression)
throws XPathExpressionException
{
return (String)xpath(expression, XPathConstants.STRING);
}
/**
* Evaluate an XPath expression in the context of the response text and
* return the result as a boolean.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String)
* @see #xpathNode(String)
* @see #xpathString(String)
* @see #xpathNumber(String)
* @since 1.1
*/
public Boolean xpathBoolean(String expression)
throws XPathExpressionException
{
return (Boolean)xpath(expression, XPathConstants.BOOLEAN);
}
/**
* Evaluate an XPath expression in the context of the response text and
* return the result as a number.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String)
* @see #xpathNode(String)
* @see #xpathString(String)
* @see #xpathBoolean(String)
* @since 1.1
*/
public Double xpathNumber(String expression)
throws XPathExpressionException
{
return (Double)xpath(expression, XPathConstants.NUMBER);
}
private Object xpath(String expression, QName returnType)
throws XPathExpressionException
{
Matcher matcher = STRIP_XHTML_XMLNS.matcher(getText());
String text = matcher.replaceAll("html xmlns=\"\"");
Reader reader = new StringReader(text);
InputSource inputsource = new InputSource(reader);
XPath xpath = XPathFactory.newInstance().newXPath();
return xpath.evaluate(expression, inputsource, returnType);
}
/**
* Evaluate an XPath expression in the provided context object and
* return the result as a list of DOM nodes.
* <p>More information about XPath can be found in the <a
* href="http://www.w3.org/TR/xpath">original specification</a> or in this
* <a href="http://zvon.org/xxl/XMLTutorial/General/book.html">tutorial</a>.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>NodeList</code>
* @see #xpathNode(String, Object)
* @see #xpathString(String, Object)
* @see #xpathBoolean(String, Object)
* @see #xpathNumber(String, Object)
* @since 1.2
*/
public NodeList xpathNodeSet(String expression, Object context)
throws XPathExpressionException
{
return (NodeList)xpath(expression, context, XPathConstants.NODESET);
}
/**
* Evaluate an XPath expression in the provided context object and
* return the result as a DOM node.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String, Object)
* @see #xpathString(String, Object)
* @see #xpathBoolean(String, Object)
* @see #xpathNumber(String, Object)
* @since 1.2
*/
public Node xpathNode(String expression, Object context)
throws XPathExpressionException
{
return (Node)xpath(expression, context, XPathConstants.NODE);
}
/**
* Evaluate an XPath expression in the provided context object and
* return the result as a string.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String, Object)
* @see #xpathNode(String, Object)
* @see #xpathBoolean(String, Object)
* @see #xpathNumber(String, Object)
* @since 1.2
*/
public String xpathString(String expression, Object context)
throws XPathExpressionException
{
return (String)xpath(expression, context, XPathConstants.STRING);
}
/**
* Evaluate an XPath expression in the provided context object and
* return the result as a boolean.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String, Object)
* @see #xpathNode(String, Object)
* @see #xpathString(String, Object)
* @see #xpathNumber(String, Object)
* @since 1.2
*/
public Boolean xpathBoolean(String expression, Object context)
throws XPathExpressionException
{
return (Boolean)xpath(expression, context, XPathConstants.BOOLEAN);
}
/**
* Evaluate an XPath expression in the provided context object and
* return the result as a number.
*
* @exception XPathExpressionException if expression cannot be evaluated.
* @return the result as a <code>Node</code>
* @see #xpathNodeSet(String, Object)
* @see #xpathNode(String, Object)
* @see #xpathString(String, Object)
* @see #xpathBoolean(String, Object)
* @since 1.2
*/
public Double xpathNumber(String expression, Object context)
throws XPathExpressionException
{
return (Double)xpath(expression, context, XPathConstants.NUMBER);
}
private Object xpath(String expression, Object context, QName returnType)
throws XPathExpressionException
{
XPath xpath = XPathFactory.newInstance().newXPath();
return xpath.evaluate(expression, context, returnType);
}
/**
* Validates the response as an XML document.
*
* @exception InvalidXmlException when the XML document isn't valid
* @since 1.2
*/
public void validateAsXml()
throws InvalidXmlException
{
Set<String> errors = new LinkedHashSet<String>();
new Jdk14Loader().loadFromString(getText(), false, errors);
if (errors.size() > 0)
{
throw new InvalidXmlException(errors);
}
}
public void print(Template template)
throws EngineException
{
mTemplate = template;
super.print(template);
}
protected void _setContentType(String contentType)
{
if (contentType == null)
{
mContentType = null;
if (mHeaders != null)
{
mHeaders.removeHeader(HEADER_CONTENT_TYPE);
}
}
else
{
// Look for encoding in contentType
int i0 = contentType.indexOf(';');
if (i0 > 0)
{
// Strip params off mimetype
mContentType = contentType.substring(0, i0).trim();
// Look for charset
int i1 = contentType.indexOf("charset=", i0);
if (i1 >= 0)
{
i1 += 8;
int i2 = contentType.indexOf(' ', i1);
mCharacterEncoding = (0 < i2)
? contentType.substring(i1, i2)
: contentType.substring(i1);
mCharacterEncoding = QuotedStringTokenizer.unquote(mCharacterEncoding);
}
else // No encoding in the params.
{
if (mCharacterEncoding != null)
{
// Add any previously set encoding.
contentType += ";charset=" +
QuotedStringTokenizer.quote(mCharacterEncoding, ";= ");
}
}
}
else // No encoding and no other params
{
mContentType = contentType;
// Add any previously set encoding.
if (mCharacterEncoding != null)
{
contentType += ";charset=" + QuotedStringTokenizer.quote(mCharacterEncoding, ";= ");
}
}
setHeader(HEADER_CONTENT_TYPE, contentType);
}
setHeader(HEADER_CONTENT_TYPE, contentType);
}
protected String _getCharacterEncoding()
{
return mCharacterEncoding;
}
protected void _setContentLength(int length)
{
setIntHeader(HEADER_CONTENT_LENGTH, length);
}
protected void _sendRedirect(String location)
{
clearBuffer();
// TODO : correctly handle absolute and relative locations
setStatus(SC_302_MOVED_TEMPORARILY);
setHeader(HEADER_LOCATION, location);
// complete
}
protected OutputStream _getOutputStream() throws IOException
{
return mMockOutputStream;
}
public Response createEmbeddedResponse(String valueId, String differentiator)
{
MockResponse response = new MockResponse(mMockConversation, getRequest(), true);
mEmbeddedResponses.put(valueId, response);
return response;
}
public void addCookie(Cookie cookie)
{
mNewCookies.put(MockConversation.buildCookieId(cookie), cookie);
mMockConversation.addCookie(cookie);
}
/**
* Retrieves the embedded responses that were processed.
*
* @return the collection of embedded responses; or
* <p>an empty collection if no embedded elements were processed
* @since 1.4
*/
public List<MockResponse> getEmbeddedResponses()
{
return new ArrayList<MockResponse>(mEmbeddedResponses.values());
}
/**
* Retrieves the embedded response that corresponds to a specific
* value in the embedding template.
*
* @param valueId the template value in which the embedded element has
* been processed, the "ELEMENT:" prefix is optional and will be
* automatically added if you leave it off
* @return the embedded responses that corresponds to the value; or
* <p><code>null</code> if no such value could be found
* @since 1.4
*/
public MockResponse getEmbeddedResponse(String valueId)
{
if (valueId != null &&
!valueId.startsWith(ElementContext.PREFIX_ELEMENT))
{
valueId = ElementContext.PREFIX_ELEMENT+valueId;
}
return mEmbeddedResponses.get(valueId);
}
/**
* Retrieves the list of cookies that have been added in this reponse.
*
* @return the list of added cookies; or
* <p>an empty list if no cookies have been added
* @since 1.1
*/
public List<String> getNewCookieNames()
{
ArrayList<String> names = new ArrayList<String>();
for (Cookie cookie : mNewCookies.values())
{
if (!names.contains(cookie.getName()))
{
names.add(cookie.getName());
}
}
return names;
}
/**
* Returns the value of the specified response header as a long value that
* represents a Date object. Use this method with headers that contain
* dates.
* <p>The date is returned as the number of milliseconds since January 1,
* 1970 GMT. The header name is case insensitive.
* <p>If the response did not have a header of the specified name, this
* method returns <code>-1</code>. If the header can't be converted to a
* date, the method throws an <code>IllegalArgumentException</code>.
*
* @param name the name of the header
* @exception java.lang.IllegalArgumentException if the header value can't
* be converted to a date
* @return a <code>long</code> value representing the date specified in
* the header expressed as the number of milliseconds since January 1,
* 1970 GMT; or
* <p><code>-1</code> if the named header was not included with the
* response
* @since 1.1
*/
public long getDateHeader(String name)
{
return mHeaders.getDateHeader(name);
}
/**
* Returns the value of the specified response header as a
* <code>String</code>. If the reponse did not include a header of the
* specified name, this method returns <code>null</code>. The header name
* is case insensitive. You can use this method with any response header.
*
* @param name the name of the header
* @return a <code>String</code> containing the value of the response
* header; or
* <p><code>null</code> if the response does not have a header of that
* name
* @since 1.1
*/
public String getHeader(String name)
{
return mHeaders.getHeader(name);
}
/**
* Returns the value of the specified response header as a
* <code>String</code>. If the reponse did not include a header of the
* specified name, this method returns <code>null</code>. The header name
* is case insensitive. You can use this method with any response header.
*
* @return a <code>Collection</code> of all the header names sent with
* this response; or
* <p>if the response has no headers, an empty <code>Collection</code>
* @since 1.1
*/
public Collection getHeaderNames()
{
return mHeaders.getHeaderNames();
}
/**
* Returns all the values of the specified response header as an
* <code>Collection</code> of <code>String</code> objects.
* <p>If the response did not include any headers of the specified name,
* this method returns an empty <code>Collection</code>. The header name
* is case insensitive. You can use this method with any response header.
*
* @param name the name of the header
* @return a <code>Collection</code> containing the values of the response
* header; or
* <p>if the response does not have any headers of that name return an
* empty <code>Collection</code>
* @since 1.1
*/
public Collection getHeaders(String name)
{
return mHeaders.getHeaders(name);
}
/**
* Returns the value of the specified response header as an
* <code>int</code>. If the response does not have a header of the
* specified name, this method returns <code>-1</code>. If the header
* cannot be converted to an <code>integer</code>, this method throws a
* <code>NumberFormatException</code>.
* <p>The header name is case insensitive.
*
* @param name the name of the header
* @return an <code>integer</code> expressing the value of the response
* header; or
* <p><code>-1</code> if the response doesn't have a header of this name
* @exception java.lang.NumberFormatException if the header value can't be
* converted to an <code>int</code>
* @since 1.1
*/
public int getIntHeader(String name)
{
return mHeaders.getIntHeader(name);
}
public void addHeader(String name, String value)
{
mHeaders.addHeader(name, value);
}
public void addDateHeader(String name, long date)
{
mHeaders.addDateHeader(name, date);
}
public void addIntHeader(String name, int integer)
{
mHeaders.addIntHeader(name, integer);
}
public boolean containsHeader(String name)
{
return mHeaders.containsHeader(name);
}
public void setDateHeader(String name, long date)
{
mHeaders.setDateHeader(name, date);
}
public void setHeader(String name, String value)
{
mHeaders.setHeader(name, value);
}
public void setIntHeader(String name, int value)
{
mHeaders.setIntHeader(name, value);
}
/**
* Removes a response header with the given name.
*
* @param name the name of the header to remove
* @since 1.1
*/
public void removeHeader(String name)
{
mHeaders.removeHeader(name);
}
/**
* Returns the status code of this response.
*
* @return an <code>integer</code> expressing the status code of this
* response
* @since 1.2
*/
public int getStatus()
{
return mStatus;
}
/**
* Returns the error reason of this response.
*
* @return an <code>String</code> expressing the reason of this response error
* @since 1.2
*/
public String getReason()
{
return mReason;
}
public void setStatus(int statusCode)
{
mStatus = statusCode;
}
public void sendError(int statusCode)
throws EngineException
{
sendError(statusCode, null);
}
public void sendError(int statusCode, String message)
throws EngineException
{
mStatus = statusCode;
mReason = message;
}
public String encodeURL(String url)
{
MockRequest request = (MockRequest)getRequest();
// should not encode if cookies in evidence
if (null == request ||
request.isRequestedSessionIdFromCookie())
{
return url;
}
// get session
HttpSession session = getRequest().getSession(false);
// no session or no url
if (null == session || null == url)
{
return url;
}
// invalid session
String id = session.getId();
if (null == id)
{
return url;
}
// Already encoded
int prefix = url.indexOf(MockConversation.SESSION_URL_PREFIX);
if (prefix != -1)
{
int suffix = url.indexOf("?", prefix);
if (suffix < 0)
{
suffix = url.indexOf("#", prefix);
}
if (suffix <= prefix)
{
return url.substring(0, prefix + MockConversation.SESSION_URL_PREFIX.length()) + id;
}
return url.substring(0, prefix + MockConversation.SESSION_URL_PREFIX.length()) + id + url.substring(suffix);
}
// edit the session
int suffix = url.indexOf('?');
if (suffix < 0)
{
suffix = url.indexOf('#');
}
if (suffix < 0)
{
return url + MockConversation.SESSION_URL_PREFIX + id;
}
return url.substring(0, suffix) + MockConversation.SESSION_URL_PREFIX + id + url.substring(suffix);
}
public void setLocale(Locale locale)
{
if (null == locale)
{
return;
}
mLocale = locale;
setHeader(HEADER_CONTENT_LANGUAGE, locale.toString().replace('_', '-'));
}
public Locale getLocale()
{
if (null == mLocale)
{
return Locale.getDefault();
}
return mLocale;
}
public PrintWriter getWriter()
throws IOException
{
mMockOutputStream.flush();
/* if there is no writer yet */
if (mMockWriter == null)
{
/* get encoding from Content-Type header */
String encoding = getCharacterEncoding();
if (encoding == null)
{
encoding = StringUtils.ENCODING_ISO_8859_1;
}
setCharacterEncoding(encoding);
/* construct Writer using correct encoding */
mMockWriter = new PrintWriter(new OutputStreamWriter(mMockOutputStream, encoding));
}
return mMockWriter;
}
private void setCharacterEncoding(String encoding)
{
if (null == encoding)
{
// Clear any encoding.
if (mCharacterEncoding != null)
{
mCharacterEncoding = null;
setHeader(HEADER_CONTENT_TYPE, mContentType);
}
}
else
{
// No, so just add this one to the mimetype
mCharacterEncoding = encoding;
if (mContentType != null)
{
setHeader(HEADER_CONTENT_TYPE,
mContentType + ";charset=" +
QuotedStringTokenizer.quote(mCharacterEncoding, ";= "));
}
}
}
public HttpServletResponse getHttpServletResponse()
{
return null;
}
// ========================================================================
// Copyright 1999-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
/* ------------------------------------------------------------ */
/**
* StringTokenizer with Quoting support. This class is a copy of the
* java.util.StringTokenizer API and the behaviour is the same, except
* that single and doulbe quoted string values are recognized. Delimiters
* within quotes are not considered delimiters. Quotes can be escaped with
* '\'.
*
* @see java.util.StringTokenizer
* @author Greg Wilkins (gregw)
*/
static class QuotedStringTokenizer
extends StringTokenizer
{
private final static String __delim="\t\n\r";
private String _string;
private String _delim = __delim;
private boolean _returnQuotes=false;
private boolean _returnTokens=false;
private StringBuilder _token;
private boolean _hasToken=false;
private int _i=0;
private int _lastStart=0;
/* ------------------------------------------------------------ */
public QuotedStringTokenizer(String str,
String delim,
boolean returnTokens,
boolean returnQuotes)
{
super("");
_string = str;
if (delim != null)
_delim = delim;
_returnTokens = returnTokens;
_returnQuotes = returnQuotes;
if (_delim.indexOf('\'') >= 0 ||
_delim.indexOf('"') >= 0)
throw new Error("Can't use quotes as delimiters: " + _delim);
_token = new StringBuilder(_string.length() > 1024 ? 512: _string.length() / 2);
}
/* ------------------------------------------------------------ */
public QuotedStringTokenizer(String str,
String delim,
boolean returnTokens)
{
this(str, delim, returnTokens, false);
}
/* ------------------------------------------------------------ */
public QuotedStringTokenizer(String str,
String delim)
{
this(str, delim, false, false);
}
/* ------------------------------------------------------------ */
public QuotedStringTokenizer(String str)
{
this(str, null, false, false);
}
/* ------------------------------------------------------------ */
public boolean hasMoreTokens()
{
// Already found a token
if (_hasToken)
return true;
_lastStart = _i;
int state=0;
boolean escape=false;
while (_i < _string.length())
{
char c=_string.charAt(_i++);
switch (state)
{
case 0: // Start
if (_delim.indexOf(c) >= 0)
{
if (_returnTokens)
{
_token.append(c);
return _hasToken = true;
}
}
else if (c == '\'')
{
if (_returnQuotes)
_token.append(c);
state = 2;
}
else if (c == '\"')
{
if (_returnQuotes)
_token.append(c);
state = 3;
}
else
{
_token.append(c);
_hasToken = true;
state = 1;
}
continue;
case 1: // Token
_hasToken = true;
if (_delim.indexOf(c) >= 0)
{
if (_returnTokens)
_i--;
return _hasToken;
}
else if (c == '\'')
{
if (_returnQuotes)
_token.append(c);
state = 2;
}
else if (c == '\"')
{
if (_returnQuotes)
_token.append(c);
state = 3;
}
else
_token.append(c);
continue;
case 2: // Single Quote
_hasToken = true;
if (escape)
{
escape = false;
_token.append(c);
}
else if (c == '\'')
{
if (_returnQuotes)
_token.append(c);
state = 1;
}
else if (c == '\\')
{
if (_returnQuotes)
_token.append(c);
escape = true;
}
else
_token.append(c);
continue;
case 3: // Double Quote
_hasToken = true;
if (escape)
{
escape = false;
_token.append(c);
}
else if (c == '\"')
{
if (_returnQuotes)
_token.append(c);
state = 1;
}
else if (c == '\\')
{
if (_returnQuotes)
_token.append(c);
escape = true;
}
else
_token.append(c);
continue;
}
}
return _hasToken;
}
/* ------------------------------------------------------------ */
public String nextToken()
throws NoSuchElementException
{
if (!hasMoreTokens() || _token == null)
throw new NoSuchElementException();
String t=_token.toString();
_token.setLength(0);
_hasToken = false;
return t;
}
/* ------------------------------------------------------------ */
public String nextToken(String delim)
throws NoSuchElementException
{
_delim = delim;
_i = _lastStart;
_token.setLength(0);
_hasToken = false;
return nextToken();
}
/* ------------------------------------------------------------ */
public boolean hasMoreElements()
{
return hasMoreTokens();
}
/* ------------------------------------------------------------ */
public Object nextElement()
throws NoSuchElementException
{
return nextToken();
}
/* ------------------------------------------------------------ */
/**
* Not implemented.
*/
public int countTokens()
{
return -1;
}
/* ------------------------------------------------------------ */
/**
* Quote a string. The string is quoted only if quoting is required
* due to embeded delimiters, quote characters or the empty string.
*
* @param s The string to quote.
* @return quoted string
*/
public static String quote(String s, String delim)
{
if (s == null)
return null;
if (s.length() == 0)
return "\"\"";
for (int i=0;i < s.length();i++)
{
char c = s.charAt(i);
if (c == '"' ||
c == '\\' ||
c == '\'' ||
delim.indexOf(c) >= 0)
{
StringBuilder b=new StringBuilder(s.length() + 8);
quote(b, s);
return b.toString();
}
}
return s;
}
/* ------------------------------------------------------------ */
/**
* Quote a string into a StringBuilder.
*
* @param buf The StringBuilder
* @param s The String to quote.
*/
public static void quote(StringBuilder buf, String s)
{
buf.append('"');
for (int i=0;i < s.length();i++)
{
char c = s.charAt(i);
if (c == '"')
{
buf.append("\\\"");
continue;
}
if (c == '\\')
{
buf.append("\\\\");
continue;
}
buf.append(c);
continue;
}
buf.append('"');
}
/* ------------------------------------------------------------ */
/**
* Unquote a string.
*
* @param s The string to unquote.
* @return quoted string
*/
public static String unquote(String s)
{
if (s == null)
return null;
if (s.length() < 2)
return s;
char first=s.charAt(0);
char last=s.charAt(s.length() - 1);
if (first != last || (first != '"' && first != '\''))
return s;
StringBuilder b=new StringBuilder(s.length() - 2);
boolean quote=false;
for (int i=1;i < s.length() - 1;i++)
{
char c = s.charAt(i);
if (c == '\\' && !quote)
{
quote = true;
continue;
}
quote = false;
b.append(c);
}
return b.toString();
}
}
}