/**
*
*/
package com.ebay.cloud.cms.typsafe.exporter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ebay.cloud.cms.typsafe.exception.CMSModelException;
import com.ebay.cloud.cms.typsafe.metadata.model.MetadataManager;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
/**
* @author gowang
* @author liasu
*
*/
@SuppressWarnings("deprecation")
public class EntityGenerator {
private static final Logger logger = LoggerFactory.getLogger(EntityGenerator.class);
private static final Map<Character, String> CHAR_NAMES;
private static final Configuration configuration;
static {
configuration = new Configuration();
configuration.setClassForTemplateLoading(EntityGenerator.class, "/");
configuration.setObjectWrapper(new DefaultObjectWrapper());
// Set the first letter capitalized
configuration.setSharedVariable("upperFC", new UpperFirstCharacter());
CHAR_NAMES = new HashMap<Character, String>();
CHAR_NAMES.put('+', "_PLUS_");
CHAR_NAMES.put('-', "_MINUS_");
}
private final String prefix;
private final String metaJson;
private final JsonNode metaNode;
private boolean hasList = false;
private boolean hasDate = false;
private boolean hasJsonObject = false;
private Map<String, Object> parameterMap;
private EntityInformation output;
private MetadataManager metaManager;
public enum PrimitiveType {
Integer("integer", "Integer"),
Long("long", "Long"),
String("string", "String"),
Json("json", "JsonNode"),
Double("double", "Double"),
Boolean("boolean", "Boolean"),
Date("date", "Date"); // java.util
// enum
// relationship
private final String metaTypeName;
private final String javaTypeName;
public String getMetaTypeName() {
return metaTypeName;
}
public String getJavaTypeName() {
return javaTypeName;
}
private PrimitiveType(String metaName, String javaName) {
this.metaTypeName = metaName;
this.javaTypeName = javaName;
}
public static PrimitiveType fromMetaTypeName(String metaName) {
for (PrimitiveType mapping : PrimitiveType.values()) {
if (mapping.getMetaTypeName().equals(metaName)) {
return mapping;
}
}
return null;
}
}
public static class EntityInformation {
final String packageName;
final String className;
final String content;
EntityInformation(String pack, String clzName, String clzContent) {
packageName = pack;
className = clzName;
content = clzContent;
}
}
EntityGenerator(String meta, String pkgPrefix, MetadataManager metaManager) {
this.metaJson = meta;
this.metaNode = parseJson();
this.parameterMap = new HashMap<String, Object>();
this.prefix = pkgPrefix;
this.metaManager = metaManager;
}
EntityGenerator(JsonNode metaNode, String pkgPrefix, MetadataManager metaManager) {
this.metaNode = metaNode;
this.metaJson = metaNode.toString();
this.parameterMap = new HashMap<String, Object>();
this.prefix = pkgPrefix;
this.metaManager = metaManager;
}
private final JsonNode parseJson() {
ObjectMapper om = new ObjectMapper();
try {
return om.readTree(metaJson);
} catch (Exception e) {
throw new CMSModelException(String.format("fail to parse meta json: %s", metaJson), e);
}
}
public void build() {
// prepare parameters' map
processMetadata(metaNode);
// apply to freemarker template
String content = analysisTemplate();
output = new EntityInformation((String) parameterMap.get("entityPackage"),
(String) parameterMap.get("className"), content);
}
public EntityInformation getEntity() {
return output;
}
private void processMetadata(JsonNode rootNode) {
// 1. entity name and package name; inheritance information
JsonNode entityName = rootNode.get("name");
JsonNode repoName = rootNode.get("repository");
if (repoName == null) {
parameterMap.put("repoName", "");
} else {
parameterMap.put("repoName", repoName.getValueAsText());
}
parameterMap.put("jacksonPackage", "org.codehaus.jackson.annotate.JsonIgnore");
// JIRA-2034 : don't need to add repository name prefix, let user fully specify the package
parameterMap.put("entityPackage", prefix /*+ "." + convertToJavaIdentifier(repoName.asText())*/);
if (rootNode.get("parent") != null && !rootNode.get("parent").isNull()) {
parameterMap.put("inherit", rootNode.get("parent").getValueAsText());
} else {
parameterMap.put("inherit", "GenericCMSEntity");
parameterMap.put("inheritParent", "com.ebay.cloud.cms.typsafe.entity");
}
parameterMap.put("className", entityName.getValueAsText());
// 2. entity fields
JsonNode fieldNode = rootNode.get("fields");
Iterator<String> fieldsItetator = fieldNode.getFieldNames();
List<Object> fieldProps = new ArrayList<Object>();
while (fieldsItetator.hasNext()) {
String fieldName = fieldsItetator.next();
String metaTypeName = fieldNode.get(fieldName).get("dataType").getValueAsText();
JsonNode cardinalityNode = fieldNode.get(fieldName).get("cardinality");
// global setting of list
hasList |= (cardinalityNode != null && cardinalityNode.getValueAsText().equals("Many"));
PrimitiveType typeMapping = PrimitiveType.fromMetaTypeName(metaTypeName);
if (typeMapping != null) {
fieldProps.add(processPrimitveField(fieldNode, fieldName, typeMapping));
} else if (metaTypeName.equals("enumeration")) {
fieldProps.add(processEnumField(fieldNode, fieldName));
} else if (metaTypeName.equals("relationship")) {
fieldProps.add(processReferenceField(fieldNode, fieldName));
} else {
throw new CMSModelException("Unknown metafield type" + metaTypeName, null);
}
}
// additional global setting based on parse result
if (hasList) {
parameterMap.put("listPackage", "java.util.List");
} else {
parameterMap.put("listPackage", "");
}
if (hasDate) {
parameterMap.put("datePackage", "java.util.Date");
} else {
parameterMap.put("datePackage", "");
}
if (hasJsonObject) {
parameterMap.put("jsonPackage", "org.codehaus.jackson.JsonNode");
} else {
parameterMap.put("jsonPackage", "");
}
parameterMap.put("properties", fieldProps);
}
private Map<String, Object> processEnumField(JsonNode fieldNode, String fieldName) {
Map<String, Object> enumProp = new HashMap<String, Object>();
enumProp.put("propType", "enumeration");
enumProp.put("propName", fieldName);
JsonNode cardinalityNode = fieldNode.get(fieldName).get("cardinality");
enumProp.put("propCardinality", cardinalityNode == null ? "One" : cardinalityNode.getValueAsText());
int enumLength = fieldNode.get(fieldName).get("enumValues").size();
List<Object> enumList = new ArrayList<Object>();
for (int i = 0; i < enumLength; i++) {
Map<String, Object> enumMap = new HashMap<String, Object>();
enumMap.put("enumElementValue", fieldNode.get(fieldName).get("enumValues").get(i).getValueAsText());
String enumVal = convertToJavaIdentifier(fieldNode.get(fieldName).get("enumValues").get(i).getValueAsText());
enumMap.put("enumElementName", enumVal);
enumList.add(enumMap);
}
enumProp.put("enumLength", enumLength);
enumProp.put("enumList", enumList);
return enumProp;
}
private Map<String, Object> processPrimitveField(JsonNode fieldNode, String fieldName, PrimitiveType mapping) {
Map<String, Object> primitiveProp = new HashMap<String, Object>();
primitiveProp.put("propType", mapping.getJavaTypeName());
primitiveProp.put("propName", fieldName);
// json type doesn't care the cardinality option
JsonNode cardinalityNode = fieldNode.get(fieldName).get("cardinality");
if (mapping != PrimitiveType.Json && cardinalityNode != null) {
primitiveProp.put("propCardinality", cardinalityNode.getValueAsText());
} else {
primitiveProp.put("propCardinality", "One");
}
if (mapping == PrimitiveType.Date) {
hasDate = true;
} else if (mapping == PrimitiveType.Json) {
hasJsonObject = true;
}
return primitiveProp;
}
private Map<String, Object> processReferenceField(JsonNode fieldNode, String fieldName) {
JsonNode currentFieldNode = fieldNode.get(fieldName);
Map<String, Object> relationProp = new HashMap<String, Object>();
relationProp.put("propType", "relationship");
relationProp.put("propName", fieldName);
JsonNode cardinalityNode = currentFieldNode.get("cardinality");
relationProp.put("propCardinality", cardinalityNode == null ? "One" : cardinalityNode.getValueAsText());
String refTypeName;
JsonNode relationNode = currentFieldNode.get("relationType");
if (relationNode != null && "CrossRepository".equals(relationNode.getValueAsText())) {
// for cross repository, generate a generic entity in the interface
refTypeName = "GenericCMSEntity";
} else {
String refDataType = currentFieldNode.get("refDataType").getValueAsText();
refTypeName = metaManager.resolveEmbedReferenceName(refDataType);
}
relationProp.put("propRefName", refTypeName);
return relationProp;
}
private String convertToJavaIdentifier(String name) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
char charAt = name.charAt(i);
if (!Character.isJavaIdentifierPart(charAt)) {
// special handling : their would be "UTC+8" and "UTC-8" in enum value for timezone. These two values
// would be converted the same enum, which causes a compilation error. To resolve the error, a "100 percent correct" solution would be converted to
// ascii code. But that hurts the readability of the generated code. Would be define some dictionary of (special char -> english abbr).
if (CHAR_NAMES.containsKey(charAt)) {
sb.append(CHAR_NAMES.get(charAt));
} else {
sb.append('_');
}
} else {
sb.append(charAt);
}
}
return sb.toString();
}
private String analysisTemplate() {
Writer outputWriter = null;
try {
Template template = configuration.getTemplate("CMSEntityTemplate.ftl", "utf8");
outputWriter = new StringWriter();
template.process(parameterMap, outputWriter);
outputWriter.flush();
return outputWriter.toString();
} catch (Exception e) {
throw new CMSModelException("error when analysis template", e);
} finally {
// close resource like files, streams...
if (outputWriter != null) {
try {
outputWriter.close();
} catch (Exception e) {
logger.error("failed to close the string writer when analysis template", e);
}
}
}
}
}