package com.ebay.cloud.cms.typsafe.service;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ebay.cloud.cms.typsafe.entity.CMSQuery;
import com.ebay.cloud.cms.typsafe.entity.CMSQueryResult;
import com.ebay.cloud.cms.typsafe.entity.GenericCMSEntity;
import com.ebay.cloud.cms.typsafe.entity.ICMSEntity;
import com.ebay.cloud.cms.typsafe.entity.RelationshipField;
import com.ebay.cloud.cms.typsafe.exception.CMSClientException;
import com.ebay.cloud.cms.typsafe.exception.CMSEntityException;
import com.ebay.cloud.cms.typsafe.metadata.model.MetaClass;
import com.ebay.cloud.cms.typsafe.metadata.model.MetaField;
import com.ebay.cloud.cms.typsafe.metadata.model.MetaField.CardinalityEnum;
import com.ebay.cloud.cms.typsafe.metadata.model.MetaRelationship;
import com.ebay.cloud.cms.typsafe.metadata.model.MetaRelationship.RelationTypeEnum;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
class RelationshipService {
private static final Logger logger = LoggerFactory.getLogger(RelationshipService.class);
private final CMSClientService service;
private LoadingCache<String, MetaClass> metadatas;
private static String RELATIONSHIP_QUERY_TEMPLATE = "%s[@_oid=\"%s\"].%s[@_oid=\"%s\"]";
private static final int MAX_RETRY_COUNT = 300;
RelationshipService(CMSClientService service) {
this.service = service;
this.metadatas = CacheBuilder.newBuilder().maximumSize(500).expireAfterAccess(30, TimeUnit.MINUTES)
.build(new CacheLoader<String, MetaClass>() {
@Override
public MetaClass load(String key) throws Exception {
return RelationshipService.this.service.getMetadata(key, null);
}
});
}
/**
* Creates relationship between the fromEntity and the toEntity. If the
* fromEntity(toEntity) is not existing, a new entity will be created.
*
* @param fromEntity
* @param refField
* @param toEntity
* @param context
*/
public <T extends ICMSEntity, K extends ICMSEntity> void createRelationship(final T fromEntity, final String refField,
final K toEntity, CMSClientContext context) {
context = context != null ? context : new CMSClientContext();
String fromType = fromEntity.get_type();
MetaClass fromMetaClass = getMetadata(fromType);
ensureFromEntity(fromEntity, refField, context);
MetaField field = fromMetaClass.getField(refField);
if (!(field instanceof MetaRelationship)) {
throw new CMSEntityException(MessageFormat.format("Field {0} on class {1} is not relationship!", refField, fromType));
}
MetaRelationship relation = (MetaRelationship) field;
ensureToEntity(fromEntity, refField, toEntity, context, fromType);
if (relation.getRelationType().equals(RelationTypeEnum.Reference)) {
// array handling
final T opEntity = createNewEntity(fromEntity);
if (field.getCardinality() == CardinalityEnum.Many) {
opEntity.removeFieldValue(refField);
opEntity.addFieldValue(refField, toEntity);
} else {
opEntity.setFieldValue(refField, toEntity);
}
final CMSClientContext fContext = context;
// CMS-3930:: add retry for update operation in relationship API
retryRelationshipOperation(service, logger, context, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
String query = String.format(RELATIONSHIP_QUERY_TEMPLATE, opEntity.get_type(), opEntity.get_id(), refField, toEntity.get_id());
CMSQuery cmsQuery = new CMSQuery(query);
CMSQueryResult<GenericCMSEntity> ge = service.query(cmsQuery, fContext);
if (ge.getEntities().size() > 0) {
// already existing
return 0;
}
service.updateEntityField(opEntity, refField, fContext);
return 0;
}
}, opEntity.get_type(), opEntity.get_id(), refField, toEntity.get_type(), toEntity.get_id(), "create");
}
}
private <T extends ICMSEntity, K extends ICMSEntity> void ensureToEntity(T fromEntity, String refField, K toEntity,
CMSClientContext context, String fromType) {
if (!StringUtils.isEmpty(toEntity.get_id())) {
K getTo = getEntity(toEntity, context);
if (getTo == null) {
// create to entity
context.setPath(fromType, fromEntity.get_id(), refField);
service.create(toEntity, context);
}
} else {
context.setPath(fromType, fromEntity.get_id(), refField);
service.create(toEntity, context);
}
}
private <T extends ICMSEntity> void ensureFromEntity(T fromEntity, String refField, CMSClientContext context) {
if (!StringUtils.isEmpty(fromEntity.get_id())) {
T getFrom = getEntity(fromEntity, context);
if (getFrom == null) {
createOrUpdateFromEntity(fromEntity, refField, context, true);
} else {
createOrUpdateFromEntity(fromEntity, refField, context, false);
}
} else {
createOrUpdateFromEntity(fromEntity, refField, context, true);
}
}
private <T extends ICMSEntity> T getEntity(T givenEntity, CMSClientContext context) {
if (givenEntity.getClass() == GenericCMSEntity.class) {
return (T) service.get(givenEntity.get_id(), givenEntity.get_type(), context);
} else {
return service.get(givenEntity.get_id(), (Class<T>) givenEntity.getClass(), context);
}
}
private <T extends ICMSEntity> void createOrUpdateFromEntity(final T fromEntity, final String refField, final CMSClientContext context, boolean create) {
boolean hasOldValue = fromEntity.hasField(refField);
Object oldFieldValue = fromEntity.getFieldValue(refField);
fromEntity.removeFieldValue(refField);
if (create) {
service.create(fromEntity, context);
} else {
// CMS-3930:: add retry for update operation in relationship API
GenericEntityService.retryOperation(service, logger, context, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
service.update(fromEntity, context);
return 0;
}
}, GenericEntityService.RETRY_LOG_MESSAGE, -1);
}
if (hasOldValue) {
fromEntity.setFieldValue(refField, oldFieldValue);
}
}
@Deprecated
public <T extends ICMSEntity, K extends ICMSEntity> void createRelationship(T fromEntity, RelationshipField<T, K> field, K toEntity, CMSClientContext context) {
Preconditions.checkNotNull(field, "Relationship field couldn't be null!");
createRelationship(fromEntity, field.getFieldName(), toEntity, context);
}
/**
* Deletes relationship between the fromEntity and the toEntity if any.
*
*
* @param fromEntity
* @param refField
* @param toEntity
* @param context
*/
public <T extends ICMSEntity, K extends ICMSEntity> void deleteRelationship(final T fromEntity, final String refField,
final K toEntity, CMSClientContext context) {
context = context != null ? context : new CMSClientContext();
Preconditions.checkArgument(!StringUtils.isEmpty(refField), String.format("refField could not be null!"));
T getFrom = getEntity(fromEntity, context);
if (getFrom == null) {
// no from entity found
return;
}
K getTo = getEntity(toEntity, context);
if (getTo == null) {
// no to entity found
return;
}
final String fromType = fromEntity.get_type();
MetaClass meta = getMetadata(fromType);
final MetaField field = meta.getField(refField);
if (! (field instanceof MetaRelationship)) {
throw new CMSEntityException(MessageFormat.format("Field {0} on class {1} is not relationship!", refField, fromType));
}
MetaRelationship relationship = (MetaRelationship)field;
if (relationship.getRelationType().equals(RelationTypeEnum.Inner) || relationship.getRelationType().equals(RelationTypeEnum.Embedded)) {
// delete inner/ebmed entity will automatically remove the reference
service.delete(toEntity, context);
} else {
// array handling
final T opEntity = createNewEntity(fromEntity);
if (field.getCardinality() == CardinalityEnum.Many) {
// create new Entity for operation. Can not rely on the given
// entity, since user might pass untentional objects
opEntity.setFieldValue(refField, Arrays.asList(toEntity));
} else {
opEntity.setFieldValue(refField, toEntity);
}
final CMSClientContext fContext = context;
// CMS-3930:: add retry for update operation in relationship API
retryRelationshipOperation(service, logger, context, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
String query = String.format(RELATIONSHIP_QUERY_TEMPLATE, opEntity.get_type(), opEntity.get_id(), refField, toEntity.get_id());
CMSQuery cmsQuery = new CMSQuery(query);
CMSQueryResult<GenericCMSEntity> ge = service.query(cmsQuery, fContext);
if (ge.getEntities().size() == 0) {
// already removed
return 0;
}
service.deleteField(opEntity, refField, fContext);
return 0;
}
}, opEntity.get_type(), opEntity.get_id(), refField, toEntity.get_type(), toEntity.get_id(), "delete");
}
}
// copy the entity with id and type set from given entity
private <T extends ICMSEntity> T createNewEntity(T entity) {
T newEntity;
try {
newEntity = (T) entity.getClass().newInstance();
newEntity.set_id(entity.get_id());
newEntity.set_type(entity.get_type());
return newEntity;
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new CMSEntityException(MessageFormat.format("Can not create new entity for operation! Type: {0}",
entity.getClass().getSimpleName()));
}
}
/**
* A almost loop-ever retry. Use mass of parameter
* for logging...
*
*/
private <T extends ICMSEntity, K> K retryRelationshipOperation(CMSClientService service, Logger logger, CMSClientContext context, Callable<K> op,
String fromType, String fromId, String refField, String toType, String toId, String opName) {
String message = String.format(" failed operation of " + opName + " relationship " +
"from entity of type: %s, _oid: %s, through field: %s, to entity of type: %s, _oid: %s ! " +
"This is the relationship loop, will retry! Current retry count is {5}! ",
fromType, fromId, refField, toType, toId);
return GenericEntityService.retryOperation(service, logger, context, op, message, MAX_RETRY_COUNT);
}
@Deprecated
public <T extends ICMSEntity, K extends ICMSEntity> void deleteRelationship(T fromEntity, RelationshipField<T, K>field, K toEntity, CMSClientContext context) {
Preconditions.checkNotNull(field, "Relationship field couldn't be null!");
deleteRelationship(fromEntity, field.getFieldName(), toEntity, context);
}
public MetaClass getMetadata(String fromType) {
MetaClass meta = null;
try {
meta = this.metadatas.get(fromType);
} catch (ExecutionException e) {
String msg = MessageFormat.format("Unable to find metaclass {0}!", fromType);
logger.error(msg);
throw new CMSClientException(msg, e);
}
return meta;
}
}