/** * */ package com.ebay.cloud.cms.typsafe.entity.internal; import java.text.MessageFormat; import java.util.List; import org.codehaus.jackson.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ebay.cloud.cms.typsafe.entity.AbstractCMSEntity; import com.ebay.cloud.cms.typsafe.entity.GenericCMSEntity; import com.ebay.cloud.cms.typsafe.entity.ICMSEntity; import com.ebay.cloud.cms.typsafe.entity.ICMSEntityVisitor; import com.ebay.cloud.cms.typsafe.entity.IGenericEntity; import com.ebay.cloud.cms.typsafe.exception.CMSEntityException; import com.ebay.cloud.cms.typsafe.service.CMSClientConfig; import com.google.common.base.Preconditions; /** * @author liasu * */ @SuppressWarnings("rawtypes") public class CMSEntityMapper implements ICMSEntityVisitor { public static enum ProcessModeEnum { JSON, GENERIC, TYPE_SAFE, DYNAMIC; } private static final Logger logger = LoggerFactory.getLogger(CMSEntityMapper.class); private final Class<? extends ICMSEntity> targetClass; /** * The code generation class that to be used as meta class for field information */ private final Class<? extends ICMSEntity> metaClass; private final CMSClientConfig config; private final ProcessModeEnum mode; private final boolean dirtyOnly; private final ObjectNode rootNode; private ICMSEntity targetEntity; public CMSEntityMapper(ObjectNode rootNode, CMSClientConfig config, Class targetClass, ProcessModeEnum mode, Class metaClass) { this(rootNode, config, targetClass, mode, metaClass, false); } @SuppressWarnings("unchecked") public CMSEntityMapper(ObjectNode rootNode, CMSClientConfig config, Class targetClass, ProcessModeEnum mode, Class metaClass, boolean dirtyOnly) { this.config = config; this.rootNode = rootNode; Preconditions.checkArgument(ICMSEntity.class.isAssignableFrom(targetClass)); Preconditions.checkArgument(ICMSEntity.class.isAssignableFrom(metaClass)); this.targetClass = targetClass; this.mode = mode; this.metaClass = metaClass; this.dirtyOnly = dirtyOnly; // init root entity createRootEntity(rootNode, mode); } private void createRootEntity(ObjectNode rootNode, CMSEntityMapper.ProcessModeEnum mode) { if (this.targetClass == JsonCMSEntity.class) { targetEntity = new JsonCMSEntity(this.metaClass); } else if (mode == CMSEntityMapper.ProcessModeEnum.DYNAMIC && rootNode != null) { targetEntity = inferTargetEntity(); } else { targetEntity = createEntity(this.targetClass); } validateRootEntityType(); initEntity(targetEntity); // set branch for root if (config != null) { targetEntity.set_branch(config.getBranch()); } } private void initEntity(ICMSEntity entity) { if (config != null) { entity.set_repo(config.getRepository()); } } @SuppressWarnings("deprecation") private void validateRootEntityType() { if (mode == ProcessModeEnum.TYPE_SAFE && rootNode != null && rootNode.has("_type")) { String responseType = rootNode.get("_type").getValueAsText(); if (!targetClass.getSimpleName().equals(responseType)) { throw new CMSEntityException(MessageFormat.format( "Got response type {0}, but given class type as {1}, this is possibly a client code issue, please use consistent type!", responseType, targetClass.getSimpleName())); } } } @SuppressWarnings({ "deprecation", "unchecked" }) private ICMSEntity inferTargetEntity() { if (rootNode == null || !rootNode.has("_type")) { throw new CMSEntityException( "Try to create entity based on query result, but can not find the _type hint. Please consider to add _type in your query projection!", null); } String type = (String) rootNode.get("_type").getValueAsText(); Class<?> t = inferFieldClass(type, null, null); Preconditions.checkState(t != GenericCMSEntity.class, MessageFormat.format( "Can not create instance for {0}, might be the code generation need to be updated?", type)); ICMSEntity entity = createEntity((Class<? extends ICMSEntity>) t); initEntity(entity); return entity; } private ICMSEntity createEntity(Class<? extends ICMSEntity> target) { ICMSEntity entity = null; try { entity = target.newInstance(); } catch (Exception e) { throw new CMSEntityException(MessageFormat.format("can not create instance for class {0}", target.getSimpleName()), e); } return entity; } @Override public void processAttribute(ICMSEntity currentEntity, String fieldName) { if (dirtyOnly && currentEntity.isDirtyCheckEnabled() && !currentEntity.isDirty(fieldName)) { return; } Object fieldValue = currentEntity.getFieldValue(fieldName); targetEntity.setFieldValue(fieldName, fieldValue); } @Override @SuppressWarnings("unchecked") public void processReference(ICMSEntity currentEntity, String fieldName) { if (dirtyOnly && currentEntity.isDirtyCheckEnabled() && !currentEntity.isDirty(fieldName)) { return; } Object fieldValue = currentEntity.getFieldValue(fieldName); ICMSEntity oldTarget = targetEntity; boolean isList = fieldValue instanceof List; if (targetClass == JsonCMSEntity.class) { // from java object to json if (isList) { List<AbstractCMSEntity> refEntities = (List<AbstractCMSEntity>) fieldValue; for (AbstractCMSEntity entity : refEntities) { ICMSEntity newEntity = new JsonCMSEntity(ClassUtil.getFieldClass(entity.getClass(), fieldName)); processJsonList(fieldName, oldTarget, newEntity, entity); } } else { AbstractCMSEntity entity = (AbstractCMSEntity) fieldValue; ICMSEntity newEntity = new JsonCMSEntity(ClassUtil.getFieldClass(entity.getClass(), fieldName)); processJsonSingle(fieldName, oldTarget, newEntity, entity); } } else { // convert from json to java object if (isList) { List<JsonCMSEntity> refJsonEntities = (List<JsonCMSEntity>) fieldValue; for (JsonCMSEntity jsonEntity : refJsonEntities) { ICMSEntity newEntity = createReferenceEntity(targetEntity, fieldName, jsonEntity); processJsonList(fieldName, oldTarget, newEntity, jsonEntity); } } else { JsonCMSEntity jsonEntity = (JsonCMSEntity) fieldValue; ICMSEntity newEntity = createReferenceEntity(targetEntity, fieldName, jsonEntity); processJsonSingle(fieldName, oldTarget, newEntity, jsonEntity); } } } private void processJsonSingle(String fieldName, ICMSEntity oldTarget, ICMSEntity newEntity, ICMSEntity subEntity) { targetEntity = newEntity; oldTarget.setFieldValue(fieldName, targetEntity); subEntity.traverse(this); targetEntity = oldTarget; } private void processJsonList(String fieldName, ICMSEntity oldTarget, ICMSEntity newEntity, ICMSEntity subEntity) { targetEntity = newEntity; oldTarget.addFieldValue(fieldName, targetEntity); subEntity.traverse(this); targetEntity = oldTarget; } private ICMSEntity createReferenceEntity(IGenericEntity parentEntity, String fieldName, JsonCMSEntity jsonNode) { if (mode == CMSEntityMapper.ProcessModeEnum.GENERIC) { ICMSEntity entity = createEntity(targetClass); initEntity(entity); return entity; } Class clz = inferFieldClass(jsonNode.get_type(), parentEntity.getClass(), fieldName); AbstractCMSEntity entity = null; try { entity = (AbstractCMSEntity) clz.newInstance(); } catch (Exception e) { throw new CMSEntityException( MessageFormat.format( "convert from json to java object failed, not able to create entity with name:{0}. Please make sure you are operating entity on the correct repository;" + "otherwise, this possibly means that metadata is changed on cms service, so the model classes are out of date. Consider use generic api or re-generate model classes.", clz.getCanonicalName()), e); } entity.set_repo(config.getRepository()); // entity.set_branch(config.getBranch()); return entity; } /** * Prefer the json type return from cms service if any, otherwise based on the code generation method signature * @param jsonSpecifiedType * @param parentEntity * @param fieldName * @return - full */ private Class<?> inferFieldClass(String jsonSpecifiedType, Class<?> parentEntityClass, String fieldName) { Class<?> clz; StringBuilder builder = new StringBuilder(config.getClientPackagePrefix()); if (jsonSpecifiedType != null) { builder.append(".").append(jsonSpecifiedType); try { clz = Class.forName(builder.toString()); } catch (Exception e) { logger.error("can not load class with canonical path: " + builder.toString()); clz = GenericCMSEntity.class; } } else { clz = ClassUtil.getGetterReturnType(parentEntityClass, fieldName); if (clz == null) { logger.error("Can not find field {0} in metaclass {1}, will use generic cms entity instead") ; clz = GenericCMSEntity.class; } } return clz; } public Object getTargetEntity() { return targetEntity; } }