net.phonex.intellij.android.dbmodel.CodeGenerator.java Source code

Java tutorial

Introduction

Here is the source code for net.phonex.intellij.android.dbmodel.CodeGenerator.java

Source

/*
 * Copyright (C) 2013 Micha Charmas (http://blog.charmas.pl)
 * Copyright (C) 2015 Dusan Klinec Ph4r05.
 *
 * 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 net.phonex.intellij.android.dbmodel;

import com.google.common.base.CaseFormat;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import net.phonex.intellij.android.dbmodel.typeserializers.*;
import net.phonex.intellij.android.dbmodel.util.FieldDef;
import net.phonex.intellij.android.dbmodel.util.NewFieldRecord;
import net.phonex.intellij.android.dbmodel.util.PsiUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * Quite a few changes here by Dallas Gutauckis [dallas@gutauckis.com]
 */
public class CodeGenerator {
    public static final String CREATOR_NAME = "CREATOR";

    private final String[] integerTypes = new String[] { "byte", "double", "float", "int", "long", "short",
            "boolean", "java.lang.Byte", "java.lang.Double", "java.lang.Float", "java.lang.Integer",
            "java.lang.Long", "java.lang.Boolean" };

    private final PsiClass mClass;
    private final List<PsiField> mFields;
    private final TypeSerializerFactory mTypeSerializerFactory;

    public CodeGenerator(PsiClass psiClass, List<PsiField> fields) {
        mClass = psiClass;
        mFields = fields;

        this.mTypeSerializerFactory = new ChainSerializerFactory(new BundleSerializerFactory(),
                new DateSerializerFactory(), new EnumerationSerializerFactory(),
                new ParcelableListSerializerFactory(), new PrimitiveTypeSerializerFactory(),
                new PrimitiveArraySerializerFactory(), new ListSerializerFactory(),
                new ParcelableSerializerFactory(), new SerializableSerializerFactory());
    }

    private String generateStaticCreator(PsiClass psiClass) {
        StringBuilder sb = new StringBuilder("public static final android.os.Parcelable.Creator<");

        String className = psiClass.getName();

        sb.append(className).append("> CREATOR = new android.os.Parcelable.Creator<").append(className)
                .append(">(){").append("public ").append(className)
                .append(" createFromParcel(android.os.Parcel source) {").append("return new ").append(className)
                .append("(source);}").append("public ").append(className).append("[] newArray(int size) {")
                .append("return new ").append(className).append("[size];}").append("};");
        return sb.toString();
    }

    private String generateConstructor(List<PsiField> fields, PsiClass psiClass) {
        String className = psiClass.getName();

        StringBuilder sb = new StringBuilder("private ");

        // Create the Parcelable-required constructor
        sb.append(className).append("(android.os.Parcel in) {");

        // Creates all of the deserialization methods for the given fields
        for (PsiField field : fields) {
            sb.append(getSerializerForType(field).readValue(field, "in"));
        }

        sb.append("}");
        return sb.toString();
    }

    private String generateWriteToParcel(List<PsiField> fields) {
        StringBuilder sb = new StringBuilder(
                "@Override public void writeToParcel(android.os.Parcel dest, int flags) {");

        for (PsiField field : fields) {
            sb.append(getSerializerForType(field).writeValue(field, "dest", "flags"));
        }

        sb.append("}");

        return sb.toString();
    }

    private TypeSerializer getSerializerForType(PsiField field) {
        return mTypeSerializerFactory.getSerializer(field.getType());
    }

    private String generateDescribeContents() {
        return "@Override public int describeContents() { return 0; }";
    }

