blob: efada7e9e2405f95a08104737b9537938ae94766 [file] [log] [blame]
/**
* *****************************************************************************
* Copyright (C) 2012 Spanish National Bioinformatics Institute (INB),
* Barcelona Supercomputing Center and The University of Manchester
*
* Modifications to the initial code base are copyright of their respective
* authors, or their employers as appropriate.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307
*****************************************************************************
*/
package net.sf.taverna.wsdl.parser;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPConstants;
import org.apache.ws.commons.schema.*;
import org.apache.ws.commons.schema.utils.XmlSchemaObjectBase;
import org.apache.ws.commons.schema.utils.XmlSchemaRef;
import org.w3c.dom.Attr;
import org.xml.sax.InputSource;
/**
* An utility class to construct WSDL type descriptors based on XML Schema.
*
* @author Dmitry Repchevsky
*/
public class TypeDescriptors
{
private final XmlSchemaCollection schemas;
/**
* Constructor that takes a collection of XML Schemas.
*
* @param schemas XML Schemas that this class uses to build type descriptors
*/
public TypeDescriptors(XmlSchemaCollection schemas) {
this.schemas = schemas;
if (schemas.schemaForNamespace(SOAPConstants.URI_NS_SOAP_ENCODING) == null) {
// get soapEncoding datatypes
try {
InputStream soapEncodingStream = TypeDescriptors.class.getClassLoader().getResourceAsStream("META-INF/soap-encoding.xsd");
try {
schemas.read(new InputSource(soapEncodingStream));
}
finally {
soapEncodingStream.close();
}
// URL soapEncodingURL = new URL(SOAPConstants.URI_NS_SOAP_ENCODING);
// InputStream soapEncodingStream = soapEncodingURL.openStream();
// try {
// schemas.read(new InputSource(soapEncodingStream));
// }
// finally {
// soapEncodingStream.close();
// }
}
catch(MalformedURLException ex) {}
catch(IOException ex) {}
}
}
public static List<TypeDescriptor> getDescriptors(LinkedHashMap<String, XmlSchemaObject> map) {
List<TypeDescriptor> result = new ArrayList<TypeDescriptor>();
for (Map.Entry<String, XmlSchemaObject> parameter : map.entrySet()) {
TypeDescriptor typeDescriptor;
String partName = parameter.getKey();
XmlSchemaObject xmlSchemaObject = parameter.getValue();
if (xmlSchemaObject instanceof XmlSchemaElement) {
typeDescriptor = getDescriptor((XmlSchemaElement)xmlSchemaObject);
} else if (xmlSchemaObject instanceof XmlSchemaType) {
typeDescriptor = getDescriptor((XmlSchemaType)xmlSchemaObject);
} else {
throw new IllegalArgumentException("wrong XmlSchemaObject. Mast be either xs:element or xs:type");
}
typeDescriptor.setName(partName);
result.add(typeDescriptor);
}
return result;
}
public static TypeDescriptor getDescriptor(XmlSchemaElement element) {
XmlSchema xmlSchema = element.getParent();
XmlSchemaCollection schemas = xmlSchema.getParent();
if (schemas == null) {
schemas = new XmlSchemaCollection();
// TODO: put somehow schema inside
}
TypeDescriptors descriptors = new TypeDescriptors(schemas);
return descriptors.getElementDescriptor(element);
}
public static TypeDescriptor getDescriptor(XmlSchemaType type) {
XmlSchema xmlSchema = type.getParent();
XmlSchemaCollection schemas = xmlSchema.getParent();
if (schemas == null) {
schemas = new XmlSchemaCollection();
}
TypeDescriptors descriptors = new TypeDescriptors(schemas);
return descriptors.getTypeDescriptor(type.getQName());
}
/**
* Builds a type descriptor for a "Literal" parameter.
*
* @param elementName an XML element name of the parameter.
* @return a Type Descriptor for the parameter.
*/
public TypeDescriptor getElementDescriptor(QName elementName) {
XmlSchemaElement element = schemas.getElementByQName(elementName);
if (element == null) {
return null;
}
return getElementDescriptor(element);
}
/**
* Builds a type descriptor for a "RPC" parameter.
*
* @param typeName an XML type name of the parameter.
* @return a Type Descriptor for the parameter.
*/
public TypeDescriptor getTypeDescriptor(QName typeName) {
XmlSchemaType xmlSchemaType = schemas.getTypeByQName(typeName);
if (xmlSchemaType == null) {
return null;
}
TypeDescriptor typeDesc;
if (SOAPConstants.URI_NS_SOAP_ENCODING.equals(typeName.getNamespaceURI()) &&
"Array".equals(typeName.getLocalPart())) {
typeDesc = new ArrayTypeDescriptor();
} else {
typeDesc = getTypeDescriptor(xmlSchemaType);
}
typeDesc.setType(typeName.getLocalPart());
return typeDesc;
}
private TypeDescriptor getElementDescriptor(XmlSchemaElement element) {
TypeDescriptor elementDescriptor;
TypeDescriptor typeDesc;
QName typeName;
if (element.isRef()) {
XmlSchemaRef<XmlSchemaElement> ref = element.getRef();
XmlSchemaElement target = ref.getTarget();
if (target == null) {
QName targetName = ref.getTargetQName();
if (targetName != null) {
target = schemas.getElementByQName(targetName);
}
}
if (target != null) {
typeName = target.getSchemaTypeName();
if (typeName != null) {
typeDesc = getTypeDescriptor(typeName);
} else {
XmlSchemaType xmlSchemaType = target.getSchemaType();
typeDesc = getTypeDescriptor(xmlSchemaType);
}
} else {
return null; // invalid XML Schema...
}
} else {
typeName = element.getSchemaTypeName();
if (typeName != null) {
typeDesc = getTypeDescriptor(typeName);
} else {
XmlSchemaType xmlSchemaType = element.getSchemaType();
typeDesc = getTypeDescriptor(xmlSchemaType);
}
}
if (element.getMaxOccurs() > 1) {
ArrayTypeDescriptor arrayElementDescriptor = new ArrayTypeDescriptor();
arrayElementDescriptor.setElementType(typeDesc);
elementDescriptor = arrayElementDescriptor;
}
else if (typeDesc instanceof ComplexTypeDescriptor) {
ComplexTypeDescriptor complexElementDescr = (ComplexTypeDescriptor)typeDesc;
ComplexTypeDescriptor complexTypeDesc = new ComplexTypeDescriptor();
complexTypeDesc.getElements().addAll(complexElementDescr.getElements());
complexTypeDesc.getAttributes().addAll(complexElementDescr.getAttributes());
elementDescriptor = complexTypeDesc;
} else {
elementDescriptor = new BaseTypeDescriptor();
}
elementDescriptor.setType(typeName == null ? "" : typeName.getLocalPart());
elementDescriptor.setName(element.isRef() ? element.getWireName().getLocalPart() : element.getName());
elementDescriptor.setQname(element.getWireName());
elementDescriptor.setOptional(element.getMinOccurs() == 0);
elementDescriptor.setUnbounded(Long.MAX_VALUE == element.getMaxOccurs());
elementDescriptor.setNillable(element.isNillable());
return elementDescriptor;
}
private TypeDescriptor getTypeDescriptor(XmlSchemaType xmlSchemaType) {
TypeDescriptor typeDesc;
if (xmlSchemaType instanceof XmlSchemaSimpleType) {
typeDesc = getSimpleTypeDescriptor((XmlSchemaSimpleType)xmlSchemaType);
} else {
typeDesc = getComplexTypeDescriptor((XmlSchemaComplexType)xmlSchemaType);
}
if (!xmlSchemaType.isAnonymous()) {
typeDesc.setName(xmlSchemaType.getName());
}
typeDesc.setQname(xmlSchemaType.getQName());
return typeDesc;
}
private TypeDescriptor getComplexTypeDescriptor(XmlSchemaComplexType xmlSchemaComplexType) {
ComplexTypeDescriptor typeDesc = new ComplexTypeDescriptor();
List<XmlSchemaAttributeOrGroupRef> attributes = xmlSchemaComplexType.getAttributes();
for (XmlSchemaAttributeOrGroupRef attribute : attributes) {
// all XmlSchemaAttributeOrGroupRef descendants are XmlSchemaAttributeGroupMember
addAttributeTypeDescriptors(typeDesc.getAttributes(), (XmlSchemaAttributeGroupMember)attribute);
}
XmlSchemaContentModel xmlSchemaContentModel = xmlSchemaComplexType.getContentModel();
if (xmlSchemaContentModel != null) {
XmlSchemaContent content = xmlSchemaContentModel.getContent();
if (content instanceof XmlSchemaComplexContentExtension) {
XmlSchemaComplexContentExtension complexContentExtension = (XmlSchemaComplexContentExtension)content;
attributes = complexContentExtension.getAttributes();
XmlSchemaParticle particle = complexContentExtension.getParticle();
addElementTypeDescriptors(typeDesc.getElements(), particle);
QName baseTypeName = complexContentExtension.getBaseTypeName();
if (baseTypeName != null) {
TypeDescriptor baseTypeDesc = getTypeDescriptor(baseTypeName);
if (baseTypeDesc instanceof ComplexTypeDescriptor) {
ComplexTypeDescriptor base = (ComplexTypeDescriptor)baseTypeDesc;
typeDesc.getElements().addAll(base.getElements());
}
}
} else if (content instanceof XmlSchemaComplexContentRestriction) {
XmlSchemaComplexContentRestriction complexContentRestriction = (XmlSchemaComplexContentRestriction)content;
attributes = complexContentRestriction.getAttributes();
// check for "SOAP Encoding"
QName baseTypeName = complexContentRestriction.getBaseTypeName();
if (baseTypeName != null) {
TypeDescriptor baseTypeDesc = getTypeDescriptor(baseTypeName);
if (baseTypeDesc instanceof ArrayTypeDescriptor) {
ArrayTypeDescriptor arrayDesc = (ArrayTypeDescriptor)baseTypeDesc;
TypeDescriptor arrayTypeDesc = getArrayType(attributes);
arrayDesc.setElementType(arrayTypeDesc);
return arrayDesc;
}
}
XmlSchemaParticle particle = complexContentRestriction.getParticle();
addElementTypeDescriptors(typeDesc.getElements(), particle);
} else if (content instanceof XmlSchemaSimpleContentExtension) {
XmlSchemaSimpleContentExtension xmlSchemaSimpleContentExtension = (XmlSchemaSimpleContentExtension)content;
attributes = xmlSchemaSimpleContentExtension.getAttributes();
QName baseTypeName = xmlSchemaSimpleContentExtension.getBaseTypeName();
if (baseTypeName != null) {
TypeDescriptor baseTypeDesc = getTypeDescriptor(baseTypeName);
typeDesc.setType(baseTypeDesc.getType());
}
} else if (content instanceof XmlSchemaSimpleContentRestriction){
XmlSchemaSimpleContentRestriction xmlSchemaSimpleContentRestriction = (XmlSchemaSimpleContentRestriction)content;
attributes = xmlSchemaSimpleContentRestriction.getAttributes();
QName baseTypeName = xmlSchemaSimpleContentRestriction.getBaseTypeName();
if (baseTypeName == null) {
XmlSchemaSimpleType simpleType = xmlSchemaSimpleContentRestriction.getBaseType();
baseTypeName = simpleType.getQName();
}
TypeDescriptor baseTypeDesc = getTypeDescriptor(baseTypeName);
typeDesc.setType(baseTypeDesc.getType());
}
for (XmlSchemaAttributeOrGroupRef attribute : attributes) {
addAttributeTypeDescriptors(typeDesc.getAttributes(), (XmlSchemaAttributeGroupMember)attribute);
}
} else {
XmlSchemaParticle particle = xmlSchemaComplexType.getParticle();
addElementTypeDescriptors(typeDesc.getElements(), particle);
}
return typeDesc;
}
/*
* Adds all elements found in the "particle" to the "elements" list
*/
private void addElementTypeDescriptors(List<TypeDescriptor> elements, XmlSchemaParticle particle)
{
if (particle instanceof XmlSchemaElement){
XmlSchemaElement element = (XmlSchemaElement)particle;
TypeDescriptor elementTypeDescriptor = getElementDescriptor(element);
elements.add(elementTypeDescriptor);
}
else if (particle instanceof XmlSchemaAll) {
addElementTypeDescriptors(elements, (XmlSchemaAll)particle);
}
else if (particle instanceof XmlSchemaSequence) {
addElementTypeDescriptors(elements, (XmlSchemaSequence)particle);
}
else if (particle instanceof XmlSchemaChoice) {
addElementTypeDescriptors(elements, (XmlSchemaChoice)particle);
}
}
/*
* Adds all elements found in a sequence to the list of elements
*/
private void addElementTypeDescriptors(List<TypeDescriptor> elements, XmlSchemaSequence sequence) {
List<XmlSchemaSequenceMember> items = sequence.getItems();
addElementTypeDescriptors(elements, items);
}
/*
* Adds all elements found in a "all" particle to the list of elements
*/
private void addElementTypeDescriptors(List<TypeDescriptor> elements, XmlSchemaAll all) {
for (XmlSchemaElement element : all.getItems()) {
TypeDescriptor elementTypeDescriptor = getElementDescriptor(element);
elements.add(elementTypeDescriptor);
}
}
/*
* Adds all elements found in a "choice" particle to the list of elements
*/
private void addElementTypeDescriptors(List<TypeDescriptor> elements, XmlSchemaChoice choice) {
List<XmlSchemaObject> items = choice.getItems();
addElementTypeDescriptors(elements, items);
}
private void addElementTypeDescriptors(List<TypeDescriptor> elements, List<? extends XmlSchemaObjectBase> items) {
for (XmlSchemaObjectBase item : items) {
if (item instanceof XmlSchemaElement) {
addElementTypeDescriptors(elements, (XmlSchemaElement)item);
} else if(item instanceof XmlSchemaSequence) {
addElementTypeDescriptors(elements, (XmlSchemaSequence)item);
} else if (item instanceof XmlSchemaChoice) {
addElementTypeDescriptors(elements, (XmlSchemaChoice)item);
} else if (item instanceof XmlSchemaGroup) {
XmlSchemaGroup group = (XmlSchemaGroup)item;
XmlSchemaGroupParticle groupParticle = group.getParticle();
addElementTypeDescriptors(elements, groupParticle);
} else if (item instanceof XmlSchemaGroupRef) {
XmlSchemaGroupRef groupRef = (XmlSchemaGroupRef)item;
XmlSchemaGroupParticle groupParticle = groupRef.getParticle();
addElementTypeDescriptors(elements, groupParticle);
}
}
}
/*
* Builds a BaseTypeDescriptor from a simpleType definition.
* Method processes simpleType restrictions to find an appropriate XML primitive type
*/
private BaseTypeDescriptor getSimpleTypeDescriptor(XmlSchemaSimpleType xmlSchemaSimpleType) {
BaseTypeDescriptor typeDesc = new BaseTypeDescriptor();
QName typeName = xmlSchemaSimpleType.getQName();
if (typeName != null && XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(typeName.getNamespaceURI())) {
typeDesc.setType(typeName.getLocalPart());
} else {
XmlSchemaSimpleTypeContent simpleTypeContent = xmlSchemaSimpleType.getContent();
if (simpleTypeContent instanceof XmlSchemaSimpleTypeRestriction) {
XmlSchemaSimpleTypeRestriction simpleTypeRestriction = (XmlSchemaSimpleTypeRestriction)simpleTypeContent;
QName baseTypeName = simpleTypeRestriction.getBaseTypeName();
if (baseTypeName == null) {
XmlSchemaSimpleType baseType = simpleTypeRestriction.getBaseType();
baseTypeName = baseType.getQName();
}
TypeDescriptor baseTypeDesc = getTypeDescriptor(baseTypeName);
typeDesc.setType(baseTypeDesc.getType());
}
else if (simpleTypeContent instanceof XmlSchemaSimpleTypeList) {
XmlSchemaSimpleTypeList simpleTypeList = (XmlSchemaSimpleTypeList)simpleTypeContent;
QName itemTypeName = simpleTypeList.getItemTypeName();
if (itemTypeName == null) {
XmlSchemaSimpleType itemType = simpleTypeList.getItemType();
itemTypeName = itemType.getQName();
}
if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(itemTypeName.getNamespaceURI())) {
typeDesc.setType(itemTypeName.getLocalPart());
}
else {
TypeDescriptor baseTypeDesc = getTypeDescriptor(itemTypeName);
typeDesc.setType(baseTypeDesc.getType());
}
}
else if (simpleTypeContent instanceof XmlSchemaSimpleTypeUnion) {
XmlSchemaSimpleTypeUnion typeUnion = (XmlSchemaSimpleTypeUnion)simpleTypeContent;
// TODO
}
}
return typeDesc;
}
/*
* Inserts attribute descriptions specified by the attribute (may be a group of attributes) into the attributes list
*/
private void addAttributeTypeDescriptors(List<TypeDescriptor> attributes, XmlSchemaAttributeGroupMember attribute) {
AttributeTypeDescriptor typeDesc = new AttributeTypeDescriptor();
if (attribute instanceof XmlSchemaAttribute) {
XmlSchemaAttribute xmlSchemaAttribute = (XmlSchemaAttribute)attribute;
QName attrName;
if (xmlSchemaAttribute.isRef()) {
XmlSchemaRef<XmlSchemaAttribute> xmlSchemaRef = xmlSchemaAttribute.getRef();
attrName = xmlSchemaRef.getTargetQName();
if (attrName == null) {
XmlSchemaAttribute xmlSchemaRefAttribute = xmlSchemaRef.getTarget();
attrName = xmlSchemaRefAttribute.getWireName();
}
} else {
attrName = xmlSchemaAttribute.getWireName();
}
typeDesc.setName(attrName.getLocalPart());
typeDesc.setQname(attrName);
TypeDescriptor attrTypeDesc;
QName attrTypeName = xmlSchemaAttribute.getSchemaTypeName();
if (attrTypeName != null) {
attrTypeDesc = getTypeDescriptor(attrTypeName);
} else {
XmlSchemaSimpleType xmlSchemaSimpleType = xmlSchemaAttribute.getSchemaType();
if (xmlSchemaSimpleType == null) {
attrTypeDesc = new BaseTypeDescriptor();
attrTypeDesc.setType("anySimpleType");
} else {
attrTypeDesc = getTypeDescriptor(xmlSchemaSimpleType);
}
}
typeDesc.setType(attrTypeDesc.getType());
typeDesc.setOptional(XmlSchemaUse.OPTIONAL == xmlSchemaAttribute.getUse());
attributes.add(typeDesc);
}
else {
XmlSchemaAttributeGroup xmlSchemaAttributeGroup;
if (attribute instanceof XmlSchemaAttributeGroup) {
xmlSchemaAttributeGroup = (XmlSchemaAttributeGroup)attribute;
}
else {
XmlSchemaAttributeGroupRef xmlSchemaAttributeGroupRef = (XmlSchemaAttributeGroupRef)attribute;
XmlSchemaRef<XmlSchemaAttributeGroup> xmlSchemaRef = xmlSchemaAttributeGroupRef.getRef();
xmlSchemaAttributeGroup = xmlSchemaRef.getTarget();
}
List<XmlSchemaAttributeGroupMember> xmlSchemaAttributeGroupMembers = xmlSchemaAttributeGroup.getAttributes();
for (XmlSchemaAttributeGroupMember xmlSchemaAttributeGroupMember : xmlSchemaAttributeGroupMembers) {
addAttributeTypeDescriptors(attributes, xmlSchemaAttributeGroupMember);
}
}
}
/*
* Looking for "SOAP-ENC:arrayType" attribute in attributes list
*/
private TypeDescriptor getArrayType(List<XmlSchemaAttributeOrGroupRef> attributes) {
for (XmlSchemaAttributeOrGroupRef attribute : attributes) {
TypeDescriptor arrayTypeDesc = getArrayType((XmlSchemaAttributeGroupMember)attribute);
if (arrayTypeDesc != null) {
return arrayTypeDesc;
}
}
return null;
}
/*
* Getting a TypeDescriptor for the attribute ("SOAP-ENC:arrayType")
*/
private TypeDescriptor getArrayType(XmlSchemaAttributeGroupMember attribute) {
if (attribute instanceof XmlSchemaAttribute) {
XmlSchemaAttribute xmlSchemaAttribute = (XmlSchemaAttribute)attribute;
QName attrName;
if (xmlSchemaAttribute.isRef()) {
XmlSchemaRef<XmlSchemaAttribute> xmlSchemaRef = xmlSchemaAttribute.getRef();
attrName = xmlSchemaRef.getTargetQName();
if (attrName == null) {
XmlSchemaAttribute xmlSchemaRefAttribute = xmlSchemaRef.getTarget();
attrName = xmlSchemaRefAttribute.getWireName();
}
} else {
attrName = xmlSchemaAttribute.getWireName();
}
if (SOAPConstants.URI_NS_SOAP_ENCODING.equals(attrName.getNamespaceURI()) &&
"arrayType".equals(attrName.getLocalPart())) {
Attr[] unhandled = xmlSchemaAttribute.getUnhandledAttributes();
String arrayType = null;
for (Attr attr : unhandled) {
if ("arrayType".equals(attr.getLocalName())) {
if (SOAPConstants.URI_NS_SOAP_ENCODING.equals(attr.getNamespaceURI())) {
arrayType = attr.getValue();
break;
}
if ("http://schemas.xmlsoap.org/wsdl/".equals(attr.getNamespaceURI())) {
arrayType = attr.getValue();
}
}
}
if (arrayType != null) {
QName arrayTypeQName = getArrayTypeQName(arrayType);
if (arrayTypeQName.getNamespaceURI() == null || arrayTypeQName.getNamespaceURI().isEmpty())
{
String namespace = xmlSchemaAttribute.getParent().getNamespaceContext().getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX);
arrayTypeQName = new QName(namespace, arrayTypeQName.getLocalPart());
}
return getTypeDescriptor(arrayTypeQName);
}
}
}
else {
XmlSchemaAttributeGroup xmlSchemaAttributeGroup;
if (attribute instanceof XmlSchemaAttributeGroup) {
xmlSchemaAttributeGroup = (XmlSchemaAttributeGroup)attribute;
}
else {
XmlSchemaAttributeGroupRef xmlSchemaAttributeGroupRef = (XmlSchemaAttributeGroupRef)attribute;
XmlSchemaRef<XmlSchemaAttributeGroup> xmlSchemaRef = xmlSchemaAttributeGroupRef.getRef();
xmlSchemaAttributeGroup = xmlSchemaRef.getTarget();
}
List<XmlSchemaAttributeGroupMember> xmlSchemaAttributeGroupMembers = xmlSchemaAttributeGroup.getAttributes();
for (XmlSchemaAttributeGroupMember xmlSchemaAttributeGroupMember : xmlSchemaAttributeGroupMembers) {
TypeDescriptor arrayTypeDesc = getArrayType(xmlSchemaAttributeGroupMember);
if (arrayTypeDesc != null) {
return arrayTypeDesc;
}
}
}
return null;
}
/*
* Parsing an arrayTypeValue to get its name.
*
* arrayTypeValue = atype asize
* atype = QName *( rank )
* rank = "[" *( "," ) "]"
* asize = "[" #length "]"
* length = 1*DIGIT
*/
private QName getArrayTypeQName(String arrayType) {
final int prefixIdx = arrayType.indexOf(':');
final int rankIdx = arrayType.indexOf('[');
if (prefixIdx < 0) {
String localName = arrayType.substring(0, rankIdx);
return new QName(localName);
}
String localName = arrayType.substring(prefixIdx + 1, rankIdx);
String prefix = arrayType.substring(0, prefixIdx);
String namespace = schemas.getNamespaceContext().getNamespaceURI(prefix);
return new QName(namespace, localName, prefix);
}
}