blob: 5c0bb556d2ca5c7ba1384bf538058b1d65fad488 [file] [log] [blame]
/*
* Copyright (c) 2011, 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:
// gonural - Initial implementation
// 2014-09-01-2.6.0 Dmitry Kornilov
// - JPARS v2.0 related changes
package org.eclipse.persistence.jpa.rs.resources.common;
import static org.eclipse.persistence.jpa.rs.util.StreamingOutputMarshaller.mediaType;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import jakarta.persistence.EntityManager;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.RelationalDescriptor;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.indirection.ValueHolder;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jpa.rs.PersistenceContext;
import org.eclipse.persistence.jpa.rs.QueryParameters;
import org.eclipse.persistence.jpa.rs.exceptions.JPARSException;
import org.eclipse.persistence.jpa.rs.features.FeatureResponseBuilder;
import org.eclipse.persistence.jpa.rs.features.FeatureSet;
import org.eclipse.persistence.jpa.rs.features.FeatureSet.Feature;
import org.eclipse.persistence.jpa.rs.features.ServiceVersion;
import org.eclipse.persistence.jpa.rs.features.fieldsfiltering.FieldsFilter;
import org.eclipse.persistence.jpa.rs.features.fieldsfiltering.FieldsFilteringValidator;
import org.eclipse.persistence.jpa.rs.features.paging.PageableFieldValidator;
import org.eclipse.persistence.jpa.rs.util.HrefHelper;
import org.eclipse.persistence.jpa.rs.util.IdHelper;
import org.eclipse.persistence.jpa.rs.util.JPARSLogger;
import org.eclipse.persistence.jpa.rs.util.StreamingOutputMarshaller;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.mappings.foundation.AbstractDirectMapping;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.sessions.DatabaseSession;
/**
* Base class for entity resource.
*
* @author gonural
*/
public abstract class AbstractEntityResource extends AbstractResource {
private static final String CLASS_NAME = AbstractEntityResource.class.getName();
protected Response findAttributeInternal(String version, String persistenceUnit, String type, String id, String attribute, HttpHeaders headers, UriInfo uriInfo) {
JPARSLogger.entering(CLASS_NAME, "findAttributeInternal", new Object[] { "GET", version, persistenceUnit, type, id, attribute, uriInfo.getRequestUri().toASCIIString() });
EntityManager em = null;
try {
PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
Object entityId = IdHelper.buildId(context, type, id);
em = context.getEmf().createEntityManager(getMatrixParameters(uriInfo, persistenceUnit));
Object entity = em.find(context.getClass(type), entityId, getQueryParameters(uriInfo));
DatabaseSession serverSession = context.getServerSession();
ClassDescriptor descriptor = serverSession.getClassDescriptor(context.getClass(type));
if (descriptor == null) {
throw JPARSException.classOrClassDescriptorCouldNotBeFoundForEntity(type, persistenceUnit);
}
DatabaseMapping attributeMapping = descriptor.getMappingForAttributeName(attribute);
if ((attributeMapping == null) || (entity == null)) {
throw JPARSException.databaseMappingCouldNotBeFoundForEntityAttribute(attribute, type, id, persistenceUnit);
}
if (!attributeMapping.isCollectionMapping()) {
Object result = attributeMapping.getRealAttributeValueFromAttribute(attributeMapping.getAttributeValueFromObject(entity), entity, (AbstractSession) serverSession);
if (result == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_entity_for_attribute", new Object[] { attribute, type, id, persistenceUnit });
throw JPARSException.attributeCouldNotBeFoundForEntity(attribute, type, id, persistenceUnit);
}
final FeatureResponseBuilder responseBuilder = context.getSupportedFeatureSet().getResponseBuilder(Feature.NO_PAGING);
return findAttributeResponse(context, attribute, type, id, persistenceUnit, result, getQueryParameters(uriInfo), headers, uriInfo, responseBuilder, null);
}
ReadQuery query = (ReadQuery) ((((ForeignReferenceMapping) attributeMapping).getSelectionQuery()).clone());
if (query == null) {
throw JPARSException.selectionQueryForAttributeCouldNotBeFoundForEntity(attribute, type, id, persistenceUnit);
}
final FeatureSet featureSet = context.getSupportedFeatureSet();
final AbstractSession clientSession = context.getClientSession(em);
// Fields filtering
FieldsFilter fieldsFilter = null;
if (context.getSupportedFeatureSet().isSupported(Feature.FIELDS_FILTERING)) {
final FieldsFilteringValidator fieldsFilteringValidator = new FieldsFilteringValidator(uriInfo);
if (fieldsFilteringValidator.isFeatureApplicable()) {
fieldsFilter = fieldsFilteringValidator.getFilter();
}
}
// Pagination
if (featureSet.isSupported(Feature.PAGING)) {
final PageableFieldValidator validator = new PageableFieldValidator(entity.getClass(), attribute, uriInfo);
if (validator.isFeatureApplicable()) {
// Adding extra one to detect are there more rows or not. It will be removed later
// on in response processor.
query.setMaxRows(validator.getLimit() + validator.getOffset() + 1);
query.setFirstResult(validator.getOffset());
// We need to add limit and offset to query parameters because request builder reads it from there
final Map<String, Object> queryParams = getQueryParameters(uriInfo);
queryParams.put(QueryParameters.JPARS_PAGING_LIMIT, String.valueOf(validator.getLimit()));
queryParams.put(QueryParameters.JPARS_PAGING_OFFSET, String.valueOf(validator.getOffset()));
// check orderBy, and generate a warning if there is none
checkOrderBy(context, query);
final Object result = clientSession.executeQuery(query, descriptor.getObjectBuilder().buildRow(entity, clientSession, WriteType.INSERT));
final FeatureResponseBuilder responseBuilder = context.getSupportedFeatureSet().getResponseBuilder(Feature.PAGING);
return findAttributeResponse(context, attribute, type, id, persistenceUnit, result, queryParams, headers, uriInfo, responseBuilder, fieldsFilter);
}
}
final Object result = clientSession.executeQuery(query, descriptor.getObjectBuilder().buildRow(entity, clientSession, WriteType.INSERT));
final FeatureResponseBuilder responseBuilder = context.getSupportedFeatureSet().getResponseBuilder(Feature.NO_PAGING);
return findAttributeResponse(context, attribute, type, id, persistenceUnit, result, getQueryParameters(uriInfo), headers, uriInfo, responseBuilder, fieldsFilter);
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
} finally {
if (em != null) {
if (em.isOpen()) {
em.close();
}
}
}
}
protected Response findInternal(String version, String persistenceUnit, String type, String id, HttpHeaders headers, UriInfo uriInfo) {
JPARSLogger.entering(CLASS_NAME, "findInternal", new Object[] { "GET", version, persistenceUnit, type, id, uriInfo.getRequestUri().toASCIIString() });
try {
final PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
final Map<String, String> discriminators = getMatrixParameters(uriInfo, persistenceUnit);
final Object entityId = IdHelper.buildId(context, type, id);
final Object entity = context.find(discriminators, type, entityId, getQueryParameters(uriInfo));
if (entity == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_entity_for_key", new Object[] { type, id, persistenceUnit });
throw JPARSException.entityNotFound(type, id, persistenceUnit);
}
// Fields filtering
if (context.getSupportedFeatureSet().isSupported(Feature.FIELDS_FILTERING)) {
final FieldsFilteringValidator fieldsFilteringValidator = new FieldsFilteringValidator(uriInfo);
if (fieldsFilteringValidator.isFeatureApplicable()) {
final StreamingOutputMarshaller marshaller = new StreamingOutputMarshaller(context,
singleEntityResponse(context, entity, uriInfo),
headers.getAcceptableMediaTypes(),
fieldsFilteringValidator.getFilter());
return Response.ok(marshaller).build();
}
}
return Response.ok(new StreamingOutputMarshaller(context, singleEntityResponse(context, entity, uriInfo), headers.getAcceptableMediaTypes())).build();
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
protected Response createInternal(String version, String persistenceUnit, String type, HttpHeaders headers, UriInfo uriInfo, InputStream in) {
JPARSLogger.entering(CLASS_NAME, "createInternal", new Object[] { "PUT", headers.getMediaType(), version, persistenceUnit, type, uriInfo.getRequestUri().toASCIIString() });
try {
final PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
final ClassDescriptor descriptor = context.getDescriptor(type);
if (descriptor == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_class_in_persistence_unit", new Object[] { type, persistenceUnit });
throw JPARSException.classOrClassDescriptorCouldNotBeFoundForEntity(type, persistenceUnit);
}
final Object entity = context.unmarshalEntity(type, mediaType(headers.getAcceptableMediaTypes()), in);
// Check idempotence of the entity
if (!checkIdempotence(descriptor, entity)) {
JPARSLogger.error(context.getSessionLog(), "jpars_put_not_idempotent", new Object[]{type, persistenceUnit});
throw JPARSException.entityIsNotIdempotent(type, persistenceUnit);
}
// Check idempotence of the entity's relationships
if (!checkIdempotenceOnRelationships(descriptor, entity)) {
JPARSLogger.error(context.getSessionLog(), "jpars_put_not_idempotent", new Object[]{type, persistenceUnit});
throw JPARSException.entityIsNotIdempotent(type, persistenceUnit);
}
// Cascade persist. Sets references to the parent object in collections with objects passed by value.
if (context.getServiceVersion().compareTo(ServiceVersion.VERSION_2_0) >= 0) {
processBidirectionalRelationships(context, descriptor, entity);
}
// No sequencing in relationships, we can create the object now...
context.create(getMatrixParameters(uriInfo, persistenceUnit), entity);
final ResponseBuilder rb = Response.status(Status.CREATED);
return rb.entity(new StreamingOutputMarshaller(context, singleEntityResponse(context, entity, uriInfo), headers.getAcceptableMediaTypes())).build();
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
/**
* Finds all bidirectional relationships of the given entity with Cascade=PERSIST and sets reference to the parent
* object.
* This method is called on creating new entities in JPARS v2.0 only.
*
* @param context the persistence context.
* @param descriptor descriptor of the entity passed in 'entity' parameter.
* @param entity entity to process.
*/
private void processBidirectionalRelationships(PersistenceContext context, ClassDescriptor descriptor, Object entity) {
final List<DatabaseMapping> mappings = descriptor.getMappings();
for (DatabaseMapping mapping : mappings) {
if (mapping instanceof ForeignReferenceMapping) {
final ForeignReferenceMapping jpaMapping = (ForeignReferenceMapping) mapping;
final Object attributeValue = mapping.getAttributeAccessor().getAttributeValueFromObject(entity);
if (jpaMapping.isCascadePersist()) {
if (jpaMapping.getMappedBy() != null) {
final ClassDescriptor inverseDescriptor = context.getDescriptor(jpaMapping.getReferenceDescriptor().getAlias());
if (inverseDescriptor != null) {
final DatabaseMapping inverseMapping = inverseDescriptor.getMappingForAttributeName(jpaMapping.getMappedBy());
if (inverseMapping != null) {
if (attributeValue != null) {
if (attributeValue instanceof ValueHolder) {
final ValueHolder<?> holder = (ValueHolder<?>) attributeValue;
final Object obj = holder.getValue();
if (obj != null) {
inverseMapping.setAttributeValueInObject(obj, entity);
}
} else if (attributeValue instanceof Collection) {
final Collection<?> collection = (Collection<?>) attributeValue;
if (!collection.isEmpty()) {
for (Object obj : collection) {
inverseMapping.setAttributeValueInObject(obj, entity);
}
}
}
}
}
}
}
}
}
}
}
/**
* This method maintains idempotence on PUT by disallowing sequencing in relationships.
*
* @param descriptor descriptor of the entity passed in 'entity' parameter.
* @param entity entity to process.
* @return true if check is passed (no sequencing)
*/
private boolean checkIdempotenceOnRelationships(ClassDescriptor descriptor, Object entity) {
final List<DatabaseMapping> mappings = descriptor.getMappings();
if ((mappings != null) && (!mappings.isEmpty())) {
for (DatabaseMapping mapping : mappings) {
if (mapping instanceof ForeignReferenceMapping) {
final ForeignReferenceMapping fkMapping = (ForeignReferenceMapping) mapping;
if ((fkMapping.isCascadePersist()) || (fkMapping.isCascadeMerge())) {
final ClassDescriptor referenceDescriptor = fkMapping.getReferenceDescriptor();
if (referenceDescriptor != null) {
if (referenceDescriptor instanceof RelationalDescriptor) {
final RelationalDescriptor relDesc = (RelationalDescriptor) referenceDescriptor;
final AbstractDirectMapping relSequenceMapping = relDesc.getObjectBuilder().getSequenceMapping();
if (relSequenceMapping != null) {
final Object value = mapping.getAttributeAccessor().getAttributeValueFromObject(entity);
if (value != null) {
if (value instanceof ValueHolder) {
final ValueHolder<?> holder = (ValueHolder<?>) value;
if (holder.getValue() != null) {
return false;
}
} else if (value instanceof Collection) {
if (!(((Collection<?>) value).isEmpty())) {
return false;
}
}
}
}
}
}
}
}
}
}
return true;
}
/**
* This method maintains idempotence on PUT by disallowing sequencing.
*
* @param descriptor descriptor of the entity passed in 'entity' parameter.
* @param entity entity to process.
* @return true if check is passed (no sequencing)
*/
private boolean checkIdempotence(ClassDescriptor descriptor, Object entity) {
final AbstractDirectMapping sequenceMapping = descriptor.getObjectBuilder().getSequenceMapping();
if (sequenceMapping != null) {
final Object value = sequenceMapping.getAttributeAccessor().getAttributeValueFromObject(entity);
return !descriptor.getObjectBuilder().isPrimaryKeyComponentInvalid(value, descriptor.getPrimaryKeyFields().indexOf(descriptor.getSequenceNumberField()))
&& !descriptor.getSequence().shouldAlwaysOverrideExistingValue();
}
return true;
}
protected Response updateInternal(String version, String persistenceUnit, String type, HttpHeaders headers, UriInfo uriInfo, InputStream in) {
JPARSLogger.entering(CLASS_NAME, "updateInternal", new Object[] { "POST", headers.getMediaType(), version, persistenceUnit, type, uriInfo.getRequestUri().toASCIIString() });
try {
PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
Object entity = context.unmarshalEntity(type, mediaType(headers.getAcceptableMediaTypes()), in);
entity = context.merge(getMatrixParameters(uriInfo, persistenceUnit), entity);
return Response.ok(new StreamingOutputMarshaller(context, singleEntityResponse(context, entity, uriInfo), headers.getAcceptableMediaTypes())).build();
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
protected Response setOrAddAttributeInternal(String version, String persistenceUnit, String type, String id, String attribute, HttpHeaders headers, UriInfo uriInfo, InputStream in) {
JPARSLogger.entering(CLASS_NAME, "setOrAddAttributeInternal", new Object[] { "POST", headers.getMediaType(), version, persistenceUnit, type, id, attribute, uriInfo.getRequestUri().toASCIIString() });
try {
PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
Object entityId = IdHelper.buildId(context, type, id);
String partner = getRelationshipPartner(getMatrixParameters(uriInfo, attribute), getQueryParameters(uriInfo));
ClassDescriptor descriptor = context.getDescriptor(type);
DatabaseMapping mapping = descriptor.getMappingForAttributeName(attribute);
if (!mapping.isForeignReferenceMapping()) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_appropriate_mapping_for_update", new Object[] { attribute, type, id, persistenceUnit });
throw JPARSException.databaseMappingCouldNotBeFoundForEntityAttribute(attribute, type, id, persistenceUnit);
}
Object entity = context.unmarshalEntity(mapping.getReferenceDescriptor().getAlias(), mediaType(headers.getAcceptableMediaTypes()), in);
Object result = context.updateOrAddAttribute(getMatrixParameters(uriInfo, persistenceUnit), type, entityId, getQueryParameters(uriInfo), attribute, entity, partner);
if (result == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_update_attribute", new Object[] { attribute, type, id, persistenceUnit });
throw JPARSException.attributeCouldNotBeUpdated(attribute, type, id, persistenceUnit);
}
return Response.ok(new StreamingOutputMarshaller(context, singleEntityResponse(context, result, uriInfo), headers.getAcceptableMediaTypes())).build();
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
protected Response removeAttributeInternal(String version, String persistenceUnit, String type, String id, String attribute, HttpHeaders headers, UriInfo uriInfo) {
JPARSLogger.entering(CLASS_NAME, "removeAttributeInternal", new Object[] { "DELETE", headers.getMediaType(), version, persistenceUnit, type, id, attribute, uriInfo.getRequestUri().toASCIIString() });
try {
String listItemId = null;
Map<String, String> matrixParams = getMatrixParameters(uriInfo, attribute);
Map<String, Object> queryParams = getQueryParameters(uriInfo);
if (!queryParams.isEmpty()) {
listItemId = (String) queryParams.get(QueryParameters.JPARS_LIST_ITEM_ID);
}
if ((attribute == null) && (listItemId == null)) {
throw JPARSException.invalidRemoveAttributeRequest(null, type, id, persistenceUnit);
}
String partner = getRelationshipPartner(matrixParams, queryParams);
PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
Object entityId = IdHelper.buildId(context, type, id);
ClassDescriptor descriptor = context.getDescriptor(type);
DatabaseMapping mapping = descriptor.getMappingForAttributeName(attribute);
if (!mapping.isForeignReferenceMapping()) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_appropriate_mapping_for_update", new Object[] { attribute, type, id, persistenceUnit });
throw JPARSException.databaseMappingCouldNotBeFoundForEntityAttribute(attribute, type, id, persistenceUnit);
}
Map<String, String> discriminators = getMatrixParameters(uriInfo, persistenceUnit);
Object entity = context.find(discriminators, type, entityId, getQueryParameters(uriInfo));
Object result = context.removeAttribute(getMatrixParameters(uriInfo, persistenceUnit), type, entityId, attribute, listItemId, entity, partner);
if (result == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_update_attribute", new Object[] { attribute, type, id, persistenceUnit });
throw JPARSException.attributeCouldNotBeUpdated(attribute, type, id, persistenceUnit);
} else {
return Response.ok(new StreamingOutputMarshaller(context, singleEntityResponse(context, result, uriInfo), headers.getAcceptableMediaTypes())).build();
}
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
protected Response deleteInternal(String version, String persistenceUnit, String type, String id, HttpHeaders headers, UriInfo uriInfo) {
JPARSLogger.entering(CLASS_NAME, "deleteInternal", new Object[] { "DELETE", headers.getMediaType(), version, persistenceUnit, type, id, uriInfo.getRequestUri().toASCIIString() });
try {
PersistenceContext context = getPersistenceContext(persistenceUnit, type, uriInfo.getBaseUri(), version, null);
Map<String, String> discriminators = getMatrixParameters(uriInfo, persistenceUnit);
Object entityId = IdHelper.buildId(context, type, id);
context.delete(discriminators, type, entityId);
return Response.ok().build();
} catch (Exception ex) {
throw JPARSException.exceptionOccurred(ex);
}
}
protected Response buildEntityOptionsResponse(String version, String persistenceUnit, String entityName, HttpHeaders httpHeaders, UriInfo uriInfo) {
JPARSLogger.entering(CLASS_NAME, "buildEntityOptionsResponse", new Object[]{"GET", version, persistenceUnit, entityName, uriInfo.getRequestUri().toASCIIString()});
final PersistenceContext context = getPersistenceContext(persistenceUnit, null, uriInfo.getBaseUri(), version, null);
// We need to make sure that entity with given name exists
final ClassDescriptor descriptor = context.getServerSession().getDescriptorForAlias(entityName);
if (descriptor == null) {
JPARSLogger.error(context.getSessionLog(), "jpars_could_not_find_entity_type", new Object[]{entityName, persistenceUnit});
throw JPARSException.classOrClassDescriptorCouldNotBeFoundForEntity(entityName, persistenceUnit);
}
final String linkValue = "<" + HrefHelper.buildEntityMetadataHref(context, entityName) + ">; rel=describedby";
httpHeaders.getRequestHeaders().putSingle("Link", linkValue);
return Response.ok()
.header("Link", linkValue)
.build();
}
private Response findAttributeResponse(PersistenceContext context,
String attribute,
String entityType,
String id,
String persistenceUnit,
Object queryResults,
Map<String, Object> queryParams,
HttpHeaders headers, UriInfo uriInfo,
FeatureResponseBuilder responseBuilder,
FieldsFilter filter) {
if (queryResults != null) {
Object results = responseBuilder.buildAttributeResponse(context, queryParams, attribute, queryResults, uriInfo);
if (results != null) {
return Response.ok(new StreamingOutputMarshaller(context, results, headers.getAcceptableMediaTypes(), filter)).build();
} else {
// something is wrong with the descriptors
throw JPARSException.responseCouldNotBeBuiltForFindAttributeRequest(attribute, entityType, id, persistenceUnit);
}
}
return Response.ok(new StreamingOutputMarshaller(context, null, headers.getAcceptableMediaTypes(), filter)).build();
}
private void checkOrderBy(PersistenceContext context, ReadQuery query) {
if (query.isReadAllQuery()) {
ReadAllQuery readAllQuery = (ReadAllQuery) query;
List<Expression> orderBy = readAllQuery.getOrderByExpressions();
if ((orderBy == null) || (orderBy.isEmpty())) {
JPARSLogger.warning(context.getSessionLog(), "no_orderby_clause_for_paging", new Object[] { query.toString() });
}
}
}
private Object singleEntityResponse(PersistenceContext context, Object entity, UriInfo uriInfo) {
FeatureSet featureSet = context.getSupportedFeatureSet();
FeatureResponseBuilder responseBuilder = featureSet.getResponseBuilder(Feature.NO_PAGING);
return responseBuilder.buildSingleEntityResponse(context, getQueryParameters(uriInfo), entity, uriInfo);
}
}