ch.rasc.extclassgenerator.ModelGenerator.java Source code

Java tutorial

Introduction

Here is the source code for ch.rasc.extclassgenerator.ModelGenerator.java

Source

/**
 * Copyright 2013-2014 Ralph Schaer <ralphschaer@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ch.rasc.extclassgenerator;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.ref.SoftReference;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.util.StringUtils;

import ch.rasc.extclassgenerator.association.AbstractAssociation;
import ch.rasc.extclassgenerator.validation.AbstractValidation;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Generator for creating ExtJS and Touch Model objects (JS code) based on a provided
 * class or {@link ModelBean}.
 */
public abstract class ModelGenerator {

    public static final Charset UTF8_CHARSET = Charset.forName("UTF-8");

    private static final Map<JsCacheKey, SoftReference<String>> jsCache = new ConcurrentHashMap<JsCacheKey, SoftReference<String>>();

    private static final Map<ModelCacheKey, SoftReference<ModelBean>> modelCache = new ConcurrentHashMap<ModelCacheKey, SoftReference<ModelBean>>();

    /**
     * Instrospects the provided class, creates a model object (JS code) and writes it
     * into the response. Creates compressed JS code. Method ignores any validation
     * annotations.
     *
     * @param request the http servlet request
     * @param response the http servlet response
     * @param clazz class that the generator should introspect
     * @param format specifies which code (ExtJS or Touch) the generator should create.
     * @throws IOException
     *
     * @see #writeModel(HttpServletRequest, HttpServletResponse, Class, OutputFormat,
     * boolean)
     */
    public static void writeModel(HttpServletRequest request, HttpServletResponse response, Class<?> clazz,
            OutputFormat format) throws IOException {
        writeModel(request, response, clazz, format, IncludeValidation.NONE, false);
    }

    /**
     * Instrospects the provided class, creates a model object (JS code) and writes it
     * into the response. Method ignores any validation annotations.
     *
     * @param request the http servlet request
     * @param response the http servlet response
     * @param clazz class that the generator should introspect
     * @param format specifies which code (ExtJS or Touch) the generator should create
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @throws IOException
     */
    public static void writeModel(HttpServletRequest request, HttpServletResponse response, Class<?> clazz,
            OutputFormat format, boolean debug) throws IOException {
        writeModel(request, response, clazz, format, IncludeValidation.NONE, debug);
    }

    /**
     * Instrospects the provided class, creates a model object (JS code) and writes it
     * into the response.
     *
     * @param request the http servlet request
     * @param response the http servlet response
     * @param clazz class that the generator should introspect
     * @param format specifies which code (ExtJS or Touch) the generator should create
     * @param includeValidation specifies if any validation configurations should be added
     * to the model code
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @throws IOException
     */
    public static void writeModel(HttpServletRequest request, HttpServletResponse response, Class<?> clazz,
            OutputFormat format, IncludeValidation includeValidation, boolean debug) throws IOException {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setIncludeValidation(includeValidation);
        outputConfig.setOutputFormat(format);
        outputConfig.setDebug(debug);
        ModelBean model = createModel(clazz, outputConfig);
        writeModel(request, response, model, outputConfig);
    }

    public static void writeModel(HttpServletRequest request, HttpServletResponse response, Class<?> clazz,
            OutputConfig outputConfig) throws IOException {
        ModelBean model = createModel(clazz, outputConfig);
        writeModel(request, response, model, outputConfig);
    }

    /**
     * Creates a model object (JS code) based on the provided {@link ModelBean} and writes
     * it into the response. Creates compressed JS code.
     *
     * @param request the http servlet request
     * @param response the http servlet response
     * @param model {@link ModelBean} describing the model to be generated
     * @param format specifies which code (ExtJS or Touch) the generator should create.
     * @throws IOException
     */
    public static void writeModel(HttpServletRequest request, HttpServletResponse response, ModelBean model,
            OutputFormat format) throws IOException {
        writeModel(request, response, model, format, false);
    }

    /**
     * Creates a model object (JS code) based on the provided ModelBean and writes it into
     * the response.
     *
     * @param request the http servlet request
     * @param response the http servlet response
     * @param model {@link ModelBean} describing the model to be generated
     * @param format specifies which code (ExtJS or Touch) the generator should create.
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @throws IOException
     */
    public static void writeModel(HttpServletRequest request, HttpServletResponse response, ModelBean model,
            OutputFormat format, boolean debug) throws IOException {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setDebug(debug);
        outputConfig.setOutputFormat(format);
        writeModel(request, response, model, outputConfig);
    }

