org.xins.client.XINSCallResultParser Maven / Gradle / Ivy
/*
* $Id: XINSCallResultParser.java,v 1.52 2006/08/28 09:12:30 agoubard Exp $
*
* Copyright 2003-2006 Orange Nederland Breedband B.V.
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.client;
import java.io.ByteArrayInputStream;
import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.collections.PropertyReader;
import org.xins.common.collections.ProtectedPropertyReader;
import org.xins.common.text.FastStringBuffer;
import org.xins.common.text.ParseException;
import org.xins.common.text.TextUtils;
import org.xins.common.xml.SAXParserProvider;
/**
* XINS call result parser. XML is parsed to produce a {@link XINSCallResult}
* object.
*
* The root element in the XML must be of type result
. Inside
* this element, param
elements optionally define parameters and
* an optional data
element defines a data section.
*
*
If the result element contains an errorcode
or a
* code
attribute, then the value of the attribute is interpreted
* as the error code. If both these attributes are set and conflicting, then
* this is considered a showstopper.
*
*
TODO: Describe rest of parse process.
*
*
Note: This parser is
* XML Namespaces-aware.
*
* @version $Revision: 1.52 $ $Date: 2006/08/28 09:12:30 $
*
* @author Anthony Goubard
* @author Ernst de Haan
*
* @since XINS 1.0.0
*/
public class XINSCallResultParser
extends Object {
//-------------------------------------------------------------------------
// Class fields
//-------------------------------------------------------------------------
/**
* Fully-qualified name of this class. This field is not null
.
*/
private static final String CLASSNAME = XINSCallResultParser.class.getName();
/**
* Fully-qualified name of the inner class Handler
. This field
* is not null
.
*/
private static final String HANDLER_CLASSNAME = XINSCallResultParser.Handler.class.getName();
/**
* The key for the ProtectedPropertyReader
instances created
* by this class.
*/
private static final Object PROTECTION_KEY = new Object();
/**
* Error state for the SAX event handler.
*/
private static final State ERROR = new State("ERROR");
/**
* Initial state for the SAX event handler, before the root element is
* processed.
*/
private static final State INITIAL = new State("INITIAL");
/**
* State for the SAX event handler just within the root element
* (result
).
*/
private static final State AT_ROOT_LEVEL = new State("AT_ROOT_LEVEL");
/**
* State for the SAX event handler at any depth within an ignorable
* element.
*/
private static final State IN_IGNORABLE_ELEMENT =
new State("IN_IGNORABLE_ELEMENT");
/**
* State for the SAX event handler within the output parameter element
* (param
).
*/
private static final State IN_PARAM_ELEMENT =
new State("IN_PARAM_ELEMENT");
/**
* State for the SAX event handler in the data section (at any depth within
* the data
element).
*/
private static final State IN_DATA_SECTION = new State("IN_DATA_SECTION");
/**
* State for the SAX event handler for the final state, when parsing is
* finished.
*/
private static final State FINISHED = new State("FINISHED");
//-------------------------------------------------------------------------
// Constructors
//-------------------------------------------------------------------------
/**
* Constructs a new XINSCallResultParser
.
*/
public XINSCallResultParser() {
// empty
}
//-------------------------------------------------------------------------
// Methods
//-------------------------------------------------------------------------
/**
* Parses the given XML string to create a XINSCallResultData
* object.
*
* @param xml
* the XML to be parsed, not null
.
*
* @return
* the parsed result of the call, not null
.
*
* @throws IllegalArgumentException
* if xml == null
.
*
* @throws ParseException
* if the specified string is not valid XML or if it is not a valid XINS
* API function call result.
*/
public XINSCallResultData parse(byte[] xml)
throws IllegalArgumentException, ParseException {
final String THIS_METHOD = "parse(byte[])";
// Check preconditions
MandatoryArgumentChecker.check("xml", xml);
// Initialize our SAX event handler
Handler handler = new Handler();
ByteArrayInputStream stream = null;
try {
// Convert the byte array to an input stream
stream = new ByteArrayInputStream(xml);
// Let SAX parse the XML, using our handler
SAXParserProvider.get().parse(stream, handler);
} catch (Throwable exception) {
// Log: Parsing failed
String detail = exception.getMessage();
Log.log_2205(exception, detail);
// Include the exception message in our error message, if any
String message = "Unable to convert the specified string to XML.";
if (detail != null) {
detail = detail.trim();
if (detail.length() > 0) {
FastStringBuffer buffer = new FastStringBuffer(182,
"Unable to convert the specified string to XML: ");
buffer.append(detail);
message = buffer.toString();
}
}
// Throw exception with message, and register cause exception
throw new ParseException(message, exception, detail);
// Always dispose the ByteArrayInputStream
} finally {
if (stream != null) {
try {
stream.close();
} catch (Throwable exception) {
final String SUBJECT_CLASS = stream.getClass().getName();
final String SUBJECT_METHOD = "close()";
Utils.logProgrammingError(CLASSNAME, THIS_METHOD,
SUBJECT_CLASS, SUBJECT_METHOD,
null, exception);
}
}
}
return handler;
}
//-------------------------------------------------------------------------
// Inner classes
//-------------------------------------------------------------------------
/**
* SAX event handler that will parse the result from a call to a XINS
* service.
*
* @version $Revision: 1.52 $ $Date: 2006/08/28 09:12:30 $
* @author Anthony Goubard
* @author Ernst de Haan
*/
private static class Handler
extends DefaultHandler
implements XINSCallResultData {
//-------------------------------------------------------------------------
// Constructors
//-------------------------------------------------------------------------
/**
* Constructs a new Handler
instance.
*/
private Handler() {
_state = INITIAL;
_level = -1;
_characters = new FastStringBuffer(145);
_dataElementStack = new Stack();
}
//-------------------------------------------------------------------------
// Fields
//-------------------------------------------------------------------------
/**
* The current state. Never null
.
*/
private State _state;
/**
* The error code returned by the function or null
, if no
* error code is returned.
*
*
The value will never return an empty string, so if the result is
* not null
, then it is safe to assume the length of the
* string is at least 1 character.
*/
private String _errorCode;
/**
* The list of the parameters (name/value) returned by the function.
* This field is lazily initialized.
*/
private ProtectedPropertyReader _parameters;
/**
* The name of the output parameter that is currently being parsed.
*/
private String _parameterName;
/**
* The character content (CDATA or PCDATA) of the element currently
* being parsed.
*/
private final FastStringBuffer _characters;
/**
* The stack of child elements within the data section. The top element
* is always <data/>
.
*/
private Stack _dataElementStack;
/**
* The level for the element pointer within the XML document. Initially
* this field is -1
, which indicates the current element
* pointer is outside the document. The value 0
is for the
* root element (result
), etc.
*/
private int _level;
//-------------------------------------------------------------------------
// Methods
//-------------------------------------------------------------------------
/**
* Receive notification of the beginning of an element.
*
* @param namespaceURI
* the namespace URI, can be null
.
*
* @param localName
* the local name (without prefix); cannot be null
.
*
* @param qName
* the qualified name (with prefix), can be null
since
* namespaceURI
and localName
are always
* used instead.
*
* @param atts
* the attributes attached to the element; if there are no
* attributes, it shall be an empty {@link Attributes} object; cannot
* be null
.
*
* @throws IllegalArgumentException
* if localName == null || atts == null
.
*
* @throws SAXException
* if the parsing failed.
*/
public void startElement(String namespaceURI,
String localName,
String qName,
Attributes atts)
throws IllegalArgumentException, SAXException {
final String THIS_METHOD = "startElement(java.lang.String,"
+ "java.lang.String,"
+ "java.lang.String,"
+ Attributes.class.getName()
+ ')';
// Temporarily enter ERROR state, on success this state is left
State currentState = _state;
_state = ERROR;
// Make sure namespaceURI is either null or non-empty
namespaceURI = "".equals(namespaceURI) ? null : namespaceURI;
// Cache quoted version of namespaceURI
String quotedNamespaceURI = TextUtils.quote(namespaceURI);
// Check preconditions
MandatoryArgumentChecker.check("localName", localName, "atts", atts);
// Increase the element depth level
_level++;
if (currentState == ERROR) {
final String DETAIL = "_state="
+ currentState
+ "; _level="
+ _level;
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
} else if (currentState == INITIAL) {
// Level and state must comply
if (_level != 0) {
final String DETAIL = "_state="
+ currentState
+ "; _level="
+ _level;
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
}
// Root element must be 'result' without namespace
if (! (namespaceURI == null && localName.equals("result"))) {
Log.log_2200(namespaceURI, localName);
final String DETAIL = "Root element is \""
+ localName
+ "\" with namespace "
+ quotedNamespaceURI
+ " instead of \"result\" with namespace"
+ " (null).";
throw new SAXException(DETAIL);
}
// Get the 'errorcode' and 'code attributes
String code1 = atts.getValue("errorcode");
String code2 = atts.getValue("code");
// Convert an empty string to null
if (code1 == null || code1.length() == 0) {
code1 = null;
}
if (code2 == null || code2.length() == 0) {
code2 = null;
}
// Only one error code attribute set
if (code1 != null && code2 == null) {
_errorCode = code1;
} else if (code1 == null && code2 != null) {
_errorCode = code2;
// Two error code attribute set
} else if (code1 == null) {
_errorCode = null;
} else if (code1.equals(code2)) {
_errorCode = code1;
// Conflicting error codes
} else {
// NOTE: No need to log here. This will be logged already in
// Logdoc log message 2205.
String detail = "Found conflicting duplicate value for the "
+ "error code, since attribute errorcode=\""
+ code1
+ "\", while attribute code=\""
+ code2
+ "\".";
throw new SAXException(detail);
}
// Change state
_state = AT_ROOT_LEVEL;
} else if (currentState == AT_ROOT_LEVEL) {
// Output parameter
if (namespaceURI == null && "param".equals(localName)) {
// Store the name of the parameter. It may be null, but that
// will be checked only after the element end tag is processed.
_parameterName = atts.getValue("name");
// TODO: Check parameter name here (null and pattern)
// Reserve buffer for PCDATA
_characters.clear();
// Update the state
_state = IN_PARAM_ELEMENT;
// Start of data section
} else if (namespaceURI == null && "data".equals(localName)) {
// A data element stack should really be empty
if (_dataElementStack.size() > 0) {
throw new SAXException("Found second data section.");
}
// Maintain a list of the elements, with data as the root
_dataElementStack.push(new DataElement(null, "data"));
// Update the state
_state = IN_DATA_SECTION;
// Ignore unrecognized element at root level
} else {
_state = IN_IGNORABLE_ELEMENT;
Log.log_2206(namespaceURI, localName);
}
// Within output parameter element, no elements are allowed
} else if (currentState == IN_PARAM_ELEMENT) {
// NOTE: No need to log here. This will be logged already (message 2205)
String detail = "Found \""
+ localName
+ "\" element with namespace "
+ quotedNamespaceURI
+ " within \"param\" element.";
throw new SAXException(detail);
// Within the data section
} else if (currentState == IN_DATA_SECTION) {
// Construct a DataElement
DataElement element = new DataElement(namespaceURI, localName);
// Add all attributes
for (int i = 0; i < atts.getLength(); i++) {
String attrNamespaceURI = atts.getURI(i);
String attrLocalName = atts.getLocalName(i);
String attrValue = atts.getValue(i);
element.setAttribute(attrNamespaceURI,
attrLocalName,
attrValue);
}
// Push the element on the stack
_dataElementStack.push(element);
// Reserve buffer for PCDATA
_characters.clear();
// Reset the state from ERROR back to IN_DATA_SECTION
_state = IN_DATA_SECTION;
// Deeper level within ignorable element
} else if (currentState == IN_IGNORABLE_ELEMENT) {
_state = IN_IGNORABLE_ELEMENT;
// Unrecognized state
} else {
final String DETAIL = "_state="
+ currentState
+ "; _level="
+ _level;
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
}
}
/**
* Receive notification of the end of an element.
*
* @param namespaceURI
* the namespace URI, can be null
.
*
* @param localName
* the local name (without prefix); cannot be null
.
*
* @param qName
* the qualified name (with prefix), can be null
since
* namespaceURI
and localName
are only
* used.
*
* @throws IllegalArgumentException
* if localName == null
.
*
* @throws SAXException
* if the parsing failed.
*/
public void endElement(String namespaceURI,
String localName,
String qName)
throws IllegalArgumentException, SAXException {
final String THIS_METHOD = "endElement(java.lang.String,"
+ "java.lang.String,"
+ "java.lang.String)";
// Temporarily enter ERROR state, on success this state is left
State currentState = _state;
_state = ERROR;
// Make sure namespaceURI is either null or non-empty
namespaceURI = (namespaceURI != null && "".equals(namespaceURI.trim()))
? null
: namespaceURI;
// Cache quoted version of namespaceURI
String quotedNamespaceURI = TextUtils.quote(namespaceURI);
// Check preconditions
MandatoryArgumentChecker.check("localName", localName);
if (currentState == ERROR) {
final String DETAIL = "_state="
+ currentState
+ "; _level="
+ _level;
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
// At root level
} else if (currentState == AT_ROOT_LEVEL) {
if (! (namespaceURI == null && "result".equals(localName))) {
final String DETAIL = "Expected end of element of type "
+ "\"result\" with namespace (null) "
+ "instead of \""
+ localName
+ "\" with namespace "
+ quotedNamespaceURI
+ '.';
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
}
_state = FINISHED;
// Ignorable element
} else if (currentState == IN_IGNORABLE_ELEMENT) {
if (_level == 1) {
_state = AT_ROOT_LEVEL;
} else {
_state = IN_IGNORABLE_ELEMENT;
}
// Within data section
} else if (currentState == IN_DATA_SECTION) {
// Get the DataElement for which we process the end tag
DataElement child = (DataElement) _dataElementStack.pop();
// If at the element level, then return to AT_ROOT_LEVEL
if (_dataElementStack.size() == 0) {
if (! (namespaceURI == null && "data".equals(localName))) {
final String DETAIL = "Expected end of element of type "
+ "\"data\" with namespace (null) "
+ "instead of \""
+ localName
+ "\" with namespace "
+ quotedNamespaceURI
+ '.';
throw Utils.logProgrammingError(HANDLER_CLASSNAME,
THIS_METHOD,
HANDLER_CLASSNAME,
THIS_METHOD,
DETAIL);
}
// Push the root DataElement back
_dataElementStack.push(child);
// Reset the state
_state = AT_ROOT_LEVEL;
// Otherwise it's a custom element
} else {
// Set the PCDATA content on the element
if (_characters != null && _characters.getLength() > 0) {
child.setText(_characters.toString());
}
// Add the child to the parent
DataElement parent = (DataElement) _dataElementStack.peek();
parent.addChild(child);
// Reset the state back frmo ERROR to IN_DATA_SECTION
_state = IN_DATA_SECTION;
}
// Output parameter
} else if (currentState == IN_PARAM_ELEMENT) {
if (! (namespaceURI == null && "param".equals(localName))) {
final String DETAIL = "Expected end of element of type "
+ "\"param\" with namespace (null) "
+ "instead of \""
+ localName
+ "\" with namespace "
+ quotedNamespaceURI
+ '.';
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
}
// Retrieve name and value for output parameter
String name = _parameterName;
String value = _characters.toString();
// Both name and value should be set
boolean noName = (name == null || name.length() < 1);
boolean noValue = (value == null || value.length() < 1);
if (noName && noValue) {
Log.log_2201();
} else if (noName) {
Log.log_2202(value);
} else if (noValue) {
Log.log_2203(name);
// Name and value are both set, correctly
} else {
Log.log_2204(name, value);
// Previously no parameters, perform (lazy) initialization
if (_parameters == null) {
_parameters = new ProtectedPropertyReader(PROTECTION_KEY);
// Check if parameter is already set
} else {
String existingValue = _parameters.get(name);
if (existingValue != null) {
if (!existingValue.equals(value)) {
// NOTE: This will be logged already (message 2205)
final String DETAIL = "Found conflicting duplicate "
+ "value for output parameter \""
+ name
+ "\". Initial value is \""
+ existingValue
+ "\". New value is \""
+ value +
"\".";
throw new SAXException(DETAIL);
}
}
}
// Store the name-value combination for the output parameter
_parameters.set(PROTECTION_KEY, name, value);
}
// Reset the state
_parameterName = null;
_state = AT_ROOT_LEVEL;
_characters.clear();
// Unknown state
} else {
final String DETAIL = "Unrecognized state: "
+ currentState
+ ". Programming error suspected.";
throw Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, THIS_METHOD,
DETAIL);
}
_level--;
_characters.clear();
}
/**
* Receive notification of character data.
*
* @param ch
* the char
array that contains the characters from the
* XML document, cannot be null
.
*
* @param start
* the start index within ch
.
*
* @param length
* the number of characters to take from ch
.
*
* @throws IndexOutOfBoundsException
* if characters outside the allowed range are specified.
*
* @throws SAXException
* if the parsing failed.
*/
public void characters(char[] ch, int start, int length)
throws IndexOutOfBoundsException, SAXException {
// Temporarily enter ERROR state, on success this state is left
State currentState = _state;
_state = ERROR;
// Check state
if (currentState != IN_PARAM_ELEMENT
&& currentState != IN_DATA_SECTION
&& currentState != IN_IGNORABLE_ELEMENT) {
String text = new String(ch, start, length);
if (text.trim().length() > 0) {
// NOTE: This will be logged already (message 2205)
String detail = "Found character content \""
+ text
+ "\" in state "
+ currentState
+ '.';
throw new SAXException(detail);
}
}
if (_characters != null) {
_characters.append(ch, start, length);
}
// Reset _state
_state = currentState;
}
/**
* Checks if the state is FINISHED
and if not throws an
* IllegalStateException
.
*
* @throws IllegalStateException
* if the current state is not {@link #FINISHED}.
*/
private void assertFinished()
throws IllegalStateException {
if (_state != FINISHED) {
// TODO: Should SUBJECT_METHOD not be something else?
final String THIS_METHOD = "assertFinished()";
final String SUBJECT_METHOD = Utils.getCallingMethod();
final String DETAIL = "State is "
+ _state
+ " instead of "
+ FINISHED
+ '.';
Utils.logProgrammingError(HANDLER_CLASSNAME, THIS_METHOD,
HANDLER_CLASSNAME, SUBJECT_METHOD,
DETAIL);
throw new IllegalStateException(DETAIL);
}
}
/**
* Returns the error code. If null
is returned the call was
* successful and thus no error code was returned. Otherwise the call
* was unsuccessful.
*
*
This method will never return an empty string, so if the result is
* not null
, then it is safe to assume the length of the
* string is at least 1 character.
*
* @return
* the returned error code, or null
if the call was
* successful.
*
* @throws IllegalStateException
* if the current state is invalid.
*/
public String getErrorCode()
throws IllegalStateException {
// Check state
assertFinished();
return _errorCode;
}
/**
* Get the parameters returned by the function.
*
* @return
* the parameters (name/value) or null
if the function
* does not have any parameters.
*
* @throws IllegalStateException
* if the current state is invalid.
*/
public PropertyReader getParameters()
throws IllegalStateException {
// Check state
assertFinished();
return _parameters;
}
/**
* Get the data element returned by the function if any.
*
* @return
* the data element, or null
if the function did not
* return any data element.
*
* @throws IllegalStateException
* if the current state is invalid.
*/
public DataElement getDataElement()
throws IllegalStateException {
// Check state
assertFinished();
if (_dataElementStack.isEmpty()) {
return null;
} else {
return (DataElement) _dataElementStack.peek();
}
}
}
/**
* State of the event handler.
*
* @version $Revision: 1.52 $ $Date: 2006/08/28 09:12:30 $
* @author Ernst de Haan
*/
private static final class State extends Object {
//----------------------------------------------------------------------
// Constructors
//----------------------------------------------------------------------
/**
* Constructs a new State
object.
*
* @param name
* the name of this state, cannot be null
.
*
* @throws IllegalArgumentException
* if name == null
.
*/
private State(String name) throws IllegalArgumentException {
// Check preconditions
MandatoryArgumentChecker.check("name", name);
_name = name;
}
//----------------------------------------------------------------------
// Fields
//----------------------------------------------------------------------
/**
* The name of this state. Cannot be null
.
*/
private final String _name;
//----------------------------------------------------------------------
// Methods
//----------------------------------------------------------------------
/**
* Returns a textual representation of this object.
*
* @return
* the name of this state, never null
.
*/
public String toString() {
return _name;
}
}
}