Json Query has been implemented

This commit is contained in:
Luca Frosini 2021-09-27 18:00:18 +02:00
parent 4e43217b63
commit 2b71d1db12
6 changed files with 599 additions and 4 deletions

View File

@ -0,0 +1,10 @@
package org.gcube.informationsystem.resourceregistry.query;
import org.gcube.informationsystem.resourceregistry.api.exceptions.ResourceRegistryException;
import org.gcube.informationsystem.resourceregistry.api.exceptions.query.InvalidQueryException;
public interface JsonQuery {
public String query(String jsonQuery) throws InvalidQueryException, ResourceRegistryException;
}

View File

@ -0,0 +1,297 @@
package org.gcube.informationsystem.resourceregistry.query;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.gcube.com.fasterxml.jackson.databind.JsonNode;
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode;
import org.gcube.informationsystem.base.reference.AccessType;
import org.gcube.informationsystem.base.reference.Element;
import org.gcube.informationsystem.model.reference.entities.Resource;
import org.gcube.informationsystem.model.reference.relations.ConsistsOf;
import org.gcube.informationsystem.model.reference.relations.Relation;
import org.gcube.informationsystem.resourceregistry.api.exceptions.ResourceRegistryException;
import org.gcube.informationsystem.resourceregistry.api.exceptions.query.InvalidQueryException;
import org.gcube.informationsystem.resourceregistry.api.exceptions.schema.SchemaNotFoundException;
import org.gcube.informationsystem.resourceregistry.api.rest.AccessPath;
import org.gcube.informationsystem.resourceregistry.contexts.ContextUtility;
import org.gcube.informationsystem.resourceregistry.contexts.security.SecurityContext;
import org.gcube.informationsystem.resourceregistry.contexts.security.SecurityContext.PermissionMode;
import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagement;
import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagementUtility;
import org.gcube.informationsystem.resourceregistry.instances.model.entities.ResourceManagement;
import org.gcube.informationsystem.resourceregistry.utils.Utility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.executor.OResultSet;
/**
* @author Luca Frosini (ISTI - CNR)
*/
public class JsonQueryImpl implements JsonQuery {
private static Logger logger = LoggerFactory.getLogger(JsonQueryImpl.class);
@Override
public String query(String jsonQuery) throws InvalidQueryException, ResourceRegistryException {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonQuery);
return query(jsonNode, objectMapper);
} catch(ResourceRegistryException e) {
throw e;
} catch(Exception e) {
throw new InvalidQueryException(e.getMessage());
}
}
protected StringBuffer createQuery(JsonNode jsonQuery) throws SchemaNotFoundException, InvalidQueryException {
StringBuffer stringBuffer = new StringBuffer();
String requestedResourceType = jsonQuery.get(Element.CLASS_PROPERTY).asText();
validateType(requestedResourceType, AccessType.RESOURCE);
ArrayNode consistsOfArray = (ArrayNode) jsonQuery.get(Resource.CONSISTS_OF_PROPERTY);
if(consistsOfArray.size()>0) {
stringBuffer.append("SELECT FROM (");
stringBuffer.append("TRAVERSE outV(\"");
stringBuffer.append(requestedResourceType);
stringBuffer.append("\") FROM (");
StringBuffer consistsOfBuffer = new StringBuffer();
for(int i=0; i<consistsOfArray.size(); i++) {
JsonNode consistsOfJsonNode = consistsOfArray.get(i);
consistsOfBuffer = analizeConsistsOf(consistsOfJsonNode, requestedResourceType, consistsOfBuffer, i);
}
stringBuffer.append(consistsOfBuffer);
stringBuffer.append(")");
stringBuffer.append(")");
}else {
stringBuffer.append("SELECT FROM ");
stringBuffer.append(requestedResourceType);
}
return stringBuffer;
}
public String query(JsonNode jsonQuery, ObjectMapper objectMapper) throws InvalidQueryException, ResourceRegistryException {
ODatabaseDocument current = ContextUtility.getCurrentODatabaseDocumentFromThreadLocal();
ODatabaseDocument oDatabaseDocument = null;
try {
Integer limit = AccessPath.UNBOUNDED;
SecurityContext securityContext = ContextUtility.getCurrentSecurityContext();
oDatabaseDocument = securityContext.getDatabaseDocument(PermissionMode.READER);
oDatabaseDocument.begin();
StringBuffer stringBuffer = createQuery(jsonQuery);
stringBuffer.append(" limit :limit");
Map<String, Object> map = new HashMap<>();
map.put("limit", limit);
OResultSet resultSet = oDatabaseDocument.query(stringBuffer.toString(), map);
ArrayNode arrayNode = objectMapper.createArrayNode();
while(resultSet.hasNext()) {
OResult oResult = resultSet.next();
OElement element = ElementManagementUtility.getElementFromOptional(oResult.getElement());
try {
JsonNode jsonNodeResult = null;
ElementManagement<?,?> erManagement = ElementManagementUtility.getERManagement(securityContext, oDatabaseDocument,
element);
if(erManagement instanceof ResourceManagement) {
jsonNodeResult = erManagement.serializeAsJsonNode();
arrayNode.add(jsonNodeResult);
}else {
// Got relations and facets because the query does not include @class="TypeName" to support polymorphism
}
} catch(ResourceRegistryException e) {
logger.error("Unable to correctly serialize {}. It will be excluded from results. {}",
element.toString(), Utility.SHOULD_NOT_OCCUR_ERROR_MESSAGE);
}
}
return objectMapper.writeValueAsString(arrayNode);
} catch(Exception e) {
throw new InvalidQueryException(e.getMessage());
} finally {
if(oDatabaseDocument != null) {
oDatabaseDocument.close();
}
if(current!=null) {
current.activateOnCurrentThread();
}
}
}
private StringBuffer addNameToCompare(StringBuffer stringBuffer, String fieldName, String fieldNamePrefix) {
if(fieldNamePrefix!=null) {
stringBuffer.append(fieldNamePrefix);
stringBuffer.append(".");
}
stringBuffer.append(fieldName);
stringBuffer.append("=");
return stringBuffer;
}
private StringBuffer addValueToMatch(StringBuffer stringBuffer, JsonNode gotJoJsonNode, String value) {
if(gotJoJsonNode.isNumber()) {
stringBuffer.append(value);
} else {
stringBuffer.append("\"");
stringBuffer.append(value);
stringBuffer.append("\"");
}
return stringBuffer;
}
private StringBuffer getNameValueToMatch(JsonNode jsonNode, String fieldName, String fieldNamePrefix) throws InvalidQueryException {
StringBuffer stringBuffer = new StringBuffer();
JsonNode gotJoJsonNode = jsonNode.get(fieldName);
if(gotJoJsonNode.isContainerNode()) {
if(gotJoJsonNode.isArray()) {
throw new InvalidQueryException("Array not supported for " + fieldName);
}
if(gotJoJsonNode.isObject()) {
StringBuffer newPrefix = new StringBuffer();
if(fieldNamePrefix!=null) {
newPrefix.append(fieldNamePrefix);
newPrefix.append(".");
}
newPrefix.append(fieldName);
return addWhereConstraint(gotJoJsonNode, stringBuffer, newPrefix.toString());
}
}
addNameToCompare(stringBuffer, fieldName, fieldNamePrefix);
String value = jsonNode.get(fieldName).asText();
addValueToMatch(stringBuffer, gotJoJsonNode, value);
return stringBuffer;
}
private StringBuffer addWhereConstraint(JsonNode jsonNode, StringBuffer stringBuffer, String fieldNamePrefix) throws InvalidQueryException {
Iterator<String> iterator = jsonNode.fieldNames();
boolean first = true;
while(iterator.hasNext()) {
String fieldName = iterator.next();
if(fieldName.compareTo(Element.CLASS_PROPERTY)==0) {
continue;
}
if(fieldName.compareTo(Relation.TARGET_PROPERTY)==0) {
continue;
}
if(first) {
first = false;
}else {
stringBuffer.append(" AND ");
}
stringBuffer.append(getNameValueToMatch(jsonNode, fieldName, fieldNamePrefix));
}
return stringBuffer;
}
private StringBuffer analizeFacet(JsonNode facetJsonNode, StringBuffer stringBuffer, int i) throws InvalidQueryException {
StringBuffer newBuffer = new StringBuffer();
int size = facetJsonNode.size();
newBuffer.append("SELECT FROM ");
String facetType = facetJsonNode.get(Element.CLASS_PROPERTY).asText();
if(i!=0) {
newBuffer.append(" (");
newBuffer.append("TRAVERSE inV(\"");
}
newBuffer.append(facetType);
if(i!=0) {
newBuffer.append("\") FROM (");
newBuffer.append(stringBuffer);
newBuffer.append(")");
newBuffer.append(")");
}
// Size 1 means that only '@class' property is present
if(size > 1) {
newBuffer.append(" WHERE ");
addWhereConstraint(facetJsonNode, newBuffer, null);
}
return newBuffer;
}
private StringBuffer analizeConsistsOf(JsonNode consistsOfJsonNode, String resourceType, StringBuffer stringBuffer, int i) throws InvalidQueryException {
StringBuffer newBuffer = new StringBuffer();
int size = consistsOfJsonNode.size();
if(size > 2) {
newBuffer.append("SELECT FROM ( ");
}
newBuffer.append("TRAVERSE inE(\"");
String consistsOfType = consistsOfJsonNode.get(Element.CLASS_PROPERTY).asText();
newBuffer.append(consistsOfType);
newBuffer.append("\") FROM ("); // Open (
JsonNode facetJsonNode = consistsOfJsonNode.get(ConsistsOf.TARGET_PROPERTY);
if(i>0) {
StringBuffer anotherBuffer = new StringBuffer();
anotherBuffer.append(" TRAVERSE outV(\"");
anotherBuffer.append(resourceType);
anotherBuffer.append("\").outE(\"");
anotherBuffer.append(consistsOfType);
anotherBuffer.append("\") FROM (");
anotherBuffer.append(stringBuffer);
anotherBuffer.append(")");
stringBuffer = anotherBuffer;
}
newBuffer.append(analizeFacet(facetJsonNode, stringBuffer, i));
newBuffer.append(")"); // Close )
// Size 2 means that only '@class' and 'target' properties present
if(size > 2) {
newBuffer.append(") WHERE ");
addWhereConstraint(consistsOfJsonNode, newBuffer, null);
}
return newBuffer;
}
private void validateType(String requestedResourceType, AccessType resource) throws SchemaNotFoundException {
// TODO Auto-generated method stub
}
}