    /**
     * Instrospects the provided class and creates a {@link ModelBean} instance. A program
     * could customize this and call
     * {@link #generateJavascript(ModelBean, OutputFormat, boolean)} or
     * {@link #writeModel(HttpServletRequest, HttpServletResponse, ModelBean, OutputFormat)}
     * to create the JS code. Calling this method does not add any validation
     * configuration.
     *
     * @param clazz the model will be created based on this class.
     * @return a instance of {@link ModelBean} that describes the provided class and can
     * be used for Javascript generation.
     */
    public static ModelBean createModel(Class<?> clazz) {
        return createModel(clazz, IncludeValidation.NONE);
    }

    /**
     * Instrospects the provided class and creates a {@link ModelBean} instance. A program
     * could customize this and call
     * {@link #generateJavascript(ModelBean, OutputFormat, boolean)} or
     * {@link #writeModel(HttpServletRequest, HttpServletResponse, ModelBean, OutputFormat)}
     * to create the JS code. Models are being cached. A second call with the same
     * parameters will return the model from the cache.
     *
     * @param clazz the model will be created based on this class.
     * @param includeValidation specifies what validation configuration should be added
     * @return a instance of {@link ModelBean} that describes the provided class and can
     * be used for Javascript generation.
     */
    public static ModelBean createModel(Class<?> clazz, IncludeValidation includeValidation) {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setIncludeValidation(includeValidation);
        return createModel(clazz, outputConfig);
    }

    /**
     * Instrospects the provided class, creates a model object (JS code) and returns it.
     * This method does not add any validation configuration.
     *
     * @param clazz class that the generator should introspect
     * @param format specifies which code (ExtJS or Touch) the generator should create
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @return the generated model object (JS code)
     */
    public static String generateJavascript(Class<?> clazz, OutputFormat format, boolean debug) {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setIncludeValidation(IncludeValidation.NONE);
        outputConfig.setOutputFormat(format);
        outputConfig.setDebug(debug);

        ModelBean model = createModel(clazz, outputConfig);
        return generateJavascript(model, outputConfig);
    }

    public static String generateJavascript(Class<?> clazz, OutputConfig outputConfig) {
        ModelBean model = createModel(clazz, outputConfig);
        return generateJavascript(model, outputConfig);
    }

    /**
     * Instrospects the provided class, creates a model object (JS code) and returns it.
     *
     * @param clazz class that the generator should introspect
     * @param format specifies which code (ExtJS or Touch) the generator should create
     * @param includeValidation specifies what validation configuration should be added to
     * the mode code
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @return the generated model object (JS code)
     */
    public static String generateJavascript(Class<?> clazz, OutputFormat format,
            IncludeValidation includeValidation, boolean debug) {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setIncludeValidation(includeValidation);
        outputConfig.setOutputFormat(format);
        outputConfig.setDebug(debug);

        ModelBean model = createModel(clazz, outputConfig);
        return generateJavascript(model, outputConfig);
    }

    /**
     * Creates JS code based on the provided {@link ModelBean} in the specified
     * {@link OutputFormat}. Code can be generated in pretty or compressed format. The
     * generated code is cached unless debug is true. A second call to this method with
     * the same model name and format will return the code from the cache.
     *
     * @param model generate code based on this {@link ModelBean}
     * @param format specifies which code (ExtJS or Touch) the generator should create
     * @param debug if true the generator creates the output in pretty format, false the
     * output is compressed
     * @return the generated model object (JS code)
     */
    public static String generateJavascript(ModelBean model, OutputFormat format, boolean debug) {
        OutputConfig outputConfig = new OutputConfig();
        outputConfig.setOutputFormat(format);
        outputConfig.setDebug(debug);
        return generateJavascript(model, outputConfig);
    }

    public static void writeModel(HttpServletRequest request, HttpServletResponse response, ModelBean model,
            OutputConfig outputConfig) throws IOException {

        byte[] data = generateJavascript(model, outputConfig).getBytes(UTF8_CHARSET);
        String ifNoneMatch = request.getHeader("If-None-Match");
        String etag = "\"0" + DigestUtils.md5DigestAsHex(data) + "\"";

        if (etag.equals(ifNoneMatch)) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        response.setContentType("application/javascript");
        response.setCharacterEncoding(UTF8_CHARSET.name());
        response.setContentLength(data.length);

        response.setHeader("ETag", etag);

        @SuppressWarnings("resource")
        ServletOutputStream out = response.getOutputStream();
        out.write(data);
        out.flush();

    }

