/*
* Copyright 2001-2004 The Apache Software Foundation.
*
* 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.apache.axis.encoding.ser;
import org.apache.axis.Constants;
import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.encoding.DeserializationContext;
import org.apache.axis.encoding.Deserializer;
import org.apache.axis.encoding.DeserializerImpl;
import org.apache.axis.encoding.DeserializerTarget;
import org.apache.axis.message.SOAPHandler;
import org.apache.axis.utils.ClassUtils;
import org.apache.axis.utils.JavaUtils;
import org.apache.axis.utils.Messages;
import org.apache.axis.wsdl.symbolTable.SchemaUtils;
import org.apache.commons.logging.Log;
import org.apache.axis.soap.SOAPConstants;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringTokenizer;
/**
* An ArrayDeserializer handles deserializing SOAP
* arrays.
*
* Some code borrowed from ApacheSOAP - thanks to Matt Duftler!
*
* @author Glen Daniels (gdaniels@apache.org)
*
* Multi-reference stuff:
* @author Rich Scheuerle (scheu@us.ibm.com)
*/
public class ArrayDeserializer extends DeserializerImpl
{
protected static Log log =
LogFactory.getLog(ArrayDeserializer.class.getName());
public QName arrayType = null;
public int curIndex = 0;
QName defaultItemType;
int length;
Class arrayClass = null;
ArrayList mDimLength = null; // If set, array of multi-dim lengths
ArrayList mDimFactor = null; // If set, array of factors for multi-dim []
SOAPConstants soapConstants = SOAPConstants.SOAP11_CONSTANTS;
/**
* This method is invoked after startElement when the element requires
* deserialization (i.e. the element is not an href & the value is not nil)
* DeserializerImpl provides default behavior, which simply
* involves obtaining a correct Deserializer and plugging its handler.
* @param namespace is the namespace of the element
* @param localName is the name of the element
* @param prefix is the prefix of the element
* @param attributes are the attrs on the element...used to get the type
* @param context is the DeserializationContext
*/
public void onStartElement(String namespace, String localName,
String prefix, Attributes attributes,
DeserializationContext context)
throws SAXException
{
// Deserializing the xml array requires processing the
// xsi:type= attribute, the soapenc:arrayType attribute,
// and the xsi:type attributes of the individual elements.
//
// The xsi:type=<qName> attribute is used to determine the java
// type of the array to instantiate. Axis expects it
// to be set to the generic "soapenc:Array" or to
// a specific qName. If the generic "soapenc:Array"
// specification is used, Axis determines the array
// type by examining the soapenc:arrayType attribute.
//
// The soapenc:arrayType=<qname><dims> is used to determine
// i) the number of dimensions,
// ii) the length of each dimension,
// iii) the default xsi:type of each of the elements.
//
// If the arrayType attribute is missing, Axis assumes
// a single dimension array with length equal to the number
// of nested elements. In such cases, the default xsi:type of
// the elements is determined using the array xsi:type.
//
// The xsi:type attributes of the individual elements of the
// array are used to determine the java type of the element.
// If the xsi:type attribute is missing for an element, the
// default xsi:type value is used.
if (log.isDebugEnabled()) {
log.debug("Enter: ArrayDeserializer::startElement()");
}
soapConstants = context.getSOAPConstants();
// Get the qname for the array type=, set it to null if
// the generic type is used.
QName typeQName = context.getTypeFromAttributes(namespace,
localName,
attributes);
if (typeQName == null) {
typeQName = getDefaultType();
}
if (typeQName != null &&
Constants.equals(Constants.SOAP_ARRAY, typeQName)) {
typeQName = null;
}
// Now get the arrayType value
QName arrayTypeValue = context.getQNameFromString(
Constants.getValue(attributes,
Constants.URIS_SOAP_ENC,
soapConstants.getAttrItemType()));
// The first part of the arrayType expression is
// the default item type qname.
// The second part is the dimension information
String dimString = null;
QName innerQName = null;
String innerDimString = "";
if (arrayTypeValue != null) {
if (soapConstants != SOAPConstants.SOAP12_CONSTANTS) {
// Doing SOAP 1.1
// Array dimension noted like this : [][x]
String arrayTypeValueNamespaceURI =
arrayTypeValue.getNamespaceURI();
String arrayTypeValueLocalPart =
arrayTypeValue.getLocalPart();
int leftBracketIndex =
arrayTypeValueLocalPart.lastIndexOf('[');
int rightBracketIndex =
arrayTypeValueLocalPart.lastIndexOf(']');
if (leftBracketIndex == -1
|| rightBracketIndex == -1
|| rightBracketIndex < leftBracketIndex) {
throw new IllegalArgumentException(
Messages.getMessage("badArrayType00",
"" + arrayTypeValue));
}
dimString =
arrayTypeValueLocalPart.substring(leftBracketIndex + 1,
rightBracketIndex);
arrayTypeValueLocalPart =
arrayTypeValueLocalPart.substring(0, leftBracketIndex);
// If multi-dim array set to soapenc:Array
if (arrayTypeValueLocalPart.endsWith("]")) {
defaultItemType = Constants.SOAP_ARRAY;
int bracket = arrayTypeValueLocalPart.indexOf("[");
innerQName = new QName(arrayTypeValueNamespaceURI,
arrayTypeValueLocalPart.substring(0, bracket));
innerDimString = arrayTypeValueLocalPart.substring(bracket);
} else {
defaultItemType = new QName(arrayTypeValueNamespaceURI,
arrayTypeValueLocalPart);
}
} else {
String arraySizeValue = attributes.getValue(soapConstants.getEncodingURI(), Constants.ATTR_ARRAY_SIZE);
int leftStarIndex = arraySizeValue.lastIndexOf('*');
// Skip to num if any
if (leftStarIndex != -1) {
// "*" => ""
if (leftStarIndex == 0 && arraySizeValue.length() == 1) {
// "* *" => ""
} else if (leftStarIndex == (arraySizeValue.length() - 1)) {
throw new IllegalArgumentException(
Messages.getMessage("badArraySize00",
"" + arraySizeValue));
// "* N" => "N"
} else {
dimString = arraySizeValue.substring(leftStarIndex + 2);
innerQName = arrayTypeValue;
innerDimString = arraySizeValue.substring(0, leftStarIndex + 1);
}
} else {
dimString = arraySizeValue;
}
if (innerDimString == null || innerDimString.length() == 0) {
defaultItemType = arrayTypeValue;
} else {
defaultItemType = Constants.SOAP_ARRAY12;
}
}
}
// If no type QName and no defaultItemType qname, use xsd:anyType
if (defaultItemType == null && typeQName == null) {
Class destClass = context.getDestinationClass();
if (destClass != null && destClass.isArray()) {
// This will get set OK down below...
} else {
defaultItemType = Constants.XSD_ANYTYPE;
}
}
// Determine the class type for the array.
arrayClass = null;
if (typeQName != null) {
arrayClass = context.getTypeMapping().
getClassForQName(typeQName);
}
if (typeQName == null || arrayClass == null) {
// type= information is not sufficient.
// Get an array of the default item type.
Class arrayItemClass = null;
QName compQName = defaultItemType;
// Nested array, use the innermost qname
String dims = "[]";
if (innerQName != null) {
compQName = innerQName;
if (soapConstants == SOAPConstants.SOAP12_CONSTANTS) {
// With SOAP 1.2 Array, we append [] for each * found
int offset = 0;
while ((offset = innerDimString.indexOf('*', offset)) != -1) {
dims += "[]";
offset ++;
}
} else {
// With SOAP 1.1 Array, we can append directly the complete innerDimString
dims += innerDimString;
}
}
// item Class
arrayItemClass = context.getTypeMapping().getClassForQName(compQName);
if (arrayItemClass != null) {
try {
// Append the dimension found to the classname computed from the itemClass
// to form the array classname
//
String loadableArrayClassName = JavaUtils.getLoadableClassName(
JavaUtils.getTextClassName(arrayItemClass.getName()) + dims);
arrayClass = ClassUtils.forName(loadableArrayClassName,
true,
arrayItemClass.getClassLoader());
} catch (Exception e) {
throw new SAXException(
Messages.getMessage("noComponent00",
"" + defaultItemType));
}
}
}
if (arrayClass == null) {
arrayClass = context.getDestinationClass();
}
if (arrayClass == null) {
throw new SAXException(
Messages.getMessage("noComponent00", "" + defaultItemType));
}
if (dimString == null || dimString.length() == 0) {
// Size determined using length of the members
value = new ArrayListExtension(arrayClass);
} else {
try
{
StringTokenizer tokenizer;
if (soapConstants == SOAPConstants.SOAP12_CONSTANTS) {
tokenizer = new StringTokenizer(dimString);
} else {
tokenizer = new StringTokenizer(dimString, "[],");
}
length = Integer.parseInt(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
// If the array is passed as a multi-dimensional array
// (i.e. int[2][3]) then store all of the
// mult-dim lengths.
// The valueReady method uses this array to set the
// proper mult-dim element.
mDimLength = new ArrayList();
mDimLength.add(new Integer(length));
while(tokenizer.hasMoreTokens()) {
mDimLength.add(
new Integer(
Integer.parseInt(tokenizer.nextToken())));
}
}
// Create an ArrayListExtension class to store the ArrayList
// plus converted objects.
ArrayList list = new ArrayListExtension(arrayClass, length);
// This is expensive as our array may not grown this big.
// Prevents problems when XML claims a huge size
// that it doesn't actually fill.
//for (int i = 0; i < length; i++) {
// list.add(null);
//}
value = list;
}
catch (NumberFormatException e)
{
throw new IllegalArgumentException(
Messages.getMessage("badInteger00", dimString));
}
}
// If soapenc:offset specified, set the current index accordingly
String offset = Constants.getValue(attributes,
Constants.URIS_SOAP_ENC,
Constants.ATTR_OFFSET);
if (offset != null) {
if (soapConstants == SOAPConstants.SOAP12_CONSTANTS) {
throw new SAXException(Messages.getMessage("noSparseArray"));
}
int leftBracketIndex = offset.lastIndexOf('[');
int rightBracketIndex = offset.lastIndexOf(']');
if (leftBracketIndex == -1
|| rightBracketIndex == -1
|| rightBracketIndex < leftBracketIndex)
{
throw new SAXException(
Messages.getMessage("badOffset00", offset));
}
curIndex =
convertToIndex(offset.substring(leftBracketIndex + 1,
rightBracketIndex),
"badOffset00");
}
if (log.isDebugEnabled()) {
log.debug("Exit: ArrayDeserializer::startElement()");
}
}
/**
* onStartChild is called on each child element.
* @param namespace is the namespace of the child element
* @param localName is the local name of the child element
* @param prefix is the prefix used on the name of the child element
* @param attributes are the attributes of the child element
* @param context is the deserialization context.
* @return is a Deserializer to use to deserialize a child (must be
* a derived class of SOAPHandler) or null if no deserialization should
* be performed.
*/
public SOAPHandler onStartChild(String namespace,
String localName,
String prefix,
Attributes attributes,
DeserializationContext context)
throws SAXException
{
if (log.isDebugEnabled()) {
log.debug("Enter: ArrayDeserializer.onStartChild()");
}
// If the position attribute is set,
// use it to update the current index
if (attributes != null) {
String pos =
Constants.getValue(attributes,
Constants.URIS_SOAP_ENC,
Constants.ATTR_POSITION);
if (pos != null) {
if (soapConstants == SOAPConstants.SOAP12_CONSTANTS) {
throw new SAXException(Messages.getMessage("noSparseArray"));
}
int leftBracketIndex = pos.lastIndexOf('[');
int rightBracketIndex = pos.lastIndexOf(']');
if (leftBracketIndex == -1
|| rightBracketIndex == -1
|| rightBracketIndex < leftBracketIndex)
{
throw new SAXException(
Messages.getMessage("badPosition00", pos));
}
curIndex =
convertToIndex(pos.substring(leftBracketIndex + 1,
rightBracketIndex),
"badPosition00");
}
// If the xsi:nil attribute, set the value to null
// and return since there is nothing to deserialize.
if (context.isNil(attributes)) {
setChildValue(null, new Integer(curIndex++));
return null;
}
}
// Use the xsi:type setting on the attribute if it exists.
QName itemType = context.getTypeFromAttributes(namespace,
localName,
attributes);
// Get the deserializer for the type.
Deserializer dSer = null;
if (itemType != null && (context.getCurElement().getHref() == null)) {
dSer = context.getDeserializerForType(itemType);
}
if (dSer == null) {
// No deserializer can be found directly. Need to look harder
QName defaultType = defaultItemType;
Class javaType = null;
if (arrayClass != null &&
arrayClass.isArray() &&
defaultType == null) {
javaType = arrayClass.getComponentType();
defaultType = context.getTypeMapping().getTypeQName(javaType);
}
// We don't have a deserializer, the safest thing to do
// is to set up using the DeserializerImpl below.
// The DeserializerImpl will take care of href/id and
// install the appropriate serializer, etc. The problem
// is that takes a lot of time and will occur
// all the time if no xsi:types are sent. Most of the
// time an item is a simple schema type (i.e. String)
// so the following shortcut is used to get a Deserializer
// for these cases.
if (itemType == null && dSer == null) {
if (defaultType != null && SchemaUtils.isSimpleSchemaType(defaultType)) {
dSer = context.getDeserializer(javaType, defaultType);
}
}
// If no deserializer is
// found, the deserializer is set to DeserializerImpl().
// It is possible that the element has an href, thus we
// won't know the type until the definitition is encountered.
if (dSer == null) {
dSer = new DeserializerImpl();
// Determine a default type for the deserializer
if (itemType == null) {
dSer.setDefaultType(defaultType);
}
}
}
// Register the callback value target, and
// keep track of this index so we know when it has been set.
dSer.registerValueTarget(
new DeserializerTarget(this, new Integer(curIndex)));
// The framework handles knowing when the value is complete, as
// long as we tell it about each child we're waiting on...
addChildDeserializer(dSer);
curIndex++;
// In case of multi-array, we need to specify the destination class
// of the children elements of this element array deserializer.
context.setDestinationClass(arrayClass.getComponentType());
if (log.isDebugEnabled()) {
log.debug("Exit: ArrayDeserializer.onStartChild()");
}
return (SOAPHandler)dSer;
}
public void onEndChild(String namespace, String localName, DeserializationContext context) throws SAXException {
// reverse onStartChild operation.
context.setDestinationClass(arrayClass);
}
public void characters(char[] chars, int i, int i1) throws SAXException {
for (int idx = i; i < i1; i++) {
if (!Character.isWhitespace(chars[idx]))
throw new SAXException(Messages.getMessage("charsInArray"));
}
}
/**
* set is called during deserialization to assign
* the Object value to the array position indicated by hint.
* The hint is always a single Integer. If the array being
* deserialized is a multi-dimensional array, the hint is
* converted into a series of indices to set the correct
* nested position.
* The array deserializer always deserializes into
* an ArrayList, which is converted and copied into the
* actual array after completion (by valueComplete).
* It is important to wait until all indices have been
* processed before invoking valueComplete.
* @param value value of the array element
* @param hint index of the array element (Integer)
**/
public void setChildValue(Object value, Object hint) throws SAXException
{
if (log.isDebugEnabled()) {
log.debug("Enter: ArrayDeserializer::setValue(" + value + ", " + hint + ")");
}
ArrayList list = (ArrayList)this.value;
int offset = ((Integer)hint).intValue();
if (this.mDimLength == null) {
// Normal Case: Set the element in the list
// grow the list if necessary to accomodate the new member
while (list.size() <= offset) {
list.add(null);
}
list.set(offset, value);
} else {
// Multi-Dim Array case: Need to find the nested ArrayList
// and set the proper element.
// Convert the offset into a series of indices
ArrayList mDimIndex = toMultiIndex(offset);
// Get/Create the nested ArrayList
for(int i=0; i < mDimLength.size(); i++) {
int length = ((Integer)mDimLength.get(i)).intValue();
int index = ((Integer)mDimIndex.get(i)).intValue();
while (list.size() < length) {
list.add(null);
}
// If not the last dimension, get the nested ArrayList
// Else set the value
if (i < mDimLength.size()-1) {
if (list.get(index) == null) {
list.set(index, new ArrayList());
}
list = (ArrayList) list.get(index);
} else {
list.set(index, value);
}
}
}
}
/**
* When valueComplete() is invoked on the array,
* first convert the array value into the expected array.
* Then call super.valueComplete() to inform referents
* that the array value is ready.
**/
public void valueComplete() throws SAXException
{
if (componentsReady()) {
try {
if (arrayClass != null) {
value = JavaUtils.convert(value, arrayClass);
}
} catch (RuntimeException e) {
// We must ignore exceptions from convert for Arrays with null - why?
}
}
super.valueComplete();
}
/**
* Converts the given string to an index.
* Assumes the string consists of a brackets surrounding comma
* separated digits. For example "[2]" or [2,3]".
* The routine returns a single index.
* For example "[2]" returns 2.
* For example "[2,3]" depends on the size of the multiple dimensions.
* if the dimensions are "[3,5]" then 13 is returned (2*5) + 3.
* @param text representing index text
* @param exceptKey exception message key
* @return index
*/
private int convertToIndex(String text, String exceptKey)
throws SAXException {
StringTokenizer tokenizer = new StringTokenizer(text, "[],");
int index = 0;
try {
if (mDimLength == null) {
// Normal Case: Single dimension
index = Integer.parseInt(tokenizer.nextToken());
if (tokenizer.hasMoreTokens()) {
throw new SAXException(
Messages.getMessage(exceptKey, text));
}
}
else {
// Multiple Dimensions:
int dim = -1;
ArrayList work = new ArrayList();
while(tokenizer.hasMoreTokens()) {
// Problem if the number of dimensions specified exceeds
// the number of dimensions of arrayType
dim++;
if (dim >= mDimLength.size()) {
throw new SAXException(
Messages.getMessage(exceptKey, text));
}
// Get the next token and convert to integer
int workIndex = Integer.parseInt(tokenizer.nextToken());
// Problem if the index is out of range.
if (workIndex < 0 ||
workIndex >=
((Integer)mDimLength.get(dim)).intValue()) {
throw new SAXException(
Messages.getMessage(exceptKey, text));
}
work.add(new Integer(workIndex));
}
index = toSingleIndex(work); // Convert to single index
}
} catch (SAXException e) {
throw e;
} catch (Exception e) {
throw new SAXException(Messages.getMessage(exceptKey, text));
}
return index;
}
/**
* Converts single index to list of multiple indices.
* @param single index
* @return list of multiple indices or null if not multiple indices.
*/
private ArrayList toMultiIndex(int single) {
if (mDimLength == null)
return null;
// Calculate the index factors if not already known
if (mDimFactor == null) {
mDimFactor = new ArrayList();
for (int i=0; i < mDimLength.size(); i++) {
int factor = 1;
for (int j=i+1; j<mDimLength.size(); j++) {
factor *= ((Integer)mDimLength.get(j)).intValue();
}
mDimFactor.add(new Integer(factor));
}
}
ArrayList rc = new ArrayList();
for (int i=0; i < mDimLength.size(); i++) {
int factor = ((Integer)mDimFactor.get(i)).intValue();
rc.add(new Integer(single / factor));
single = single % factor;
}
return rc;
}
/**
* Converts multiple index to single index.
* @param indexArray list of multiple indices
* @return single index
*/
private int toSingleIndex(ArrayList indexArray) {
if (mDimLength == null || indexArray == null)
return -1;
// Calculate the index factors if not already known
if (mDimFactor == null) {
mDimFactor = new ArrayList();
for (int i=0; i < mDimLength.size(); i++) {
int factor = 1;
for (int j=i+1; j<mDimLength.size(); j++) {
factor *= ((Integer)mDimLength.get(j)).intValue();
}
mDimFactor.add(new Integer(factor));
}
}
int single = 0;
for (int i=0; i < indexArray.size(); i++) {
single += ((Integer)mDimFactor.get(i)).intValue()*
((Integer)indexArray.get(i)).intValue();
}
return single;
}
/**
* During processing, the Array Deserializer stores the array in
* an ArrayListExtension class. This class contains all of the
* normal function of an ArrayList, plus it keeps a list of the
* converted array values. This class is essential to support
* arrays that are multi-referenced.
**/
public class ArrayListExtension extends ArrayList
implements JavaUtils.ConvertCache {
private HashMap table = null;
private Class arrayClass = null; // The array class.
/**
* Constructors
*/
ArrayListExtension(Class arrayClass) {
super();
this.arrayClass = arrayClass;
// Don't use the array class as a hint
// if it can't be instantiated
if (arrayClass == null ||
arrayClass.isInterface() ||
java.lang.reflect.Modifier.isAbstract(
arrayClass.getModifiers())) {
arrayClass = null;
}
}
ArrayListExtension(Class arrayClass, int length) {
// Sanity check the array size, 50K is big enough to start
super(length > 50000 ? 50000 : length);
this.arrayClass = arrayClass;
// Don't use the array class as a hint
// if it can't be instantiated
if (arrayClass == null ||
arrayClass.isInterface() ||
java.lang.reflect.Modifier.isAbstract(
arrayClass.getModifiers())) {
arrayClass = null;
}
}
/**
* Store converted value
**/
public void setConvertedValue(Class cls, Object value) {
if (table == null)
table = new HashMap();
table.put(cls, value);
}
/**
* Get previously converted value
**/
public Object getConvertedValue(Class cls) {
if (table == null)
return null;
return table.get(cls);
}
/**
* Get the destination array class described by the xml
**/
public Class getDestClass() {
return arrayClass;
}
}
}
|