/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.cxf.aegis.type.encoded;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import org.apache.cxf.aegis.Context;
import org.apache.cxf.aegis.DatabindingException;
import org.apache.cxf.aegis.type.AegisType;
import org.apache.cxf.aegis.type.TypeMapping;
import org.apache.cxf.aegis.type.TypeUtil;
import org.apache.cxf.aegis.type.basic.BeanType;
import org.apache.cxf.aegis.xml.MessageReader;
import org.apache.cxf.aegis.xml.MessageWriter;
import org.apache.cxf.binding.soap.Soap11;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.ws.commons.schema.XmlSchema;
import static org.apache.cxf.aegis.type.encoded.SoapEncodingUtil.readAttributeValue;
public class SoapArrayType extends AegisType {
private static final Logger LOG = LogUtils.getL7dLogger(SoapArrayType.class);
private static final String SOAP_ENCODING_NS_1_1 = Soap11.getInstance().getSoapEncodingStyle();
private static final QName SOAP_ARRAY_POSITION = new QName(SOAP_ENCODING_NS_1_1, "position");
private QName componentName;
@Override
public Object readObject(MessageReader reader, Context context) throws DatabindingException {
try {
// get the encoded array type info
TypeMapping tm = context.getTypeMapping();
if (tm == null) {
tm = getTypeMapping();
}
ArrayTypeInfo arrayTypeInfo = new ArrayTypeInfo(reader, tm);
// verify arrayType dimensions are the same as this type class's array dimensions
if (getDimensions() != arrayTypeInfo.getTotalDimensions()) {
throw new DatabindingException("In " + getSchemaType() + " expected array with "
+ getDimensions() + " dimensions, but arrayType has "
+ arrayTypeInfo.getTotalDimensions() + " dimensions: "
+ arrayTypeInfo.toString());
}
// calculate max size
int maxSize = 1;
for (int dimension : arrayTypeInfo.getDimensions()) {
maxSize *= dimension;
}
// verify offset doesn't exceed maximum size
if (arrayTypeInfo.getOffset() >= maxSize) {
throw new DatabindingException("The array offset " + arrayTypeInfo.getOffset() + " in "
+ getSchemaType() + " exceeds the expecte size of " + maxSize);
}
// read the values
List<Object> values = readCollection(reader,
context,
arrayTypeInfo,
maxSize - arrayTypeInfo.getOffset());
// if it is a partially transmitted array offset the array values
if (arrayTypeInfo.getOffset() > 0) {
List<Object> list = new ArrayList<>(values.size() + arrayTypeInfo.getOffset());
list.addAll(Collections.nCopies(arrayTypeInfo.getOffset(), null));
list.addAll(values);
values = list;
}
// check bounds
if (values.size() > maxSize) {
throw new DatabindingException("The number of elements " + values.size() + " in "
+ getSchemaType() + " exceeds the expecte size of " + maxSize);
}
if (values.size() < maxSize) {
values.addAll(Collections.nCopies(maxSize - values.size(), null));
// todo is this an error?
// throw new DatabindingException("The number of elements in " + getSchemaType() +
// " is less then the expected size of " + expectedSize);
}
if (values.size() != maxSize) {
throw new IllegalStateException("Internal error: Expected values collection to contain "
+ maxSize + " elements but it contains " + values.size() + " elements");
}
// create the array
return makeArray(values, arrayTypeInfo.getDimensions(), getTypeClass().getComponentType());
} catch (IllegalArgumentException e) {
throw new DatabindingException("Illegal argument.", e);
}
}
protected List<Object> readCollection(MessageReader reader,
Context context,
ArrayTypeInfo arrayTypeInfo,
int maxSize) throws DatabindingException {
List<Object> values = new ArrayList<>();
Boolean sparse = null;
while (reader.hasMoreElementReaders()) {
MessageReader creader = reader.getNextElementReader();
// if the first element contains a position attribute, this is a sparse array
// and all subsequent elements must contain the position attribute
String position = readAttributeValue(creader, SOAP_ARRAY_POSITION);
if (sparse == null) {
sparse = position != null;
}
// nested element names can specify a type
AegisType compType = getTypeMapping().getType(creader.getName());
if (compType == null) {
// use the type declared in the arrayType attribute
compType = arrayTypeInfo.getType();
}
// check for an xsi:type override
compType = TypeUtil.getReadType(creader.getXMLStreamReader(),
context.getGlobalContext(), compType);
// wrap type with soap ref to handle hrefs
compType = new SoapRefType(compType);
// read the value
Object value;
if (creader.isXsiNil()) {
value = null;
creader.readToEnd();
} else {
value = compType.readObject(creader, context);
}
// add the value
if (!sparse) {
if (values.size() + 1 > maxSize) {
throw new DatabindingException("The number of elements in " + getSchemaType()
+ " exceeds the maximum size of " + maxSize);
}
values.add(value);
} else {
int valuesPosition = readValuesPosition(position, arrayTypeInfo.getDimensions());
if (valuesPosition > maxSize) {
throw new DatabindingException("Array position " + valuesPosition + " in "
+ getSchemaType() + " exceeds the maximum size of " + maxSize);
}
if (values.size() <= valuesPosition) {
values.addAll(Collections.nCopies(valuesPosition - values.size() + 1, null));
}
Object oldValue = values.set(valuesPosition, value);
if (oldValue != null) {
throw new DatabindingException("Array position " + valuesPosition + " in "
+ getSchemaType() + " is already assigned to value " + oldValue);
}
}
}
return values;
}
private int readValuesPosition(String positionString, List<Integer> dimensions) {
if (positionString == null) {
throw new DatabindingException("Sparse array entry does not contain a position attribute");
}
try {
// position = "[" , length , { "," , lenght } , "]" ;
List<String> tokens = CastUtils.cast(Collections.list(new StringTokenizer(positionString,
"[],",
true)));
if (tokens.size() == 2 + dimensions.size() + dimensions.size() - 1 && tokens.get(0).equals("[")
&& tokens.get(tokens.size() - 1).equals("]")) {
// strip off leading [ and trailing ]
tokens = tokens.subList(1, tokens.size() - 1);
// return the product of the values
int[] index = new int[dimensions.size()];
for (int i = 0; i < index.length; i++) {
int tokenId = i * 2;
index[i] = Integer.parseInt(tokens.get(tokenId));
if (tokenId + 1 < tokens.size() && !tokens.get(tokenId + 1).equals(",")) {
throw new IllegalStateException(
"Expected a comma but got " + tokens.get(tokenId + 1));
}
}
// determine the real position withing the flattened square array
int valuePosition = 0;
int multiplier = 1;
for (int i = index.length - 1; i >= 0; i--) {
int position = index[i];
valuePosition += position * multiplier;
multiplier *= dimensions.get(i);
}
return valuePosition;
}
} catch (Exception ignored) {
// exception is thrown below
}
// failed print the expected format
StringBuilder expectedFormat = new StringBuilder();
expectedFormat.append("[x");
for (int i = 1; i < dimensions.size(); i++) {
expectedFormat.append(",x");
}
expectedFormat.append("]");
throw new DatabindingException("Expected sparse array position value in format " + expectedFormat
+ ", but was " + positionString);
}
protected Object makeArray(List<Object> values, List<Integer> dimensions, Class<?> componentType) {
// if this is an array of arrays, recurse into this function
// for each nested array
if (componentType.isArray() && dimensions.size() > 1) {
// square array
int chunkSize = 1;
for (Integer dimension : dimensions.subList(1, dimensions.size())) {
chunkSize *= dimension;
}
Object[] array = (Object[]) Array.newInstance(componentType, dimensions.get(0));
for (int i = 0; i < array.length; i++) {
List<Object> chunk = values.subList(i * chunkSize, (i + 1) * chunkSize);
Object value = makeArray(chunk,
dimensions.subList(1, dimensions.size()),
componentType.getComponentType());
Array.set(array, i, value);
}
return array;
}
// build the array
Object array = Array.newInstance(componentType, dimensions.get(0));
for (int i = 0; i < values.size(); i++) {
Object value = values.get(i);
if (value != null) {
SoapRef soapRef = (SoapRef) value;
soapRef.setAction(new SetArrayAction(array, i));
}
}
return array;
}
@Override
public void writeObject(Object values,
MessageWriter writer,
Context context) throws DatabindingException {
if (values == null) {
return;
}
// ComponentType
AegisType type = getComponentType();
if (type == null) {
throw new DatabindingException("Couldn't find component type for array.");
}
// Root component's schema type
QName rootType = getRootType();
String prefix = writer.getPrefixForNamespace(rootType.getNamespaceURI(), rootType.getPrefix());
if (prefix == null) {
prefix = "";
}
rootType = new QName(rootType.getNamespaceURI(), rootType.getLocalPart(), prefix);
// write the soap arrayType attribute
ArrayTypeInfo arrayTypeInfo = new ArrayTypeInfo(rootType,
getDimensions() - 1,
Array.getLength(values));
// ensure that the writer writes out this prefix...
writer.getPrefixForNamespace(arrayTypeInfo.getTypeName().getNamespaceURI(),
arrayTypeInfo.getTypeName().getPrefix());
arrayTypeInfo.writeAttribute(writer);
// write each element
for (int i = 0; i < Array.getLength(values); i++) {
writeValue(Array.get(values, i), writer, context, type);
}
}
protected void writeValue(Object value,
MessageWriter writer,
Context context,
AegisType type) throws DatabindingException {
type = TypeUtil.getWriteType(context.getGlobalContext(), value, type);
MessageWriter cwriter = writer.getElementWriter(type.getSchemaType().getLocalPart(), "");
if (value == null && type.isNillable()) {
// null
cwriter.writeXsiNil();
} else if (type instanceof BeanType || type instanceof SoapArrayType) {
// write refs to complex type
String refId = MarshalRegistry.get(context).getInstanceId(value);
SoapEncodingUtil.writeRef(cwriter, refId);
} else {
// write simple types inline
type.writeObject(value, cwriter, context);
}
cwriter.close();
}
/**
* Throws UnsupportedOperationException
*/
@Override
public void writeSchema(XmlSchema root) {
throw new UnsupportedOperationException();
}
/**
* We need to write a complex type schema for Beans, so return true.
*
* @see org.apache.cxf.aegis.type.AegisType#isComplex()
*/
@Override
public boolean isComplex() {
return true;
}
/**
* Gets the QName of the component type of this array.
* @return the QName of the component type of this array
*/
public QName getComponentName() {
return componentName;
}
/**
* Sets the QName of the component type of this array.
* @param componentName the QName of the component type of this array
*/
public void setComponentName(QName componentName) {
this.componentName = componentName;
}
@Override
public Set<AegisType> getDependencies() {
Set<AegisType> deps = new HashSet<>();
deps.add(getComponentType());
return deps;
}
/**
* Get the <code>AegisType</code> of the elements in the array. This is only used for writing an array.
* When reading the type is solely determined by the required arrayType soap attribute.
*/
public AegisType getComponentType() {
Class<?> compType = getTypeClass().getComponentType();
AegisType type;
if (componentName == null) {
type = getTypeMapping().getType(compType);
} else {
type = getTypeMapping().getType(componentName);
// We couldn't find the type the user specified. One is created
// below instead.
if (type == null) {
LOG.finest("Couldn't find array component type " + componentName + ". Creating one instead.");
}
}
if (type == null) {
type = getTypeMapping().getTypeCreator().createType(compType);
getTypeMapping().register(type);
}
return type;
}
/**
* Gets the QName of the root component type of this array. This will be a non-array type such as
* a simple xsd type.
* @return the QName of the root component type of this array
*/
protected QName getRootType() {
AegisType componentType = getComponentType();
if (componentType instanceof SoapArrayType) {
SoapArrayType arrayType = (SoapArrayType) componentType;
return arrayType.getRootType();
}
return componentType.getSchemaType();
}
/**
* Gets the number of array dimensions in the class for this type.
* @return the number of array dimensions
*/
private int getDimensions() {
int dimensions = 0;
for (Class<?> type = getTypeClass(); type.isArray(); type = type.getComponentType()) {
dimensions++;
}
return dimensions;
}
/**
* Sets an array entry when the soap ref is resolved
*/
private static class SetArrayAction implements SoapRef.Action {
private final Object array;
private final int index;
SetArrayAction(Object array, int index) {
this.array = array;
this.index = index;
}
public void onSet(SoapRef ref) {
Array.set(array, index, ref.get());
}
}
}