    public void generate() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());

        removeExistingParcelableImplementation(mClass);

        // Describe contents method
        PsiMethod describeContentsMethod = elementFactory.createMethodFromText(generateDescribeContents(), mClass);
        // Method for writing to the parcel
        PsiMethod writeToParcelMethod = elementFactory.createMethodFromText(generateWriteToParcel(mFields), mClass);

        // Default constructor if needed
        String defaultConstructorString = generateDefaultConstructor(mClass);
        PsiMethod defaultConstructor = null;

        if (defaultConstructorString != null) {
            defaultConstructor = elementFactory.createMethodFromText(defaultConstructorString, mClass);
        }

        // Constructor
        PsiMethod constructor = elementFactory.createMethodFromText(generateConstructor(mFields, mClass), mClass);
        // CREATOR
        PsiField creatorField = elementFactory.createFieldFromText(generateStaticCreator(mClass), mClass);

        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());

        // Shorten all class references
        styleManager.shortenClassReferences(addAsLast(describeContentsMethod));
        styleManager.shortenClassReferences(addAsLast(writeToParcelMethod));

        // Only adds if available
        if (defaultConstructor != null) {
            styleManager.shortenClassReferences(addAsLast(defaultConstructor));
        }

        styleManager.shortenClassReferences(addAsLast(constructor));
        styleManager.shortenClassReferences(addAsLast(creatorField));

        makeClassImplementParcelable(elementFactory);
    }

    public void generateFields() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());

        List<NewFieldRecord> newFields = new ArrayList<NewFieldRecord>();
        newFields.add(new NewFieldRecord("TABLE", mClass.getName(), "TODO: verify"));
        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            newFields.add(new NewFieldRecord(fieldName, field.getName()));
        }

        PsiElement prevField = null;
        for (NewFieldRecord newField : newFields) {
            // Find if field is already present in file.
            FieldDef prevFieldImpl = PsiUtils.findField(mClass, newField.name);
            if (prevFieldImpl == null) {
                PsiField curField = elementFactory.createFieldFromText(newField.decl, mClass);
                PsiElement element = prevField == null ? addAsLast(curField) : mClass.addAfter(curField, prevField);
                prevField = styleManager.shortenClassReferences(element);
                continue;
            } else {
                prevField = prevFieldImpl.field;
            }

            // Present, do we have exact match of the values?
            final String valueInitializer = "\"" + newField.value + "\"";
            if (newField.value.equals(prevFieldImpl.initializer)
                    || valueInitializer.equals(prevFieldImpl.initializer)) {
                continue;
            }

            // Present and value differs, add anyway, but under previous field.
            newField.addComment("TODO: verify");
            PsiField curField = elementFactory.createFieldFromText(newField.decl, mClass);
            PsiElement element = prevField == null ? addAsLast(curField) : mClass.addAfter(curField, prevField);
            prevField = styleManager.shortenClassReferences(element);
        }
    }

    public void generateFullProjection() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());
        StringBuilder sb = new StringBuilder("public static final String[] FULL_PROJECTION = new String[] {\n");
        final int nmFields = mFields.size();
        int cnFields = 0;

        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            String comma = cnFields + 1 == nmFields ? "" : ",";

            sb.append(fieldName).append(comma);
            cnFields += 1;
        }
        sb.append("\n};\n");

        FieldDef prevField = PsiUtils.findField(mClass, "FULL_PROJECTION");
        PsiField projectionField = elementFactory.createFieldFromText(sb.toString(), mClass);
        PsiElement element = prevField == null ? addAsLast(projectionField)
                : mClass.addAfter(projectionField, prevField.field);
        styleManager.shortenClassReferences(element);
    }

    public void generateCreateTable() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());

        final String className = mClass.getQualifiedName();
        StringBuilder sb = new StringBuilder(
                "public static final String CREATE_TABLE = \"CREATE TABLE IF NOT EXISTS \"\n");
        sb.append("+ TABLE\n");
        sb.append("+ \" (\"\n");

        // Compute longest field name.
        int maxFieldLen = 0;
        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            final int fLen = fieldName.length();
            if (fLen > maxFieldLen) {
                maxFieldLen = fLen;
            }
        }

        // Build CREATE TABLE.
        final int nmFields = mFields.size();
        int cnFields = 0;
        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            String comma = cnFields + 1 == nmFields ? "" : ",";
            String sqlType = getSqlType(field);
            cnFields += 1;

            if ("FIELD_ID".equalsIgnoreCase(fieldName)) {
                sb.append("+ ").append(rightPad(fieldName, maxFieldLen + 3))
                        .append(" + \" INTEGER PRIMARY KEY AUTOINCREMENT").append(comma).append(" \"\n");
                continue;
            }

            sb.append("+ ").append(rightPad(fieldName, maxFieldLen + 3)).append(" + \" ").append(sqlType)
                    .append(comma).append(" \"\n");
        }

        sb.append("+ \");\";\n");

        FieldDef prevField = PsiUtils.findField(mClass, "CREATE_TABLE");
        PsiField projectionField = elementFactory.createFieldFromText(sb.toString(), mClass);
        PsiElement element = prevField == null ? addAsLast(projectionField)
                : mClass.addAfter(projectionField, prevField.field);
        styleManager.shortenClassReferences(element);
    }

    public void generateCreateFromCursor() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());

        StringBuilder sb = new StringBuilder("private final void createFromCursor(Cursor c){\n"
                + "        int colCount = c.getColumnCount();\n" + "        for(int i=0; i<colCount; i++){\n"
                + "            final String colname = c.getColumnName(i);");

        final int nmFields = mFields.size();
        int cnFields = 0;

        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            final boolean lastOne = cnFields + 1 == nmFields;
            if (cnFields > 0) {
                sb.append(" else ");
            }

            String sqlDeserializer = getSqlDeserializer(field);

            sb.append("if (").append(fieldName).append(".equals(colname)){\n");
            sb.append("this.").append(field.getName()).append(" = ").append(sqlDeserializer).append(";\n");
            sb.append("}");
            cnFields += 1;
        }

        if (nmFields > 0) {
            sb.append("else {\n" + "Log.w(THIS_FILE, \"Unknown column name: \" + colname);\n" + "}");
        }

        sb.append("}\n}\n");

        PsiMethod prevMethod = PsiUtils.findMethod(mClass, "createFromCursor", "android.database.Cursor");
        PsiMethod method = elementFactory.createMethodFromText(sb.toString(), mClass);
        PsiElement element = prevMethod == null ? addAsLast(method) : mClass.addAfter(method, prevMethod);
        styleManager.shortenClassReferences(element);
    }

    public void generateGetDbContentValues() {
        PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(mClass.getProject());
        JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mClass.getProject());

        StringBuilder sb = new StringBuilder(
                "public ContentValues getDbContentValues() {\n" + "    ContentValues args = new ContentValues();");

        final int nmFields = mFields.size();
        int cnFields = 0;

        for (PsiField field : mFields) {
            String fieldName = getFieldName(field.getName());
            PsiType fieldType = field.getType();
            String cnType = fieldType.getCanonicalText();
            final boolean lastOne = cnFields + 1 == nmFields;
            final boolean primitiveType = isPrimitiveType(fieldType);
            if (!primitiveType) {
                sb.append("if (this." + (field.getName()) + " != null)\n");
                sb.append("    ");
            }

            final String sqlSerializer = getSqlSerializer(field);
            sb.append("args.put(" + fieldName + ", " + (sqlSerializer) + ");\n");

            cnFields += 1;
        }

        sb.append("return args;\n}");

        PsiMethod prevMethod = PsiUtils.findMethod(mClass, "getDbContentValues");
        PsiMethod method = elementFactory.createMethodFromText(sb.toString(), mClass);
        PsiElement element = prevMethod == null ? addAsLast(method) : mClass.addAfter(method, prevMethod);
        styleManager.shortenClassReferences(element);
    }

    private String rightPad(String name, int desiredSize) {
        final int ln = name.length();
        if (desiredSize <= ln) {
            return name;
        }

        int toAdd = desiredSize - ln;
        StringBuilder sb = new StringBuilder(name);
        for (int i = 0; i < toAdd; i++) {
            sb.append(" ");
        }

        return sb.toString();
    }

    private boolean isPrimitiveType(PsiType type) {
        final String cnType = type.getCanonicalText();
        return "byte".equals(cnType) || "char".equals(cnType) || "double".equals(cnType) || "float".equals(cnType)
                || "int".equals(cnType) || "long".equals(cnType) || "short".equals(cnType)
                || "boolean".equals(cnType) || "void".equals(cnType);
    }

    private String getSqlType(PsiField field) {
        String typeStr = field.getType().getCanonicalText();

        // Check for blob.
        if ("byte[]".equals(typeStr)) {
            return "BLOB";
        }

        // Check for INTEGER.
        for (String integerType : integerTypes) {
            if (integerType.equals(typeStr)) {
                return "INTEGER DEFAULT 0";
            }
        }

        // Special case - date.
        if ("java.util.Date".equals(typeStr)) {
            return "INTEGER DEFAULT 0";
        }

        // By default, return TEXT.
        return "TEXT";
    }

    private String getSqlSerializer(PsiField field) {
        String typeStr = field.getType().getCanonicalText();
        if ("java.util.Date".equalsIgnoreCase(typeStr)) {
            return (field.getName()) + ".getTime()";
        } else if ("boolean".equals(typeStr) || "java.lang.Boolean".equalsIgnoreCase(typeStr)) {
            return field.getName() + " ? 1 : 0";
        } else {
            return field.getName();
        }
    }

    private String getSqlDeserializer(PsiField field) {
        String typeStr = field.getType().getCanonicalText();
        if ("byte".equals(typeStr)) {
            return "(byte) c.getInt(i)";
        } else if ("java.lang.Byte".equals(typeStr)) {
            return "(Byte) c.getInt(i)";
        } else if ("short".equals(typeStr)) {
            return "c.getShort(i)";
        } else if ("int".equals(typeStr) || "java.lang.Integer".equals(typeStr)) {
            return "c.getInt(i)";
        } else if ("long".equals(typeStr) || "java.lang.Long".equals(typeStr)) {
            return "c.getLong(i)";
        } else if ("double".equals(typeStr) || "java.lang.Double".equals(typeStr)) {
            return "c.getDouble(i)";
        } else if ("float".equals(typeStr) || "java.lang.Float".equals(typeStr)) {
            return "c.getFloat(i)";
        } else if ("byte[]".equals(typeStr)) {
            return "c.getBlob(i)";
        } else if ("boolean".equals(typeStr)) {
            return "c.getInt(i) == 1";
        } else if ("java.lang.Boolean".equals(typeStr)) {
            return "(Boolean) (c.getInt(i) == 1)";
        } else if ("java.util.Date".equals(typeStr)) {
            return "new Date(c.getLong(i))";
        } else {
            return "c.getString(i)";
        }
    }

    private String getFieldName(String varName) {
        return "FIELD_" + CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, varName);
    }

    private PsiElement addAsLast(PsiElement elem) {
        return mClass.addBefore(elem, mClass.getLastChild());
    }

    /**
     * Strips the
     * @param psiClass
     */
    private void removeExistingParcelableImplementation(PsiClass psiClass) {
        PsiField[] allFields = psiClass.getAllFields();

        // Look for an existing CREATOR and remove it
        for (PsiField field : allFields) {
            if (field.getName().equals(CREATOR_NAME)) {
                // Creator already exists, need to remove/replace it
                field.delete();
            }
        }

        findAndRemoveMethod(psiClass, psiClass.getName(), "android.os.Parcel");
        findAndRemoveMethod(psiClass, "describeContents");
        findAndRemoveMethod(psiClass, "writeToParcel", "android.os.Parcel", "int");
    }

    private String generateDefaultConstructor(PsiClass clazz) {
        // Check for any constructors; if none exist, we'll make a default one
        if (clazz.getConstructors().length == 0) {
            // No constructors exist, make a default one for convenience
            StringBuilder sb = new StringBuilder();
            sb.append("public ").append(clazz.getName()).append("(){}").append('\n');
            return sb.toString();
        } else {
            return null;
        }
    }

    private void makeClassImplementParcelable(PsiElementFactory elementFactory) {
        final PsiClassType[] implementsListTypes = mClass.getImplementsListTypes();
        final String implementsType = "android.os.Parcelable";

        for (PsiClassType implementsListType : implementsListTypes) {
            PsiClass resolved = implementsListType.resolve();

            // Already implements Parcelable, no need to add it
            if (resolved != null && implementsType.equals(resolved.getQualifiedName())) {
                return;
            }
        }

        PsiJavaCodeReferenceElement implementsReference = elementFactory.createReferenceFromText(implementsType,
                mClass);
        PsiReferenceList implementsList = mClass.getImplementsList();

        if (implementsList != null) {
            implementsList.add(implementsReference);
        }
    }

    private static void findAndRemoveMethod(PsiClass clazz, String methodName, String... arguments) {
        // Maybe there's an easier way to do this with mClass.findMethodBySignature(), but I'm not an expert on Psi*
        PsiMethod[] methods = clazz.findMethodsByName(methodName, false);

        for (PsiMethod method : methods) {
            PsiParameterList parameterList = method.getParameterList();

            if (parameterList.getParametersCount() == arguments.length) {
                boolean shouldDelete = true;

                PsiParameter[] parameters = parameterList.getParameters();

                for (int i = 0; i < arguments.length; i++) {
                    if (!parameters[i].getType().getCanonicalText().equals(arguments[i])) {
                        shouldDelete = false;
                    }
                }

                if (shouldDelete) {
                    method.delete();
                }
            }
        }
    }

}