/**
* ***************************************************************************
* Copyright (c) 2010 Qcadoo Limited
* Project: Qcadoo Framework
* Version: 1.4
*
* This file is part of Qcadoo.
*
* Qcadoo is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation; either version 3 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* ***************************************************************************
*/
package com.qcadoo.model.internal.classconverter;
import com.qcadoo.model.constants.VersionableConstants;
import com.qcadoo.model.internal.AbstractModelXmlConverter;
import com.qcadoo.model.internal.api.ModelXmlToClassConverter;
import com.qcadoo.model.internal.utils.ClassNameUtils;
import javassist.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.*;
@Component
public final class ModelXmlToClassConverterImpl extends AbstractModelXmlConverter implements ModelXmlToClassConverter,
BeanClassLoaderAware {
private static final String L_FAILED_TO_COMPILE_CLASS = "Failed to compile class ";
private static final String L_NAME = "name";
private static final String L_ERROR_WHILE_PARSING_MODEL_XML = "Error while parsing model.xml: ";
private static final Logger LOG = LoggerFactory.getLogger(ModelXmlToClassConverterImpl.class);
private final ClassPool classPool = ClassPool.getDefault();
private ClassLoader classLoader;
public ModelXmlToClassConverterImpl() {
super();
classPool.appendClassPath(new ClassClassPath(org.hibernate.collection.PersistentSet.class));
classPool.appendClassPath(new ClassClassPath(com.qcadoo.model.internal.classconverter.QcadooModelBean.class));
}
@Override
public void setBeanClassLoader(final ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
@SuppressWarnings("deprecation")
public Collection<Class<?>> convert(final Resource... resources) {
Map<String, CtClass> ctClasses = new HashMap<String, CtClass>();
Map<String, Class<?>> existingClasses = new HashMap<String, Class<?>>();
for (Resource resource : resources) {
if (resource.isReadable()) {
LOG.info("Getting existing classes from " + resource);
try {
existingClasses.putAll(findExistingClasses(resource.getInputStream()));
} catch (XMLStreamException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
} catch (IOException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
}
}
}
for (Resource resource : resources) {
if (resource.isReadable()) {
LOG.info("Creating classes from " + resource);
try {
ctClasses.putAll(createClasses(existingClasses, resource.getInputStream()));
} catch (XMLStreamException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
} catch (IOException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
}
}
}
for (Resource resource : resources) {
if (resource.isReadable()) {
LOG.info("Defining classes from " + resource + " to classes");
try {
defineClasses(ctClasses, resource.getInputStream());
} catch (XMLStreamException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
} catch (ModelXmlCompilingException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
} catch (IOException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
} catch (NotFoundException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
}
}
}
List<Class<?>> classes = new ArrayList<Class<?>>();
for (CtClass ctClass : ctClasses.values()) {
try {
classes.add(ctClass.toClass(classLoader));
} catch (CannotCompileException e) {
throw new IllegalStateException(L_ERROR_WHILE_PARSING_MODEL_XML + e.getMessage(), e);
}
}
classes.addAll(existingClasses.values());
return classes;
}
private Map<String, Class<?>> findExistingClasses(final InputStream stream) throws XMLStreamException {
XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(stream);
Map<String, Class<?>> existingClasses = new HashMap<String, Class<?>>();
while (reader.hasNext() && reader.next() > 0) {
if (isTagStarted(reader, TAG_MODEL)) {
String pluginIdentifier = getPluginIdentifier(reader);
String modelName = getStringAttribute(reader, L_NAME);
String className = ClassNameUtils.getFullyQualifiedClassName(pluginIdentifier, modelName);
try {
existingClasses.put(className, classLoader.loadClass(className));
LOG.info("Class " + className + " already exists, skipping");
} catch (ClassNotFoundException e) {
LOG.info("Class " + className + " not found, will be generated");
}
break;
}
}
reader.close();
return existingClasses;
}
private Map<String, CtClass> createClasses(final Map<String, Class<?>> existingClasses, final InputStream stream)
throws XMLStreamException {
XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(stream);
Map<String, CtClass> ctClasses = new HashMap<String, CtClass>();
while (reader.hasNext() && reader.next() > 0) {
if (isTagStarted(reader, TAG_MODEL)) {
String pluginIdentifier = getPluginIdentifier(reader);
String modelName = getStringAttribute(reader, L_NAME);
String className = ClassNameUtils.getFullyQualifiedClassName(pluginIdentifier, modelName);
if (existingClasses.containsKey(className)) {
LOG.info("Class " + className + " already exists, skipping");
} else {
LOG.info("Creating class " + className);
ctClasses.put(className, classPool.makeClass(className));
}
break;
}
}
reader.close();
return ctClasses;
}
private void defineClasses(final Map<String, CtClass> ctClasses, final InputStream stream) throws XMLStreamException,
ModelXmlCompilingException, NotFoundException {
XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(stream);
while (reader.hasNext() && reader.next() > 0) {
if (isTagStarted(reader, TAG_MODEL)) {
String pluginIdentifier = getPluginIdentifier(reader);
String modelName = getStringAttribute(reader, L_NAME);
String className = ClassNameUtils.getFullyQualifiedClassName(pluginIdentifier, modelName);
if (ctClasses.containsKey(className)) {
parse(reader, ctClasses.get(className), pluginIdentifier);
}
}
}
reader.close();
}
private void parse(final XMLStreamReader reader, final CtClass ctClass, final String pluginIdentifier)
throws XMLStreamException, ModelXmlCompilingException, NotFoundException {
LOG.info("Defining class " + ctClass.getName());
ctClass.addInterface(classPool.get(QcadooModelBean.class.getName()));
createField(ctClass, "id", Long.class.getCanonicalName());
List<String> fields = new ArrayList<String>();
fields.add("id");
boolean activable = getBooleanAttribute(reader, "activable", false);
if (getBooleanAttribute(reader, "auditable", false)) {
createField(ctClass, "createDate", Date.class.getCanonicalName());
createField(ctClass, "updateDate", Date.class.getCanonicalName());
createField(ctClass, "createUser", String.class.getCanonicalName());
createField(ctClass, "updateUser", String.class.getCanonicalName());
}
if (getBooleanAttribute(reader, VersionableConstants.VERSIONABLE_ATTRIBUTE_NAME, false)) {
createField(ctClass, VersionableConstants.VERSION_FIELD_NAME, Long.class.getCanonicalName());
}
while (reader.hasNext() && reader.next() > 0) {
if (isTagEnded(reader, TAG_MODEL)) {
break;
}
if (TAG_FIELDS.equals(getTagStarted(reader))) {
while (reader.hasNext() && reader.next() > 0) {
if (isTagEnded(reader, TAG_FIELDS)) {
break;
}
String tag = getTagStarted(reader);
if (tag == null) {
continue;
}
parseField(reader, pluginIdentifier, ctClass, tag, fields);
}
break;
}
}
if (activable && !fields.contains("active")) {
createField(ctClass, "active", Boolean.class.getCanonicalName());
}
buildToString(ctClass, fields);
buildHashCode(ctClass, fields);
buildEquals(ctClass, fields);
}
private void buildToString(final CtClass ctClass, final List<String> fields) throws ModelXmlCompilingException {
try {
StringBuilder sb = new StringBuilder();
sb.append("return new java.lang.StringBuilder().append(\"" + ctClass.getName() + "[\").");
boolean first = true;
for (String field : fields) {
if (first) {
first = false;
} else {
sb.append("append(\",\").");
}
sb.append("append(\"" + field + "=\").append(get" + StringUtils.capitalize(field) + "()).");
}
sb.append("append(\"]\").toString();");
ctClass.addMethod(CtNewMethod.make("public String toString() { " + sb.toString() + " }", ctClass));
} catch (CannotCompileException e) {
throw new ModelXmlCompilingException(L_FAILED_TO_COMPILE_CLASS + ctClass.getName(), e);
}
}
private void buildHashCode(final CtClass ctClass, final List<String> fields) throws ModelXmlCompilingException {
buildHashCode(ctClass, fields, false);
buildHashCode(ctClass, fields, true);
}
private void buildHashCode(final CtClass ctClass, final List<String> fields, final boolean deep)
throws ModelXmlCompilingException {
try {
String methodName = null;
StringBuilder methodCode = new StringBuilder();
methodCode.append("final int prime = 31;");
methodCode.append("int result = 1;");
if (deep) {
methodName = "hashCode";
getFieldsDeepHashCode(methodCode, fields);
} else {
methodName = "flatHashCode";
getFieldsFlatHashCode(methodCode, fields);
}
methodCode.append("return result;");
ctClass.addMethod(CtNewMethod.make("public int " + methodName + "() { " + methodCode.toString() + " }", ctClass));
} catch (CannotCompileException e) {
throw new ModelXmlCompilingException(L_FAILED_TO_COMPILE_CLASS + ctClass.getName(), e);
}
}
private String getGetterForField(final String fieldName) {
return "get" + StringUtils.capitalize(fieldName) + "()";
}
private void getFieldsDeepHashCode(final StringBuilder sb, final List<String> fieldNames) {
for (String fieldName : fieldNames) {
sb.append("final Object fieldValue = " + getGetterForField(fieldName) + ";");
sb.append("if (fieldValue instanceof com.qcadoo.model.internal.classconverter.QcadooModelBean) {");
sb.append(" result += prime * result + ((com.qcadoo.model.internal.classconverter.QcadooModelBean) fieldValue).flatHashCode();");
sb.append("} else if (fieldValue != null && !(fieldValue instanceof org.hibernate.collection.PersistentSet)) {");
sb.append(" if (fieldValue instanceof Iterable) {");
sb.append(" for (java.util.Iterator iter = ((Iterable) fieldValue).iterator(); iter.hasNext(); ) {");
sb.append(" Object collectionElement = iter.next();");
sb.append(" if (collectionElement == null) { continue; }");
sb.append(" result += prime * result + "
+ "(collectionElement instanceof com.qcadoo.model.internal.classconverter.QcadooModelBean ? "
+ "((com.qcadoo.model.internal.classconverter.QcadooModelBean) collectionElement).flatHashCode() : "
+ "collectionElement.hashCode());");
sb.append(" }");
sb.append(" } else {");
sb.append(" result += prime * result + fieldValue.hashCode();");
sb.append(" }");
sb.append("}");
}
}
private void getFieldsFlatHashCode(final StringBuilder sb, final List<String> fieldNames) {
for (String fieldName : fieldNames) {
sb.append("final Object fieldValue = " + getGetterForField(fieldName) + ";");
sb.append("if (fieldValue != null && !(fieldValue instanceof org.hibernate.collection.PersistentSet)) {");
sb.append(" if (fieldValue instanceof com.qcadoo.model.internal.classconverter.QcadooModelBean) {");
sb.append(" Object relatedEntityId = ((com.qcadoo.model.internal.classconverter.QcadooModelBean) fieldValue).getId();");
sb.append(" result += prime * result + (relatedEntityId == null ? 0 : relatedEntityId.hashCode());");
sb.append(" } else if (!(fieldValue instanceof Iterable)) {");
sb.append(" result += prime * result + fieldValue.hashCode();");
sb.append(" }");
sb.append("}");
}
}
private void buildEquals(final CtClass ctClass, final List<String> fields) throws ModelXmlCompilingException {
try {
StringBuilder sb = new StringBuilder();
sb.append("if (obj == null) { return false; }");
sb.append("if (obj == this) { return true; }");
sb.append("if (!(obj instanceof " + ctClass.getName() + ")) { return false; }");
sb.append(ctClass.getName() + " other = (" + ctClass.getName() + ") obj;");
for (String field : fields) {
sb.append("if (get" + StringUtils.capitalize(field) + "() == null) {");
sb.append(" if (other.get" + StringUtils.capitalize(field) + "() != null) { return false; }");
sb.append("} else if (!get" + StringUtils.capitalize(field) + "().equals(other.get"
+ StringUtils.capitalize(field) + "())) { return false; }");
}
sb.append("return true;");
ctClass.addMethod(CtNewMethod.make("public boolean equals(Object obj) { " + sb.toString() + " }", ctClass));
} catch (CannotCompileException e) {
throw new ModelXmlCompilingException(L_FAILED_TO_COMPILE_CLASS + ctClass.getName(), e);
}
}
private void parseField(final XMLStreamReader reader, final String pluginIdentifier, final CtClass ctClass, final String tag,
final List<String> fields) throws XMLStreamException, ModelXmlCompilingException {
FieldsTag modelTag = FieldsTag.valueOf(tag.toUpperCase(Locale.ENGLISH));
if (getBooleanAttribute(reader, "persistent", true) || getStringAttribute(reader, "expression") == null) {
switch (modelTag) {
case PRIORITY:
case INTEGER:
createField(ctClass, getStringAttribute(reader, L_NAME), Integer.class.getCanonicalName());
fields.add(getStringAttribute(reader, L_NAME));
break;
case STRING:
case FILE:
case TEXT:
case ENUM:
case DICTIONARY:
case PASSWORD:
createField(ctClass, getStringAttribute(reader, L_NAME), String.class.getCanonicalName());
fields.add(getStringAttribute(reader, L_NAME));
break;
case DECIMAL:
createField(ctClass, getStringAttribute(reader, L_NAME), BigDecimal.class.getCanonicalName());
fields.add(getStringAttribute(reader, L_NAME));
break;
case DATETIME:
case DATE:
createField(ctClass, getStringAttribute(reader, L_NAME), Date.class.getCanonicalName());
fields.add(getStringAttribute(reader, L_NAME));
break;
case BOOLEAN:
createField(ctClass, getStringAttribute(reader, L_NAME), Boolean.class.getCanonicalName());
fields.add(getStringAttribute(reader, L_NAME));
break;
case BELONGSTO:
createBelongsField(ctClass, pluginIdentifier, reader);
fields.add(getStringAttribute(reader, L_NAME));
break;
case MANYTOMANY:
createSetField(ctClass, reader);
fields.add(getStringAttribute(reader, L_NAME));
break;
case HASMANY:
case TREE:
createSetField(ctClass, reader);
break;
default:
break;
}
}
while (reader.hasNext() && reader.next() > 0) {
if (isTagEnded(reader, tag)) {
break;
}
}
}
private void createSetField(final CtClass ctClass, final XMLStreamReader reader) throws ModelXmlCompilingException {
createField(ctClass, getStringAttribute(reader, L_NAME), "java.util.Set");
}
private void createBelongsField(final CtClass ctClass, final String pluginIdentifier, final XMLStreamReader reader)
throws ModelXmlCompilingException {
String plugin = getStringAttribute(reader, "plugin");
if (plugin == null) {
plugin = pluginIdentifier;
}
String model = getStringAttribute(reader, "model");
createField(ctClass, getStringAttribute(reader, L_NAME), ClassNameUtils.getFullyQualifiedClassName(plugin, model));
}
private void createField(final CtClass ctClass, final String name, final String clazz) throws ModelXmlCompilingException {
try {
ctClass.addField(CtField.make("private " + clazz + " " + name + ";", ctClass));
ctClass.addMethod(CtNewMethod.make("public " + clazz + " get" + StringUtils.capitalize(name) + "() { return " + name
+ "; }", ctClass));
ctClass.addMethod(CtNewMethod.make("public void set" + StringUtils.capitalize(name) + "(" + clazz + " " + name
+ ") { this." + name + " = " + name + "; }", ctClass));
} catch (CannotCompileException e) {
throw new ModelXmlCompilingException(L_FAILED_TO_COMPILE_CLASS + ctClass.getName(), e);
}
}
}