    public static ModelBean createModel(final Class<?> clazz, final OutputConfig outputConfig) {

        Assert.notNull(clazz, "clazz must not be null");
        Assert.notNull(outputConfig.getIncludeValidation(), "includeValidation must not be null");

        ModelCacheKey key = new ModelCacheKey(clazz.getName(), outputConfig);
        SoftReference<ModelBean> modelReference = modelCache.get(key);
        if (modelReference != null && modelReference.get() != null) {
            return modelReference.get();
        }

        Model modelAnnotation = clazz.getAnnotation(Model.class);

        final ModelBean model = new ModelBean();

        if (modelAnnotation != null && StringUtils.hasText(modelAnnotation.value())) {
            model.setName(modelAnnotation.value());
        } else {
            model.setName(clazz.getName());
        }

        if (modelAnnotation != null) {
            model.setAutodetectTypes(modelAnnotation.autodetectTypes());
        }

        if (modelAnnotation != null) {
            model.setExtend(modelAnnotation.extend());
            model.setIdProperty(modelAnnotation.idProperty());
            model.setVersionProperty(trimToNull(modelAnnotation.versionProperty()));
            model.setPaging(modelAnnotation.paging());
            model.setDisablePagingParameters(modelAnnotation.disablePagingParameters());
            model.setCreateMethod(trimToNull(modelAnnotation.createMethod()));
            model.setReadMethod(trimToNull(modelAnnotation.readMethod()));
            model.setUpdateMethod(trimToNull(modelAnnotation.updateMethod()));
            model.setDestroyMethod(trimToNull(modelAnnotation.destroyMethod()));
            model.setMessageProperty(trimToNull(modelAnnotation.messageProperty()));
            model.setWriter(trimToNull(modelAnnotation.writer()));
            model.setReader(trimToNull(modelAnnotation.reader()));
            model.setSuccessProperty(trimToNull(modelAnnotation.successProperty()));
            model.setTotalProperty(trimToNull(modelAnnotation.totalProperty()));
            model.setRootProperty(trimToNull(modelAnnotation.rootProperty()));
            model.setWriteAllFields(modelAnnotation.writeAllFields());
            model.setIdentifier(trimToNull(modelAnnotation.identifier()));
            String clientIdProperty = trimToNull(modelAnnotation.clientIdProperty());
            if (StringUtils.hasText(clientIdProperty)) {
                model.setClientIdProperty(clientIdProperty);
                model.setClientIdPropertyAddToWriter(true);
            } else {
                model.setClientIdProperty(null);
                model.setClientIdPropertyAddToWriter(false);
            }
        }

        final Set<String> hasReadMethod = new HashSet<String>();

        BeanInfo bi;
        try {
            bi = Introspector.getBeanInfo(clazz);
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }

        for (PropertyDescriptor pd : bi.getPropertyDescriptors()) {
            if (pd.getReadMethod() != null && pd.getReadMethod().getAnnotation(JsonIgnore.class) == null) {
                hasReadMethod.add(pd.getName());
            }
        }

        if (clazz.isInterface()) {
            final List<Method> methods = new ArrayList<Method>();

            ReflectionUtils.doWithMethods(clazz, new MethodCallback() {
                @Override
                public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
                    methods.add(method);
                }
            });

            Collections.sort(methods, new Comparator<Method>() {
                @Override
                public int compare(Method o1, Method o2) {
                    return o1.getName().compareTo(o2.getName());
                }
            });

            for (Method method : methods) {
                createModelBean(model, method, outputConfig);
            }
        } else {

            final Set<String> fields = new HashSet<String>();

            Set<ModelField> modelFieldsOnType = AnnotationUtils.getRepeatableAnnotation(clazz, ModelFields.class,
                    ModelField.class);
            for (ModelField modelField : modelFieldsOnType) {
                if (StringUtils.hasText(modelField.value())) {
                    ModelFieldBean modelFieldBean;

                    if (StringUtils.hasText(modelField.customType())) {
                        modelFieldBean = new ModelFieldBean(modelField.value(), modelField.customType());
                    } else {
                        modelFieldBean = new ModelFieldBean(modelField.value(), modelField.type());
                    }

                    updateModelFieldBean(modelFieldBean, modelField);
                    model.addField(modelFieldBean);
                }
            }

            Set<ModelAssociation> modelAssociationsOnType = AnnotationUtils.getRepeatableAnnotation(clazz,
                    ModelAssociations.class, ModelAssociation.class);
            for (ModelAssociation modelAssociationAnnotation : modelAssociationsOnType) {
                AbstractAssociation modelAssociation = AbstractAssociation
                        .createAssociation(modelAssociationAnnotation);
                if (modelAssociation != null) {
                    model.addAssociation(modelAssociation);
                }
            }

            Set<ModelValidation> modelValidationsOnType = AnnotationUtils.getRepeatableAnnotation(clazz,
                    ModelValidations.class, ModelValidation.class);
            for (ModelValidation modelValidationAnnotation : modelValidationsOnType) {
                AbstractValidation modelValidation = AbstractValidation.createValidation(
                        modelValidationAnnotation.propertyName(), modelValidationAnnotation,
                        outputConfig.getIncludeValidation());
                if (modelValidation != null) {
                    model.addValidation(modelValidation);
                }
            }

            ReflectionUtils.doWithFields(clazz, new FieldCallback() {

                @Override
                public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                    if (!fields.contains(field.getName()) && (field.getAnnotation(ModelField.class) != null
                            || field.getAnnotation(ModelAssociation.class) != null
                            || (Modifier.isPublic(field.getModifiers()) || hasReadMethod.contains(field.getName()))
                                    && field.getAnnotation(JsonIgnore.class) == null)) {

                        // ignore superclass declarations of fields already
                        // found in a subclass
                        fields.add(field.getName());
                        createModelBean(model, field, outputConfig);

                    }
                }

            });
        }

        modelCache.put(key, new SoftReference<ModelBean>(model));
        return model;
    }

    private static void createModelBean(ModelBean model, AccessibleObject accessibleObject,
            OutputConfig outputConfig) {
        Class<?> javaType = null;
        String name = null;
        Class<?> declaringClass = null;

        if (accessibleObject instanceof Field) {
            Field field = (Field) accessibleObject;
            javaType = field.getType();
            name = field.getName();
            declaringClass = field.getDeclaringClass();
        } else if (accessibleObject instanceof Method) {
            Method method = (Method) accessibleObject;

            javaType = method.getReturnType();
            if (javaType.equals(Void.TYPE)) {
                return;
            }

            if (method.getName().startsWith("get")) {
                name = StringUtils.uncapitalize(method.getName().substring(3));
            } else if (method.getName().startsWith("is")) {
                name = StringUtils.uncapitalize(method.getName().substring(2));
            } else {
                name = method.getName();
            }

            declaringClass = method.getDeclaringClass();
        }

        ModelType modelType = null;
        if (model.isAutodetectTypes()) {
            for (ModelType mt : ModelType.values()) {
                if (mt.supports(javaType)) {
                    modelType = mt;
                    break;
                }
            }
        } else {
            modelType = ModelType.AUTO;
        }

        ModelFieldBean modelFieldBean = null;

        ModelField modelFieldAnnotation = accessibleObject.getAnnotation(ModelField.class);
        if (modelFieldAnnotation != null) {

            if (StringUtils.hasText(modelFieldAnnotation.value())) {
                name = modelFieldAnnotation.value();
            }

            if (StringUtils.hasText(modelFieldAnnotation.customType())) {
                modelFieldBean = new ModelFieldBean(name, modelFieldAnnotation.customType());
            } else {
                ModelType type = null;
                if (modelFieldAnnotation.type() != ModelType.NOT_SPECIFIED) {
                    type = modelFieldAnnotation.type();
                } else {
                    type = modelType;
                }

                modelFieldBean = new ModelFieldBean(name, type);
            }

            updateModelFieldBean(modelFieldBean, modelFieldAnnotation);
            model.addField(modelFieldBean);
        } else {
            if (modelType != null) {
                modelFieldBean = new ModelFieldBean(name, modelType);
                model.addField(modelFieldBean);
            }
        }

        ModelId modelIdAnnotation = accessibleObject.getAnnotation(ModelId.class);
        if (modelIdAnnotation != null) {
            model.setIdProperty(name);
        }

        ModelClientId modelClientId = accessibleObject.getAnnotation(ModelClientId.class);
        if (modelClientId != null) {
            model.setClientIdProperty(name);
            model.setClientIdPropertyAddToWriter(modelClientId.configureWriter());
        }

        ModelVersion modelVersion = accessibleObject.getAnnotation(ModelVersion.class);
        if (modelVersion != null) {
            model.setVersionProperty(name);
        }

        ModelAssociation modelAssociationAnnotation = accessibleObject.getAnnotation(ModelAssociation.class);
        if (modelAssociationAnnotation != null) {
            model.addAssociation(AbstractAssociation.createAssociation(modelAssociationAnnotation, model, javaType,
                    declaringClass, name));
        }

        if (modelFieldBean != null && outputConfig.getIncludeValidation() != IncludeValidation.NONE) {

            Set<ModelValidation> modelValidationAnnotations = AnnotationUtils
                    .getRepeatableAnnotation(accessibleObject, ModelValidations.class, ModelValidation.class);
            if (!modelValidationAnnotations.isEmpty()) {
                for (ModelValidation modelValidationAnnotation : modelValidationAnnotations) {
                    AbstractValidation modelValidation = AbstractValidation.createValidation(name,
                            modelValidationAnnotation, outputConfig.getIncludeValidation());
                    if (modelValidation != null) {
                        model.addValidation(modelValidation);
                    }
                }
            } else {
                Annotation[] fieldAnnotations = accessibleObject.getAnnotations();

                for (Annotation fieldAnnotation : fieldAnnotations) {
                    AbstractValidation.addValidationToModel(model, modelFieldBean, fieldAnnotation,
                            outputConfig.getIncludeValidation());
                }

                if (accessibleObject instanceof Field) {
                    PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(declaringClass, name);
                    if (pd != null && pd.getReadMethod() != null) {
                        for (Annotation readMethodAnnotation : pd.getReadMethod().getAnnotations()) {
                            AbstractValidation.addValidationToModel(model, modelFieldBean, readMethodAnnotation,
                                    outputConfig.getIncludeValidation());
                        }
                    }
                }
            }
        }

    }

    private static void updateModelFieldBean(ModelFieldBean modelFieldBean, ModelField modelFieldAnnotation) {

        ModelType type = modelFieldBean.getModelType();

        if (StringUtils.hasText(modelFieldAnnotation.dateFormat()) && type == ModelType.DATE) {
            modelFieldBean.setDateFormat(modelFieldAnnotation.dateFormat());
        }

        String defaultValue = modelFieldAnnotation.defaultValue();
        if (StringUtils.hasText(defaultValue)) {
            if (ModelField.DEFAULTVALUE_UNDEFINED.equals(defaultValue)) {
                modelFieldBean.setDefaultValue(ModelField.DEFAULTVALUE_UNDEFINED);
            } else {
                if (type == ModelType.BOOLEAN) {
                    modelFieldBean.setDefaultValue(Boolean.valueOf(defaultValue));
                } else if (type == ModelType.INTEGER) {
                    modelFieldBean.setDefaultValue(Long.valueOf(defaultValue));
                } else if (type == ModelType.FLOAT || type == ModelType.NUMBER) {
                    modelFieldBean.setDefaultValue(Double.valueOf(defaultValue));
                } else {
                    modelFieldBean.setDefaultValue("\"" + defaultValue + "\"");
                }
            }
        }

        if ((modelFieldAnnotation.useNull() || modelFieldAnnotation.allowNull())
                && (type == ModelType.INTEGER || type == ModelType.FLOAT || type == ModelType.NUMBER
                        || type == ModelType.STRING || type == ModelType.BOOLEAN)) {
            modelFieldBean.setAllowNull(Boolean.TRUE);
        }

        if (!modelFieldAnnotation.allowBlank()) {
            modelFieldBean.setAllowBlank(Boolean.FALSE);
        }

        if (modelFieldAnnotation.unique()) {
            modelFieldBean.setUnique(Boolean.TRUE);
        }

        modelFieldBean.setMapping(trimToNull(modelFieldAnnotation.mapping()));

        if (!modelFieldAnnotation.persist()) {
            modelFieldBean.setPersist(Boolean.FALSE);
        }

        if (modelFieldAnnotation.critical()) {
            modelFieldBean.setCritical(Boolean.TRUE);
        }

        modelFieldBean.setConvert(trimToNull(modelFieldAnnotation.convert()));

        modelFieldBean.setCalculate(trimToNull(modelFieldAnnotation.calculate()));

        List<String> depends = Arrays.asList(modelFieldAnnotation.depends());
        if (!depends.isEmpty()) {
            modelFieldBean.setDepends(depends);
        } else {
            modelFieldBean.setDepends(null);
        }

        ReferenceBean reference = new ReferenceBean(modelFieldAnnotation.reference());
        if (reference.hasAnyProperties()) {
            if (reference.typeOnly()) {
                modelFieldBean.setReference(reference.getType());
            } else {
                modelFieldBean.setReference(reference);
            }
        }
    }

    public static String generateJavascript(ModelBean model, OutputConfig outputConfig) {

        if (!outputConfig.isDebug()) {
            JsCacheKey key = new JsCacheKey(model, outputConfig);

            SoftReference<String> jsReference = jsCache.get(key);
            if (jsReference != null && jsReference.get() != null) {
                return jsReference.get();
            }
        }

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);

        if (!outputConfig.isSurroundApiWithQuotes()) {
            if (outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
                mapper.addMixInAnnotations(ProxyObject.class, ProxyObjectWithoutApiQuotesExtJs5Mixin.class);
            } else {
                mapper.addMixInAnnotations(ProxyObject.class, ProxyObjectWithoutApiQuotesMixin.class);
            }
            mapper.addMixInAnnotations(ApiObject.class, ApiObjectMixin.class);
        } else {
            if (outputConfig.getOutputFormat() != OutputFormat.EXTJS5) {
                mapper.addMixInAnnotations(ProxyObject.class, ProxyObjectWithApiQuotesMixin.class);
            }
        }

        Map<String, Object> modelObject = new LinkedHashMap<String, Object>();
        modelObject.put("extend", model.getExtend());

        if (!model.getAssociations().isEmpty()) {
            Set<String> usesClasses = new HashSet<String>();
            for (AbstractAssociation association : model.getAssociations()) {
                usesClasses.add(association.getModel());
            }

            usesClasses.remove(model.getName());

            if (!usesClasses.isEmpty()) {
                modelObject.put("uses", usesClasses);
            }
        }

        Map<String, Object> configObject = new LinkedHashMap<String, Object>();
        ProxyObject proxyObject = new ProxyObject(model, outputConfig);

        Map<String, ModelFieldBean> fields = model.getFields();
        Set<String> requires = new HashSet<String>();

        if (!model.getValidations().isEmpty() && outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
            requires = addValidatorsToField(fields, model.getValidations());
        }

        if (proxyObject.hasContent() && outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
            requires.add("Ext.data.proxy.Direct");
        }

        if (StringUtils.hasText(model.getIdentifier()) && outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
            if ("sequential".equals(model.getIdentifier())) {
                requires.add("Ext.data.identifier.Sequential");
            } else if ("uuid".equals(model.getIdentifier())) {
                requires.add("Ext.data.identifier.Uuid");
            } else if ("negative".equals(model.getIdentifier())) {
                requires.add("Ext.data.identifier.Negative");
            }
        }

        if (requires != null && !requires.isEmpty()) {
            configObject.put("requires", requires);
        }

        if (StringUtils.hasText(model.getIdentifier())) {
            if (outputConfig.getOutputFormat() == OutputFormat.EXTJS5
                    || outputConfig.getOutputFormat() == OutputFormat.TOUCH2) {
                configObject.put("identifier", model.getIdentifier());
            } else {
                configObject.put("idgen", model.getIdentifier());
            }
        }

        if (StringUtils.hasText(model.getIdProperty()) && !model.getIdProperty().equals("id")) {
            configObject.put("idProperty", model.getIdProperty());
        }

        if (outputConfig.getOutputFormat() == OutputFormat.EXTJS5
                && StringUtils.hasText(model.getVersionProperty())) {
            configObject.put("versionProperty", model.getVersionProperty());
        }

        if (StringUtils.hasText(model.getClientIdProperty())) {

            if (outputConfig.getOutputFormat() == OutputFormat.EXTJS5
                    || outputConfig.getOutputFormat() == OutputFormat.EXTJS4) {
                configObject.put("clientIdProperty", model.getClientIdProperty());
            } else if (outputConfig.getOutputFormat() == OutputFormat.TOUCH2
                    && !"clientId".equals(model.getClientIdProperty())) {
                configObject.put("clientIdProperty", model.getClientIdProperty());
            }
        }

        for (ModelFieldBean field : fields.values()) {
            field.updateTypes(outputConfig);
        }

        List<Object> fieldConfigObjects = new ArrayList<Object>();
        for (ModelFieldBean field : fields.values()) {
            if (field.hasOnlyName(outputConfig)) {
                fieldConfigObjects.add(field.getName());
            } else {
                fieldConfigObjects.add(field);
            }
        }
        configObject.put("fields", fieldConfigObjects);

        if (!model.getAssociations().isEmpty()) {
            configObject.put("associations", model.getAssociations());
        }

        if (!model.getValidations().isEmpty() && !(outputConfig.getOutputFormat() == OutputFormat.EXTJS5)) {
            configObject.put("validations", model.getValidations());
        }

        if (proxyObject.hasContent()) {
            configObject.put("proxy", proxyObject);
        }

        if (outputConfig.getOutputFormat() == OutputFormat.EXTJS4
                || outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
            modelObject.putAll(configObject);
        } else {
            modelObject.put("config", configObject);
        }

        StringBuilder sb = new StringBuilder();
        sb.append("Ext.define(\"").append(model.getName()).append("\",");
        if (outputConfig.isDebug()) {
            sb.append("\n");
        }

        String configObjectString;
        Class<?> jsonView = JsonViews.ExtJS4.class;
        if (outputConfig.getOutputFormat() == OutputFormat.TOUCH2) {
            jsonView = JsonViews.Touch2.class;
        } else if (outputConfig.getOutputFormat() == OutputFormat.EXTJS5) {
            jsonView = JsonViews.ExtJS5.class;
        }

        try {
            if (outputConfig.isDebug()) {
                configObjectString = mapper.writerWithDefaultPrettyPrinter().withView(jsonView)
                        .writeValueAsString(modelObject);
            } else {
                configObjectString = mapper.writerWithView(jsonView).writeValueAsString(modelObject);
            }

        } catch (JsonGenerationException e) {
            throw new RuntimeException(e);
        } catch (JsonMappingException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        sb.append(configObjectString);
        sb.append(");");

        String result = sb.toString();

        if (outputConfig.isUseSingleQuotes()) {
            result = result.replace('"', '\'');
        }

        if (!outputConfig.isDebug()) {
            jsCache.put(new JsCacheKey(model, outputConfig), new SoftReference<String>(result));
        }
        return result;
    }

    private static Set<String> addValidatorsToField(Map<String, ModelFieldBean> fields,
            List<AbstractValidation> validations) {

        Set<String> requires = new TreeSet<String>();

        for (ModelFieldBean field : fields.values()) {
            for (AbstractValidation validation : validations) {
                if (field.getName().equals(validation.getField())) {
                    List<AbstractValidation> validators = field.getValidators();
                    if (validators == null) {
                        validators = new ArrayList<AbstractValidation>();
                        field.setValidators(validators);
                    }

                    String validatorClass = getValidatorClass(validation.getType());
                    if (validatorClass != null) {
                        requires.add(validatorClass);
                    }

                    boolean alreadyExists = false;
                    for (AbstractValidation validator : validators) {
                        if (validation.getType().equals(validator.getType())) {
                            alreadyExists = true;
                            break;
                        }
                    }

                    if (!alreadyExists) {
                        validators.add(validation);
                    }
                }
            }
        }

        return requires;
    }

    private static String getValidatorClass(String type) {
        if (type.equals("email")) {
            return "Ext.data.validator.Email";
        } else if (type.equals("exclusion")) {
            return "Ext.data.validator.Exclusion";
        } else if (type.equals("format")) {
            return "Ext.data.validator.Format";
        } else if (type.equals("inclusion")) {
            return "Ext.data.validator.Inclusion";
        } else if (type.equals("length")) {
            return "Ext.data.validator.Length";
        } else if (type.equals("presence")) {
            return "Ext.data.validator.Presence";
        } else if (type.equals("range")) {
            return "Ext.data.validator.Range";
        }
        return null;
    }

    static String trimToNull(String str) {
        String trimmedStr = StringUtils.trimWhitespace(str);
        if (StringUtils.hasLength(trimmedStr)) {
            return trimmedStr;
        }
        return null;
    }

    /**
     * Clears the model and Javascript code caches
     */
    public static void clearCaches() {
        modelCache.clear();
        jsCache.clear();
    }

}