blob: d73dc24bcb4b1bdadd3f8670732ddca3290648c0 [file] [log] [blame]
/*
* Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Blaise Doughan - 2.5 - initial implementation
package org.eclipse.persistence.internal.oxm;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import javax.xml.namespace.QName;
import org.eclipse.persistence.core.mappings.CoreMapping;
import org.eclipse.persistence.core.sessions.CoreProject;
import org.eclipse.persistence.core.sessions.CoreSession;
import org.eclipse.persistence.core.sessions.CoreSessionEventListener;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.core.databaseaccess.CorePlatform;
import org.eclipse.persistence.internal.core.descriptors.CoreObjectBuilder;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.mappings.Descriptor;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.oxm.schema.XMLSchemaReference;
public abstract class Context<
ABSTRACT_SESSION extends CoreAbstractSession,
DESCRIPTOR extends Descriptor<?, ?, ?, ?, ?, NAMESPACE_RESOLVER, ?, ?, ?, ?>,
FIELD extends Field,
NAMESPACE_RESOLVER extends NamespaceResolver,
PROJECT extends CoreProject,
SESSION extends CoreSession,
SESSION_EVENT_LISTENER extends CoreSessionEventListener> {
public static class ContextState<
ABSTRACT_SESSION extends CoreAbstractSession,
DESCRIPTOR extends Descriptor,
PROJECT extends CoreProject,
SESSION extends CoreSession,
SESSION_EVENT_LISTENER extends CoreSessionEventListener> {
protected Context context;
protected Map<XPathQName, DESCRIPTOR> descriptorsByQName;
protected Map<XPathFragment, DESCRIPTOR> descriptorsByGlobalType;
protected SESSION session;
private Collection<SESSION_EVENT_LISTENER> sessionEventListeners;
protected ContextState() {
descriptorsByQName = new HashMap<>();
descriptorsByGlobalType = new HashMap<>();
}
protected ContextState(Context context, PROJECT project, ClassLoader classLoader, Collection<SESSION_EVENT_LISTENER> sessionEventListeners) {
this();
this.context = context;
preLogin(project, classLoader);
session = (SESSION) project.createDatabaseSession();
// if an event listener was passed in as a parameter, register it with the event manager
if (sessionEventListeners != null) {
for(SESSION_EVENT_LISTENER sessionEventListener : sessionEventListeners) {
session.getEventManager().addListener(sessionEventListener);
}
}
setupSession(session);
storeDescriptorsByQName(session);
}
public void addDescriptorByQName(QName qName, DESCRIPTOR descriptor) {
XPathQName xpathQName = new XPathQName(qName, true);
addDescriptorByQName(xpathQName, descriptor);
}
private void addDescriptorByQName(XPathQName qName, DESCRIPTOR descriptor) {
descriptorsByQName.put(qName, descriptor);
}
protected void preLogin(PROJECT project, ClassLoader classLoader) {
}
/**
* INTERNAL: Return the Descriptor with the default root mapping matching
* the QName parameter.
*/
private DESCRIPTOR getDescriptor(QName qName) {
XPathQName xpathQName = new XPathQName(qName, true);
return descriptorsByQName.get(xpathQName);
}
/**
* INTERNAL: Return the Descriptor with the default root mapping matching
* the QName parameter.
*/
private DESCRIPTOR getDescriptor(XPathQName qName) {
return descriptorsByQName.get(qName);
}
/**
* INTERNAL: Return the Descriptor mapped to the global type matching the
* XPathFragment parameter.
*/
private DESCRIPTOR getDescriptorByGlobalType(XPathFragment xPathFragment) {
return this.descriptorsByGlobalType.get(xPathFragment);
}
protected SESSION getSession() {
return session;
}
/**
* INTERNAL: Return the session corresponding to this class. Since the class
* may be mapped by more that one of the projects used to create the
* Context, this method will return the first match.
*/
protected ABSTRACT_SESSION getSession(Class clazz) {
if (null == clazz) {
return null;
}
if (session.getDescriptor(clazz) != null) {
return (ABSTRACT_SESSION) session;
}
throw XMLMarshalException.descriptorNotFoundInProject(clazz.getName());
}
/**
* INTERNAL: Return the session corresponding to this Descriptor. Since
* the class may be mapped by more that one of the projects used to create
* the Context, this method will return the first match.
*/
protected ABSTRACT_SESSION getSession(DESCRIPTOR descriptor) {
if (null == descriptor) {
return null;
}
if (session.getProject().getOrderedDescriptors().contains(descriptor)) {
return (ABSTRACT_SESSION) session;
}
throw XMLMarshalException.descriptorNotFoundInProject(descriptor.getJavaClass().getName());
}
/**
* INTERNAL: Return the session corresponding to this object. Since the
* object may be mapped by more that one of the projects used to create the
* Context, this method will return the first match.
*/
protected ABSTRACT_SESSION getSession(Object object) {
if (null == object) {
return null;
}
if (session.getDescriptor(object) != null) {
return (ABSTRACT_SESSION) session;
}
throw XMLMarshalException.descriptorNotFoundInProject(object.getClass().getName());
}
protected void setupSession(SESSION session) {
}
/**
* INTERNAL:
*/
public void storeDescriptorByQName(DESCRIPTOR descriptor, CorePlatform platform, Set<DESCRIPTOR> processedDescriptors) {
XPathQName descriptorQName;
String defaultRootName;
if (processedDescriptors == null) {
processedDescriptors = new HashSet<>();
}
if (processedDescriptors.contains(descriptor)) {
return;
} else {
processedDescriptors.add(descriptor);
if (descriptor.hasInheritance() && !descriptor.getInheritancePolicy().isRootParentDescriptor()) {
//this means we have a descriptor that is a child in an inheritance hierarchy
storeDescriptorByQName((DESCRIPTOR) descriptor.getInheritancePolicy().getParentDescriptor(), platform, processedDescriptors);
}
}
List tableNames = descriptor.getTableNames();
for (int i = 0; i < tableNames.size(); i++) {
defaultRootName = (String) tableNames.get(i);
if (null != defaultRootName) {
int index = defaultRootName.indexOf(':');
String defaultRootLocalName = defaultRootName.substring(index + 1);
if (!defaultRootLocalName.isEmpty()) {
if (index > -1) {
String defaultRootPrefix = defaultRootName.substring(0, index);
String defaultRootNamespaceURI = descriptor.getNamespaceResolver().resolveNamespacePrefix(defaultRootPrefix);
descriptorQName = new XPathQName(defaultRootNamespaceURI, defaultRootLocalName, true);
} else {
if(descriptor.getNamespaceResolver() != null) {
descriptorQName = new XPathQName(descriptor.getNamespaceResolver().getDefaultNamespaceURI(), defaultRootLocalName, true);
} else {
descriptorQName = new XPathQName(defaultRootLocalName, true);
}
}
if (!descriptor.hasInheritance() || descriptor.getInheritancePolicy().isRootParentDescriptor()) {
addDescriptorByQName(descriptorQName, descriptor);
} else {
Descriptor existingDescriptor = getDescriptor(descriptorQName);
if (existingDescriptor == null) {
addDescriptorByQName(descriptorQName, descriptor);
}
}
}
}
}
XMLSchemaReference xmlSchemaReference = descriptor.getSchemaReference();
if (null != xmlSchemaReference) {
String schemaContext = xmlSchemaReference.getSchemaContext();
if ((xmlSchemaReference.getType() == XMLSchemaReference.COMPLEX_TYPE) || (xmlSchemaReference.getType() == XMLSchemaReference.SIMPLE_TYPE)) {
if ((null != schemaContext) && (schemaContext.lastIndexOf('/') == 0)) {
schemaContext = schemaContext.substring(1, schemaContext.length());
XPathFragment typeFragment = new XPathFragment(schemaContext);
if (null != descriptor.getNamespaceResolver()) {
String uri = descriptor.getNamespaceResolver().resolveNamespacePrefix(typeFragment.getPrefix());
if(uri == null && xmlSchemaReference.getSchemaContextAsQName() != null){
uri = xmlSchemaReference.getSchemaContextAsQName().getNamespaceURI();
}
typeFragment.setNamespaceURI(uri);
}
this.descriptorsByGlobalType.put(typeFragment, descriptor);
} else {
QName qname = xmlSchemaReference.getSchemaContextAsQName();
if (qname != null) {
if (descriptor.isWrapper() && descriptor.getJavaClassName().contains("ObjectWrapper")) {
return;
}
XPathFragment typeFragment = new XPathFragment();
typeFragment.setLocalName(qname.getLocalPart());
typeFragment.setNamespaceURI(qname.getNamespaceURI());
this.descriptorsByGlobalType.put(typeFragment, descriptor);
}
}
}
}
}
public void storeDescriptorsByQName(CoreSession session) {
Iterator iterator = session.getProject().getOrderedDescriptors().iterator();
Set<DESCRIPTOR> processedDescriptors = new HashSet<>();
while (iterator.hasNext()) {
DESCRIPTOR descriptor = (DESCRIPTOR) iterator.next();
storeDescriptorByQName(descriptor, session.getDatasourcePlatform(), processedDescriptors);
}
}
}
private static class XPathQueryResult {
/*
* Mapping corresponding to the XPath query
*/
private CoreMapping mapping;
/*
* Mapping's owning object
*/
private Object owner;
/*
* Index into mapping, from XPath query (may be null)
*/
private Integer index;
}
protected volatile ContextState<ABSTRACT_SESSION, DESCRIPTOR, PROJECT, SESSION, SESSION_EVENT_LISTENER> contextState;
private <T> T createByXPath(Object object, CoreObjectBuilder objectBuilder, StringTokenizer stringTokenizer, NAMESPACE_RESOLVER namespaceResolver, Class<T> returnType) {
XPathQueryResult queryResult = getMappingForXPath(object, objectBuilder, stringTokenizer, namespaceResolver);
if (null != queryResult.mapping) {
DESCRIPTOR refDescriptor = (DESCRIPTOR) queryResult.mapping.getReferenceDescriptor();
if (null != refDescriptor) {
return (T) refDescriptor.getInstantiationPolicy().buildNewInstance();
}
}
return null;
}
/**
* Create a new object instance for a given XPath, relative to the parentObject.
*
* @param <T>
* The return type of this method corresponds to the returnType parameter.
* @param parentObject
* The XPath will be executed relative to this object.
* @param xPath
* The XPath statement.
* @param namespaceResolver
* A NamespaceResolver containing the prefix/URI pairings from the XPath statement.
* @param returnType
* The return type.
*
* @return
* An instance of the Java class mapped to the supplied return type, or null
* if no result was found.
*/
public <T> T createByXPath(Object parentObject, String xPath, NAMESPACE_RESOLVER namespaceResolver, Class<T> returnType) {
ABSTRACT_SESSION session = this.getSession(parentObject);
DESCRIPTOR descriptor = (DESCRIPTOR) session.getDescriptor(parentObject);
StringTokenizer stringTokenizer = new StringTokenizer(xPath, "/");
return createByXPath(parentObject, descriptor.getObjectBuilder(), stringTokenizer, namespaceResolver, returnType);
}
protected abstract FIELD createField(String path);
public abstract Marshaller createMarshaller();
public abstract Unmarshaller createUnmarshaller();
/**
* INTERNAL:
* Return the Descriptor with the default root mapping matching the QName
* parameter.
*/
public DESCRIPTOR getDescriptor(QName qName) {
XPathQName xpathQName = new XPathQName(qName, true);
return contextState.getDescriptor(xpathQName);
}
/**
* INTERNAL:
* Return the Descriptor with the default root mapping matching the
* XPathQName parameter.
*/
public DESCRIPTOR getDescriptor(XPathQName xpathQName) {
return contextState.getDescriptor(xpathQName);
}
/**
* INTERNAL:
* Return the Descriptor mapped to the global type matching the
* XPathFragment parameter.
*/
public DESCRIPTOR getDescriptorByGlobalType(XPathFragment xPathFragment) {
return contextState.getDescriptorByGlobalType(xPathFragment);
}
private XPathQueryResult getMappingForXPath(Object object, CoreObjectBuilder objectBuilder, StringTokenizer stringTokenizer, NAMESPACE_RESOLVER namespaceResolver) {
XPathQueryResult queryResult = new XPathQueryResult();
String xPath = "";
FIELD field = createField(null);
field.setNamespaceResolver(namespaceResolver);
while (stringTokenizer.hasMoreElements()) {
String nextToken = stringTokenizer.nextToken();
field.setXPath(xPath + nextToken);
field.initialize();
CoreMapping mapping = objectBuilder.getMappingForField(field);
if (null == mapping) {
// XPath might have indexes, while the mapping's XPath may not,
// so remove them and look again
XPathFragment xPathFragment = new XPathFragment(nextToken);
int fieldIndex = field.getXPathFragment().getIndexValue();
int fragmentIndex = xPathFragment.getIndexValue();
if (fieldIndex > 0 || fragmentIndex > 0) {
int index = fieldIndex - 1;
if (index < 0) {
index = fragmentIndex - 1;
}
String xPathNoIndexes = removeIndexesFromXPath(field.getXPath());
field.setXPath(xPathNoIndexes);
field.initialize();
mapping = objectBuilder.getMappingForField(field);
if (null == mapping) {
// Try adding /text()
field.setXPath(xPathNoIndexes + Constants.XPATH_SEPARATOR + Constants.TEXT);
field.initialize();
mapping = objectBuilder.getMappingForField(field);
}
if (null != mapping) {
if (field.getXPath().endsWith(Constants.TEXT) || !stringTokenizer.hasMoreElements()) {
// End of the line, we found a mapping so return it
queryResult.mapping = mapping;
queryResult.owner = object;
queryResult.index = index;
return queryResult;
} else {
// We need to keep looking -- get the mapping value,
// then recurse into getMappingForXPath with new root object
Object childObject = mapping.getAttributeValueFromObject(object);
if (mapping.isCollectionMapping()) {
Object collection = mapping.getAttributeValueFromObject(object);
if (null != collection && List.class.isAssignableFrom(collection.getClass())) {
List list = (List) collection;
if (index >= list.size()) {
// Index used in query is out of range, no matches
return null;
}
childObject = list.get(index);
}
}
if (null == childObject) {
childObject = mapping.getReferenceDescriptor().getObjectBuilder().buildNewInstance();
}
CoreObjectBuilder childObjectBuilder = mapping.getReferenceDescriptor().getObjectBuilder();
return getMappingForXPath(childObject, childObjectBuilder, stringTokenizer, namespaceResolver);
}
}
}
} else {
if (!stringTokenizer.hasMoreElements()) {
// End of the line, we found a mapping so return it
queryResult.mapping = mapping;
queryResult.owner = object;
return queryResult;
} else {
// We need to keep looking -- get the mapping value,
// then recurse into getMappingForXPath with new root object
Object childObject = mapping.getAttributeValueFromObject(object);
if (mapping.isCollectionMapping()) {
Object collection = mapping.getAttributeValueFromObject(object);
if (null != collection && List.class.isAssignableFrom(collection.getClass())) {
List list = (List) collection;
if (0 >= list.size()) {
return null;
}
childObject = list.get(0);
}
}
if (null == childObject) {
childObject = mapping.getReferenceDescriptor().getObjectBuilder().buildNewInstance();
}
CoreObjectBuilder childObjectBuilder = mapping.getReferenceDescriptor().getObjectBuilder();
return getMappingForXPath(childObject, childObjectBuilder, stringTokenizer, namespaceResolver);
}
}
xPath = xPath + nextToken + Constants.XPATH_SEPARATOR;
}
return null;
}
/**
* INTERNAL:
* Return the session corresponding to this class. Since the class
* may be mapped by more that one of the projects used to create the
* Context, this method will return the first match.
*/
public ABSTRACT_SESSION getSession(Class clazz) {
return contextState.getSession(clazz);
}
/**
* INTERNAL:
*/
public SESSION getSession() {
return contextState.getSession();
}
public ABSTRACT_SESSION getSession(DESCRIPTOR descriptor) {
return contextState.getSession(descriptor);
}
/**
* INTERNAL:
* Return the session corresponding to this object. Since the
* object may be mapped by more that one of the projects used to create the
* Context, this method will return the first match.
*/
public ABSTRACT_SESSION getSession(Object object) {
return contextState.getSession(object);
}
/**
* <p>Query the object model based on the corresponding document. The following pairings are equivalent:</p>
*
* <i>Return the Customer's ID</i>
* <pre> Integer id = context.getValueByXPath(customer, "@id", null, Integer.class);
* Integer id = customer.getId();</pre>
*
* <i>Return the Customer's Name</i>
* <pre> String name = context.getValueByXPath(customer, "ns:personal-info/ns:name/text()", null, String.class);
* String name = customer.getName();</pre>
*
* <i>Return the Customer's Address</i>
* <pre> Address address = context.getValueByXPath(customer, "ns:contact-info/ns:address", aNamespaceResolver, Address.class);
* Address address = customer.getAddress();</pre>
*
* <i>Return all the Customer's PhoneNumbers</i>
* <pre> List phoneNumbers = context.getValueByXPath(customer, "ns:contact-info/ns:phone-number", aNamespaceResolver, List.class);
* List phoneNumbers = customer.getPhoneNumbers();</pre>
*
* <i>Return the Customer's second PhoneNumber</i>
* <pre> PhoneNumber phoneNumber = context.getValueByXPath(customer, "ns:contact-info/ns:phone-number[2]", aNamespaceResolver, PhoneNumber.class);
* PhoneNumber phoneNumber = customer.getPhoneNumbers().get(1);</pre>
*
* <i>Return the base object</i>
* <pre> Customer customer = context.getValueByXPath(customer, ".", aNamespaceResolver, Customer.class);
* Customer customer = customer;
* </pre>
*
* @param <T> The return type of this method corresponds to the returnType parameter.
* @param object The XPath will be executed relative to this object.
* @param xPath The XPath statement
* @param namespaceResolver A NamespaceResolver containing the prefix/URI pairings from the XPath statement.
* @param returnType The return type.
* @return The object corresponding to the XPath or null if no result was found.
*/
public <T> T getValueByXPath(Object object, String xPath, NAMESPACE_RESOLVER namespaceResolver, Class<T> returnType) {
if (null == xPath || null == object) {
return null;
}
if (".".equals(xPath)) {
return (T) object;
}
ABSTRACT_SESSION session = this.getSession(object);
DESCRIPTOR descriptor = (DESCRIPTOR) session.getDescriptor(object);
StringTokenizer stringTokenizer = new StringTokenizer(xPath, Constants.XPATH_SEPARATOR);
T value = getValueByXPath(object, descriptor.getObjectBuilder(), stringTokenizer, namespaceResolver, returnType);
if (null == value) {
CoreMapping selfMapping = descriptor.getObjectBuilder().getMappingForField(createField(String.valueOf(Constants.DOT)));
if (null != selfMapping && selfMapping.getReferenceDescriptor() != null) {
return getValueByXPath(selfMapping.getAttributeValueFromObject(object), selfMapping.getReferenceDescriptor().getObjectBuilder(),
new StringTokenizer(xPath, Constants.XPATH_SEPARATOR), ((DESCRIPTOR) selfMapping.getReferenceDescriptor()).getNamespaceResolver(), returnType);
}
}
return value;
}
private <T> T getValueByXPath(Object object, CoreObjectBuilder objectBuilder, StringTokenizer stringTokenizer, NAMESPACE_RESOLVER namespaceResolver, Class<T> returnType) {
XPathQueryResult queryResult = getMappingForXPath(object, objectBuilder, stringTokenizer, namespaceResolver);
if (null != queryResult) {
CoreMapping mapping = queryResult.mapping;
Object owner = queryResult.owner;
Integer index = queryResult.index;
if (null != owner) {
Object childObject = null;
if (mapping.isCollectionMapping()) {
Object collection = mapping.getAttributeValueFromObject(owner);
if (List.class.isAssignableFrom(collection.getClass())) {
List list = (List) collection;
if (null == index) {
return (T) collection;
}
if (index >= list.size()) {
return null;
}
childObject = list.get(index);
}
} else {
childObject = mapping.getAttributeValueFromObject(owner);
}
return (T) childObject;
}
}
return null;
}
/**
* INTERNAL:
* Return true if any session held onto by this context has a document preservation
* policy that requires unmarshalling from a Node.
*/
public abstract boolean hasDocumentPreservation();
private String removeIndexesFromXPath(String xpathWithIndexes) {
String newXPath = xpathWithIndexes;
while (newXPath.contains(Constants.XPATH_INDEX_OPEN)) {
int open = newXPath.lastIndexOf(Constants.XPATH_INDEX_OPEN);
int closed = newXPath.lastIndexOf(Constants.XPATH_INDEX_CLOSED);
newXPath = newXPath.substring(0, open) + newXPath.substring(closed + 1);
}
return newXPath;
}
private void setValueByXPath(Object object, CoreObjectBuilder objectBuilder, StringTokenizer stringTokenizer, NAMESPACE_RESOLVER namespaceResolver, Object value) {
XPathQueryResult queryResult = getMappingForXPath(object, objectBuilder, stringTokenizer, namespaceResolver);
if (null != queryResult) {
CoreMapping mapping = queryResult.mapping;
Object owner = queryResult.owner;
Integer index = queryResult.index;
if (null != owner) {
if (mapping.isCollectionMapping()) {
Object childObject = null;
Object collection = mapping.getAttributeValueFromObject(owner);
if (List.class.isAssignableFrom(collection.getClass())) {
List list = (List) collection;
if (null == index) {
// We are setting the whole collection, not an element in the collection
if (value.getClass().isArray()) {
ArrayList newList = new ArrayList();
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
newList.add(Array.get(value, i));
}
value = newList;
}
mapping.setAttributeValueInObject(owner, value);
return;
}
if (index >= list.size()) {
return;
}
// Set into collection
list.set(index, value);
mapping.setAttributeValueInObject(owner, list);
return;
}
} else {
mapping.setAttributeValueInObject(owner, value);
}
}
}
}
/**
* <p>Set values in the object model based on the corresponding document. The following pairings are equivalent:</p>
*
* <i>Set the Customer's ID</i>
* <pre> context.setValueByXPath(customer, "@id", null, Integer.valueOf(123));
* customer.setId(Integer.valueOf(123));</pre>
*
* <i>Set the Customer's Name</i>
* <pre> context.setValueByXPath(customer, "ns:personal-info/ns:name/text()", aNamespaceResolver, "Jane Doe");
* customer.setName("Jane Doe");</pre>
*
* <i>Set the Customer's Address</i>
* <pre> context.setValueByXPath(customer, "ns:contact-info/ns:address", aNamespaceResolver, anAddress);
* customer.setAddress(anAddress);</pre>
*
* <i>Set the Customer's PhoneNumbers</i>
* <pre> context.setValueByXPath(customer, "ns:contact-info/ns:phone-number", aNamespaceResolver, phoneNumbers);
* customer.setPhoneNumbers(phoneNumbers);</pre>
*
* <i>Set the Customer's second PhoneNumber</i>
* <pre> context.setValueByXPath(customer, "ns:contact-info/ns:phone-number[2]", aNamespaceResolver, aPhoneNumber);
* customer.getPhoneNumbers().get(1);</pre>
*
* @param object The XPath will be executed relative to this object.
* @param xPath The XPath statement
* @param namespaceResolver A NamespaceResolver containing the prefix/URI pairings from the XPath statement.
* @param value The value to be set.
*/
public void setValueByXPath(Object object, String xPath, NAMESPACE_RESOLVER namespaceResolver, Object value) {
ABSTRACT_SESSION session = this.getSession(object);
DESCRIPTOR descriptor = (DESCRIPTOR) session.getDescriptor(object);
StringTokenizer stringTokenizer = new StringTokenizer(xPath, "/");
setValueByXPath(object, descriptor.getObjectBuilder(), stringTokenizer, namespaceResolver, value);
}
}