View File

@ -8,6 +8,7 @@ import java.util.UUID;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.HEAD; import javax.ws.rs.HEAD;
import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@ -35,6 +36,8 @@ import org.gcube.informationsystem.resourceregistry.contexts.entities.ContextMan
import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagement; import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagement;
import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagementUtility; import org.gcube.informationsystem.resourceregistry.instances.base.ElementManagementUtility;
import org.gcube.informationsystem.resourceregistry.instances.model.entities.ResourceManagement; import org.gcube.informationsystem.resourceregistry.instances.model.entities.ResourceManagement;
import org.gcube.informationsystem.resourceregistry.query.JsonQuery;
import org.gcube.informationsystem.resourceregistry.query.JsonQueryImpl;
import org.gcube.informationsystem.resourceregistry.query.Query; import org.gcube.informationsystem.resourceregistry.query.Query;
import org.gcube.informationsystem.resourceregistry.query.QueryImpl; import org.gcube.informationsystem.resourceregistry.query.QueryImpl;
import org.gcube.informationsystem.resourceregistry.types.SchemaManagement; import org.gcube.informationsystem.resourceregistry.types.SchemaManagement;
@ -222,7 +225,7 @@ public class Access extends BaseRest {
* https://orientdb.com/docs/last/SQL-Syntax.html </a> <br /> * https://orientdb.com/docs/last/SQL-Syntax.html </a> <br />
* <br /> * <br />
* *
* e.g. GET /access/query?q=SELECT FROM V&limit=20&fetchPlan=*:-1 * e.g. GET /access/graph-query?q=SELECT FROM V&limit=20&fetchPlan=*:-1
* *
* @param query Defines the query to send to the backend. * @param query Defines the query to send to the backend.
* @param limit Defines the number of results you want returned (default 20, use -1 to unbounded results) * @param limit Defines the number of results you want returned (default 20, use -1 to unbounded results)
@ -235,15 +238,15 @@ public class Access extends BaseRest {
* @throws InvalidQueryException if the query is invalid or not idempotent * @throws InvalidQueryException if the query is invalid or not idempotent
*/ */
@GET @GET
@Path(AccessPath.QUERY_PATH_PART) @Path(AccessPath.GRAPH_QUERY_PATH_PART)
@Produces(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8) @Produces(ResourceInitializer.APPLICATION_JSON_CHARSET_UTF_8)
public String query(@QueryParam(AccessPath.QUERY_PARAM) String query, public String graphQuery(@QueryParam(AccessPath.QUERY_PARAM) String query,
@QueryParam(AccessPath.LIMIT_PARAM) Integer limit, @QueryParam(AccessPath.LIMIT_PARAM) Integer limit,
@QueryParam(AccessPath.FETCH_PLAN_PARAM) @DefaultValue(AccessPath.DEFAULT_FETCH_PLAN_PARAM) String fetchPlan, @QueryParam(AccessPath.FETCH_PLAN_PARAM) @DefaultValue(AccessPath.DEFAULT_FETCH_PLAN_PARAM) String fetchPlan,
@QueryParam(AccessPath.RAW_PARAM) @DefaultValue(AccessPath.DEFAULT_RAW_PARAM) Boolean raw) @QueryParam(AccessPath.RAW_PARAM) @DefaultValue(AccessPath.DEFAULT_RAW_PARAM) Boolean raw)
throws InvalidQueryException { throws InvalidQueryException {
logger.info("Requested query (fetch plan {}, limit : {}, Raw : raw):\n{}", fetchPlan, limit, query, raw); logger.info("Requested query (fetch plan {}, limit : {}, Raw : raw):\n{}", fetchPlan, limit, query, raw);
CalledMethodProvider.instance.set("rawQuery"); CalledMethodProvider.instance.set("graphQuery");
checkHierarchicalMode(); checkHierarchicalMode();
checkIncludeInstancesContexts(); checkIncludeInstancesContexts();
@ -252,6 +255,20 @@ public class Access extends BaseRest {
return queryManager.query(query, limit, fetchPlan, raw); return queryManager.query(query, limit, fetchPlan, raw);
} }
@POST
@Path(AccessPath.QUERY_PATH_PART)
public String jsonQuery(String jsonQuery) throws InvalidQueryException, ResourceRegistryException {
logger.info("Requested json query \n{}", jsonQuery);
CalledMethodProvider.instance.set("jsonQuery");
checkHierarchicalMode();
checkIncludeInstancesContexts();
JsonQuery jsonQueryManager = new JsonQueryImpl();
return jsonQueryManager.query(jsonQuery);
}
/* /*
* /access/query/{RESOURCE_TYPE_NAME}/{RELATION_TYPE_NAME}/{ENTITY_TYPE_NAME}[?reference={REFERENCE_ENTITY_UUID}&polymorphic=true&direction=out] * /access/query/{RESOURCE_TYPE_NAME}/{RELATION_TYPE_NAME}/{ENTITY_TYPE_NAME}[?reference={REFERENCE_ENTITY_UUID}&polymorphic=true&direction=out]
* *

View File

@ -0,0 +1,46 @@
package org.gcube.informationsystem.resourceregistry.query;
import java.io.File;
import java.net.URL;
import org.gcube.com.fasterxml.jackson.databind.JsonNode;
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
import org.gcube.informationsystem.resourceregistry.ContextTest;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JsonQueryTest extends ContextTest {
private static Logger logger = LoggerFactory.getLogger(JsonQueryTest.class);
@Test
public void testCreateQuery() throws Exception {
URL url = JsonQueryTest.class.getResource("query.json");
File queryFile = new File(url.toURI());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(queryFile);
logger.info("Going to test the following JSON query {}", jsonNode.toString());
JsonQueryImpl jsonQuery = new JsonQueryImpl();
StringBuffer stringBuffer = jsonQuery.createQuery(jsonNode);
logger.info(stringBuffer.toString());
}
@Test
public void testQuery() throws Exception {
ContextTest.setContextByName(DEVVRE);
URL url = JsonQueryTest.class.getResource("query.json");
File queryFile = new File(url.toURI());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(queryFile);
logger.info("Going to test the following JSON query {}", jsonNode.toString());
JsonQueryImpl jsonQuery = new JsonQueryImpl();
String res = jsonQuery.query(jsonNode, objectMapper);
logger.info(res);
}
}

View File

@ -0,0 +1,186 @@
{
"@class": "EService",
"consistsOf": [
{
"@class": "ConsistsOf",
"propagationConstraint" : {
"add": "propagate"
},
"target": {
"@class": "StateFacet",
"value": "down"
}
},
{
"@class": "IsIdentifiedBy",
"target": {
"@class": "SoftwareFacet",
"name": "data-transfer-service",
"group": "DataTransfer"
}
},
{
"@class": "ConsistsOf",
"target": {
"@class": "AccessPointFacet",
"endpoint": "http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
}
}
],
"isRelatedTo" : [
{
"@class": "activates",
"source": {
"@class": "HostingNode"
}
}
]
}
SELECT FROM (
TRAVERSE outV("HostingNode") FROM (
TRAVERSE inE("Activates") FROM (
TRAVERSE outV("EService") FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM (
TRAVERSE inV("AccessPointFacet") FROM (
SELECT FROM (
TRAVERSE outE("ConsistsOf") FROM (
TRAVERSE outV("EService") FROM (
TRAVERSE inE("IsIdentifiedBy") FROM (
SELECT FROM (
TRAVERSE inV("SoftwareFacet") FROM (
SELECT FROM (
TRAVERSE outE("IsIdentifiedBy") FROM (
TRAVERSE outV("EService") FROM (
SELECT FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM StateFacet WHERE value="down"
)
) WHERE propagationConstraint.add="propagate"
)
)
)
)
) WHERE name="data-transfer-service" AND group="DataTransfer"
)
)
)
)
)
) WHERE endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
)
)
)
)
)
// WHERE @class="EService"
Ottimizzata rimuovendo i SELECT FROM dove non ci sono filtri da applicare sulla istanza:
SELECT FROM (
TRAVERSE outV("EService") FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM (
TRAVERSE inV("AccessPointFacet") FROM (
TRAVERSE outE("ConsistsOf") FROM (
TRAVERSE outV("EService") FROM (
TRAVERSE inE("IsIdentifiedBy") FROM (
SELECT FROM (
TRAVERSE inV("SoftwareFacet") FROM (
TRAVERSE outE("IsIdentifiedBy") FROM (
TRAVERSE outV("EService") FROM (
SELECT FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM StateFacet WHERE value="down"
)
) WHERE propagationConstraint.add="propagate"
)
)
)
) WHERE name="data-transfer-service" AND group="DataTransfer"
)
)
)
)
) WHERE endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
)
)
) WHERE @class="EService"
SELECT FROM (
TRAVERSE inE("ConsistsOf").outV("EService") FROM (
SELECT FROM (
TRAVERSE inE("IsIdentifiedBy").outV("EService").outE("ConsistsOf").inV("AccessPointFacet") FROM (
SELECT FROM (
TRAVERSE outV("EService").outE("IsIdentifiedBy").inV("SoftwareFacet") FROM (
SELECT FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM StateFacet WHERE value="down"
)
) WHERE propagationConstraint.add="propagate"
)
) WHERE name="data-transfer-service" AND group="DataTransfer"
)
) WHERE endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
)
) WHERE @class="EService"
// Generated from code:
SELECT FROM (
TRAVERSE outV("EService") FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM (
TRAVERSE inV("AccessPointFacet") FROM (
TRAVERSE outV("EService").outE("ConsistsOf") FROM (
TRAVERSE inE("IsIdentifiedBy") FROM (
SELECT FROM (
TRAVERSE inV("SoftwareFacet") FROM (
TRAVERSE outV("EService").outE("IsIdentifiedBy") FROM (
SELECT FROM (
TRAVERSE inE("ConsistsOf") FROM (
SELECT FROM StateFacet WHERE value="down"
)
) WHERE propagationConstraint.add="propagate"
)
)
) WHERE name="data-transfer-service" AND group="DataTransfer"
)
)
)
) WHERE endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
)
)
)
// This is validated by the code to get polymorphic result
// WHERE @class="EService"
MATCH
{class: StateFacet, where: (value="down")} .(in("ConsistsOf"){ where: (propagationConstraint!=null) }) {class: EService}
-IsIdentifiedBy-> {class: SoftwareFacet, where: (name="data-transfer-service" AND group="DataTransfer")}
<-ConsistsOf- {class: EService, as: result}
-ConsistsOf->{class: AccessPointFacet, where: (endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service")}
RETURN result
Non funziona con propagationConstraint.add="propagate"
MATCH
{class: StateFacet, where: (value="down")} .(in("ConsistsOf"){ where: (propagationConstraint.add="propagate") }) {class: EService}
-IsIdentifiedBy-> {class: SoftwareFacet, where: (name="data-transfer-service" AND group="DataTransfer")}
<-ConsistsOf- {class: EService, as: result}
-ConsistsOf->{class: AccessPointFacet, where: (endpoint="http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service")}
RETURN result

View File

@ -0,0 +1,39 @@
{
"@class": "EService",
"consistsOf": [
{
"@class": "ConsistsOf",
"propagationConstraint" : {
"add": "propagate"
},
"target": {
"@class": "StateFacet",
"value": "down"
}
},
{
"@class": "IsIdentifiedBy",
"target": {
"@class": "SoftwareFacet",
"name": "data-transfer-service",
"group": "DataTransfer"
}
},
{
"@class": "ConsistsOf",
"target": {
"@class": "AccessPointFacet",
"endpoint": "http://pc-frosini.isti.cnr.it:8080/data-transfer-service/gcube/service"
}
}
],
"isRelatedTo" : [
{
"@class": "activates",
"source": {
"@class": "HostingNode"
}
}
]
}