/*
* Copyright 2010 Philipp Erlacher
*
* 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.
*/
package org.exolab.castor.xml;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.MapItem;
import org.exolab.castor.xml.UnmarshalHandler.ArrayHandler;
import org.exolab.castor.xml.util.XMLFieldDescriptorImpl;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/**
* A processor that assists {@link UnmarshalHandler} in dealing with the SAX 2
* {@link ContentHandler#endElement(String, String, String)} callback method.
*
* @author <a href=" mailto:philipp.erlacher AT gmail DOT com">Philipp
* Erlacher</a>
* @since 1.3.2
*/
public class EndElementProcessor {
/**
* Standard logger to use.
*/
private static final Log LOG = LogFactory.getLog(EndElementProcessor.class);
/**
* resource bundle
*/
protected static ResourceBundle resourceBundle;
/**
* Callback {@link UnmarshalHandler} reference to set the actual state on
* this instance.
*/
private final UnmarshalHandler _unmarshalHandler;
static {
resourceBundle = ResourceBundle.getBundle("UnmarshalHandlerMessages",
Locale.getDefault());
}
/**
* Creates an instance of this class, with a reference to the actual
* {@link UnmarshalHandler} for which this processor deals with the SAX 2
* endElement() callback method.
*
* @param unmarshalHandler
* The {@link UnmarshalHandler} instance on which the results of
* processing the endElement method will be 'persisted'/set.
*/
public EndElementProcessor(final UnmarshalHandler unmarshalHandler) {
_unmarshalHandler = unmarshalHandler;
}
public void compute(String name) throws org.xml.sax.SAXException {
if (LOG.isTraceEnabled()) {
String trace = MessageFormat.format(resourceBundle
.getString("unmarshalHandler.log.trace.endElement"),
new Object[] { name });
LOG.trace(trace);
}
// -- If we are skipping elements that have appeared in the XML but for
// -- which we have no mapping, decrease the ignore depth counter and
// return
if (_unmarshalHandler.getStrictElementHandler().skipEndElement()) {
return;
}
// -- Do delagation if necessary
if (_unmarshalHandler.getAnyNodeHandler().hasAnyUnmarshaller()) {
_unmarshalHandler.getAnyNodeHandler().endElement(name);
// we are back to the starting node
if (_unmarshalHandler.getAnyNodeHandler().isStartingNode()) {
_unmarshalHandler.setAnyNode(_unmarshalHandler
.getAnyNodeHandler().getStartingNode());
} else
return;
}
if (_unmarshalHandler.getStateStack().isEmpty()) {
String err = MessageFormat.format(resourceBundle
.getString("unmarshalHandler.error.missing.startElement"),
new Object[] { name });
throw new SAXException(err);
}
// -- * Begin Namespace Handling
// -- XXX Note: This code will change when we update the XML event API
int idx = name.indexOf(':');
if (idx >= 0) {
name = name.substring(idx + 1);
}
// -- * End Namespace Handling
UnmarshalState state = _unmarshalHandler.getStateStack()
.removeLastState();
// -- make sure we have the correct closing tag
XMLFieldDescriptor descriptor = state.getFieldDescriptor();
if (!state.getElementName().equals(name)) {
// maybe there is still a container to end
if (descriptor.isContainer()) {
_unmarshalHandler.getStateStack().pushState(state);
// -- check for possible characters added to
// -- the container's state that should
// -- really belong to the parent state
StringBuffer tmpBuffer = null;
if (state.getBuffer() != null) {
if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
if (state.getClassDescriptor().getContentDescriptor() == null) {
tmpBuffer = state.getBuffer();
state.setBuffer(null);
}
}
}
// -- end container
_unmarshalHandler.endElement(state.getElementName());
if (tmpBuffer != null) {
state = _unmarshalHandler.getStateStack().getLastState();
if (state.getBuffer() == null)
state.setBuffer(tmpBuffer);
else
state.getBuffer().append(tmpBuffer.toString());
}
_unmarshalHandler.endElement(name);
return;
}
String err = MessageFormat
.format(resourceBundle
.getString("unmarshalHandler.error.different.endElement.expected"),
new Object[] { state.getElementName(), name });
throw new SAXException(err);
}
// -- clean up current Object
Class<?> type = state.getType();
if (type == null) {
if (!state.isWrapper()) {
// -- this message will only show up if debug
// -- is turned on...how should we handle this case?
// -- should it be a fatal error?
String info = MessageFormat
.format(resourceBundle
.getString("unmarshalHandler.log.info.no.Descriptor.found"),
new Object[] { state.getElementName() });
LOG.info(info);
}
// -- handle possible location text content
// -- TODO: cleanup location path support.
// -- the following code needs to be improved as
// -- for searching descriptors in this manner can
// -- be slow
StringBuffer tmpBuffer = null;
if (state.getBuffer() != null) {
if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
tmpBuffer = state.getBuffer();
state.setBuffer(null);
}
}
if (tmpBuffer != null) {
UnmarshalState targetState = state;
String locPath = targetState.getElementName();
while ((targetState = targetState.getParent()) != null) {
if ((targetState.isWrapper())
|| (targetState.getClassDescriptor() == null)) {
locPath = targetState.getElementName() + "/" + locPath;
continue;
}
XMLFieldDescriptor tmpDesc = targetState.getClassDescriptor()
.getContentDescriptor();
if (tmpDesc != null
&& locPath.equals(tmpDesc.getLocationPath())) {
if (targetState.getBuffer() == null)
targetState.setBuffer(tmpBuffer);
else
targetState.getBuffer().append(tmpBuffer.toString());
}
}
}
// -- remove current namespace scoping
_unmarshalHandler.getNamespaceHandling()
.removeCurrentNamespaceInstance();
return;
}
// -- check for special cases
boolean byteArray = false;
if (type.isArray()) {
byteArray = (type.getComponentType() == Byte.TYPE);
}
// -- If we don't have an instance object and the Class type
// -- is not a primitive or a byte[] we must simply return
if ((state.getObject() == null) && (!state.isPrimitiveOrImmutable())) {
// -- remove current namespace scoping
_unmarshalHandler.getNamespaceHandling()
.removeCurrentNamespaceInstance();
return;
}
// / DEBUG System.out.println("end: " + name);
if (state.isPrimitiveOrImmutable()) {
String str = null;
if (state.getBuffer() != null) {
str = state.getBuffer().toString();
state.getBuffer().setLength(0);
}
if (type == String.class
&& !((XMLFieldDescriptorImpl) descriptor)
.isDerivedFromXSList()) {
if (str != null)
state.setObject(str);
else if (state.isNil()) {
state.setObject(null);
} else {
state.setObject("");
}
}
// -- special handling for byte[]
else if (byteArray && !descriptor.isDerivedFromXSList()) {
if (str == null)
state.setObject(new byte[0]);
else {
state.setObject(_unmarshalHandler.decodeBinaryData(
descriptor, str));
}
} else if (state.getConstructorArguments() != null) {
state.setObject(_unmarshalHandler.createInstance(state.getType(),
state.getConstructorArguments()));
} else if (descriptor.isMultivalued()
&& descriptor.getSchemaType() != null
&& descriptor.getSchemaType().equals("list")
&& ((XMLFieldDescriptorImpl) descriptor)
.isDerivedFromXSList()) {
StringTokenizer attrValueTokenizer = new StringTokenizer(str);
List<Object> primitives = new ArrayList<Object>();
while (attrValueTokenizer.hasMoreTokens()) {
String tokenValue = attrValueTokenizer.nextToken();
if (MarshalFramework.isPrimitive(descriptor.getFieldType())) {
primitives.add(_unmarshalHandler.toPrimitiveObject(
type, tokenValue, state.getFieldDescriptor()));
} else {
Class<?> valueType = descriptor.getFieldType();
// -- handle base64/hexBinary
if (valueType.isArray()
&& (valueType.getComponentType() == Byte.TYPE)) {
primitives.add(_unmarshalHandler.decodeBinaryData(
descriptor, tokenValue));
}
}
}
state.setObject(primitives);
} else {
if (state.isNil()) {
state.setObject(null);
} else {
state.setObject(_unmarshalHandler.toPrimitiveObject(type,
str, state.getFieldDescriptor()));
}
}
} else if (ArrayHandler.class.isAssignableFrom(state.getType())) {
state.setObject(((ArrayHandler) state.getObject()).getObject());
state.setType(state.getObject().getClass());
}
// -- check for character content
if ((state.getBuffer() != null) && (state.getBuffer().length() > 0)
&& (state.getClassDescriptor() != null)) {
XMLFieldDescriptor cdesc = state.getClassDescriptor().getContentDescriptor();
if (cdesc != null) {
Object value = state.getBuffer().toString();
if (MarshalFramework.isPrimitive(cdesc.getFieldType()))
value = _unmarshalHandler.toPrimitiveObject(
cdesc.getFieldType(), (String) value,
state.getFieldDescriptor());
else {
Class<?> valueType = cdesc.getFieldType();
// -- handle base64/hexBinary
if (valueType.isArray()
&& (valueType.getComponentType() == Byte.TYPE)) {
value = _unmarshalHandler.decodeBinaryData(descriptor,
(String) value);
}
}
try {
FieldHandler handler = cdesc.getHandler();
boolean addObject = true;
if (_unmarshalHandler.isReuseObjects()) {
// -- check to see if we need to
// -- add the object or not
Object tmp = handler.getValue(state.getObject());
if (tmp != null) {
// -- Do not add object if values
// -- are equal
addObject = (!tmp.equals(value));
}
}
if (addObject)
handler.setValue(state.getObject(), value);
} catch (java.lang.IllegalStateException ise) {
String err = MessageFormat
.format(resourceBundle
.getString("unmarshalHandler.error.unable.add.text"),
new Object[] { descriptor.getXMLName(),
ise.toString() });
throw new SAXException(err, ise);
}
}
// -- Handle references
else if (descriptor.isReference()) {
UnmarshalState pState = _unmarshalHandler.getStateStack()
.getLastState();
_unmarshalHandler.processIDREF(state.getBuffer().toString(),
descriptor, pState.getObject());
_unmarshalHandler.getNamespaceHandling()
.removeCurrentNamespaceInstance();
return;
} else {
// -- check for non-whitespace...and report error
if (!UnmarshalHandler.isWhitespace(state.getBuffer())) {
String err = MessageFormat.format(resourceBundle
.getString("unmarshalHandler.error.illegal.text"),
new Object[] { name, state.getBuffer() });
throw new SAXException(err);
}
}
}
// -- We're finished processing the object, so notify the
// -- Listener (if any).
Object stateObject = state.getObject();
Object parentObject = (state.getParent() == null) ? null
: state.getParent().getObject();
_unmarshalHandler.getDelegateUnmarshalListener().unmarshalled(
stateObject, parentObject);
// -- if we are at root....just validate and we are done
if (_unmarshalHandler.getStateStack().isEmpty()) {
if (_unmarshalHandler.isValidating()) {
ValidationException first = null;
ValidationException last = null;
// -- check unresolved references
if (_unmarshalHandler.getResolveTable() != null
&& !_unmarshalHandler.getInternalContext()
.getLenientIdValidation()) {
Enumeration enumeration = _unmarshalHandler
.getResolveTable().keys();
while (enumeration.hasMoreElements()) {
Object ref = enumeration.nextElement();
// if
// (ref.toString().startsWith(MapItem.class.getName()))
// continue;
String msg = "unable to resolve reference: " + ref;
if (first == null) {
first = new ValidationException(msg);
last = first;
} else {
last.setNext(new ValidationException(msg));
last = last.getNext();
}
}
}
try {
Validator validator = new Validator();
ValidationContext context = new ValidationContext();
context.setInternalContext(_unmarshalHandler
.getInternalContext());
validator.validate(state.getObject(), context);
if (!_unmarshalHandler.getInternalContext()
.getLenientIdValidation()) {
validator.checkUnresolvedIdrefs(context);
}
context.cleanup();
} catch (ValidationException vEx) {
if (first == null)
first = vEx;
else
last.setNext(vEx);
}
if (first != null) {
throw new SAXException(first);
}
}
return;
}
// -- Add object to parent if necessary
if (descriptor.isIncremental()) {
// -- remove current namespace scoping
_unmarshalHandler.getNamespaceHandling()
.removeCurrentNamespaceInstance();
return; // -- already added
}
Object val = state.getObject();
// --special code for AnyNode handling
if (_unmarshalHandler.getAnyNode() != null) {
val = _unmarshalHandler.getAnyNode();
_unmarshalHandler.setAnyNode(null);
}
// -- save fieldState
UnmarshalState fieldState = state;
// -- have we seen this object before?
boolean firstOccurance = false;
// -- get target object
state = _unmarshalHandler.getStateStack().getLastState();
if (state.isWrapper()) {
state = fieldState.getTargetState();
}
// -- check to see if we have already read in
// -- an element of this type.
// -- (Q: if we have a container, do we possibly need to
// -- also check the container's multivalued status?)
if (!descriptor.isMultivalued()) {
if (state.isUsed(descriptor)) {
String location = name;
while (!_unmarshalHandler.getStateStack().isEmpty()) {
UnmarshalState tmpState = _unmarshalHandler.getStateStack()
.removeLastState();
if (!tmpState.isWrapper()) {
if (tmpState.getFieldDescriptor().isContainer())
continue;
}
location = state.getElementName() + "/" + location;
}
String err = MessageFormat
.format(resourceBundle
.getString("unmarshalHandler.error.element.occurs.more.than.once"),
new Object[] { name, state.getType().getName(),
location });
ValidationException vx = new ValidationException(err);
throw new SAXException(vx);
}
state.markAsUsed(descriptor);
// -- if this is the identity then save id
if (state.getClassDescriptor().getIdentity() == descriptor) {
state.setKey(val);
}
} else {
// -- check occurance of descriptor
if (!state.isUsed(descriptor)) {
firstOccurance = true;
}
// -- record usage of descriptor
state.markAsUsed(descriptor);
}
try {
FieldHandler handler = descriptor.getHandler();
// check if the value is a QName that needs to
// be resolved (ns:value -> {URI}value)
String valueType = descriptor.getSchemaType();
if ((valueType != null)
&& (valueType.equals(MarshalFramework.QNAME_NAME))) {
val = _unmarshalHandler.getNamespaceHandling()
.resolveNamespace(val);
}
boolean addObject = true;
if (_unmarshalHandler.isReuseObjects()
&& fieldState.isPrimitiveOrImmutable()) {
// -- check to see if we need to
// -- add the object or not
Object tmp = handler.getValue(state.getObject());
if (tmp != null) {
// -- Do not add object if values
// -- are equal
addObject = (!tmp.equals(val));
}
}
// -- special handling for mapped objects
if (descriptor.isMapped()) {
if (!(val instanceof MapItem)) {
MapItem mapItem = new MapItem(fieldState.getKey(), val);
val = mapItem;
} else {
// -- make sure value exists (could be a reference)
MapItem mapItem = (MapItem) val;
if (mapItem.getValue() == null) {
// -- save for later...
addObject = false;
_unmarshalHandler.addReference(mapItem.toString(),
state.getObject(), descriptor);
}
}
}
if (addObject) {
// -- clear any collections if necessary
if (firstOccurance && _unmarshalHandler.isClearCollections()) {
handler.resetValue(state.getObject());
}
if (descriptor.isMultivalued()
&& descriptor.getSchemaType() != null
&& descriptor.getSchemaType().equals("list")
&& ((XMLFieldDescriptorImpl) descriptor)
.isDerivedFromXSList()) {
List<Object> values = (List<Object>) val;
for (Object value : values) {
// -- finally set the value!!
handler.setValue(state.getObject(), value);
// If there is a parent for this object, pass along
// a notification that we've finished adding a child
_unmarshalHandler.getDelegateUnmarshalListener()
.fieldAdded(descriptor.getFieldName(),
state.getObject(), fieldState.getObject());
}
} else {
// -- finally set the value!!
handler.setValue(state.getObject(), val);
// If there is a parent for this object, pass along
// a notification that we've finished adding a child
_unmarshalHandler.getDelegateUnmarshalListener()
.fieldAdded(descriptor.getFieldName(),
state.getObject(), fieldState.getObject());
}
}
}
/*
* catch(java.lang.reflect.InvocationTargetException itx) {
*
* Throwable toss = itx.getTargetException(); if (toss == null) toss =
* itx;
*
* String err = "unable to add '" + name + "' to <"; err +=
* state.descriptor.getXMLName(); err +=
* "> due to the following exception: " + toss; throw new
* SAXException(err); }
*/
catch (Exception ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
pw.flush();
String err = MessageFormat
.format(resourceBundle
.getString("unmarshalHandler.error.unable.add.element"),
new Object[] { name, state.getFieldDescriptor().getXMLName(),
sw.toString() });
throw new SAXException(err, ex);
}
// -- remove current namespace scoping
_unmarshalHandler.getNamespaceHandling()
.removeCurrentNamespaceInstance();
// remove additional (artifical aka container) state introduced for
// single-valued (iow maxOccurs="1") choices.
if (state.getFieldDescriptor().isContainer() && state.getClassDescriptor().isChoice()
&& !state.getFieldDescriptor().isMultivalued()) {
_unmarshalHandler.endElement(state.getElementName());
}
}
}