Java tutorial
/* * Copyright (C) 2015 Contentful GmbH * * 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 com.contentful.vault.compiler; import com.contentful.vault.ContentType; import com.contentful.vault.Field; import com.contentful.vault.FieldMeta; import com.contentful.vault.Resource; import com.contentful.vault.Space; import com.google.common.base.Joiner; import com.squareup.javapoet.ClassName; import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.Type; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import org.apache.commons.lang3.StringUtils; import static com.contentful.java.cda.CDAType.ASSET; import static com.contentful.java.cda.CDAType.ENTRY; import static com.contentful.vault.Constants.SUFFIX_FIELDS; import static com.contentful.vault.Constants.SUFFIX_MODEL; import static com.contentful.vault.Constants.SUFFIX_SPACE; import static javax.tools.Diagnostic.Kind.ERROR; public class Processor extends AbstractProcessor { private Elements elementUtils; private Types typeUtils; private Filer filer; private static final String FQ_ASSET = "com.contentful.vault.Asset"; @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(ContentType.class.getCanonicalName()); types.add(Space.class.getCanonicalName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); typeUtils = processingEnv.getTypeUtils(); filer = processingEnv.getFiler(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Injection injection : findAndParseTargets(roundEnv)) { try { injection.brewJava().writeTo(filer); } catch (Exception e) { TypeElement element = injection.originatingElement; error(element, "Failed writing injection for \"%s\", message: %s", element.getQualifiedName(), e.getMessage()); } } return true; } private Set<Injection> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, ModelInjection> models = new LinkedHashMap<>(); Map<TypeElement, FieldsInjection> fields = new LinkedHashMap<>(); Map<TypeElement, SpaceInjection> spaces = new LinkedHashMap<>(); // Parse ContentType bindings for (Element element : env.getElementsAnnotatedWith(ContentType.class)) { try { parseContentType((TypeElement) element, models); } catch (Exception e) { parsingError(element, ContentType.class, e); } } // Parse Space bindings for (Element element : env.getElementsAnnotatedWith(Space.class)) { try { parseSpace((TypeElement) element, spaces, models); } catch (Exception e) { parsingError(element, Space.class, e); } } // Prepare FieldsInjection targets for (ModelInjection modelInjection : models.values()) { fields.put(modelInjection.originatingElement, createFieldsInjection(modelInjection)); } Set<Injection> result = new LinkedHashSet<>(); result.addAll(models.values()); result.addAll(fields.values()); result.addAll(spaces.values()); return result; } private FieldsInjection createFieldsInjection(ModelInjection injection) { ClassName name = getInjectionClassName(injection.originatingElement, SUFFIX_FIELDS); return new FieldsInjection(injection.remoteId, name, injection.originatingElement, injection.fields); } private void parseSpace(TypeElement element, Map<TypeElement, SpaceInjection> spaces, Map<TypeElement, ModelInjection> models) { Space annotation = element.getAnnotation(Space.class); String id = annotation.value(); if (id.isEmpty()) { error(element, "@%s id may not be empty. (%s)", Space.class.getSimpleName(), element.getQualifiedName()); return; } TypeMirror spaceMirror = elementUtils.getTypeElement(Space.class.getName()).asType(); List<ModelInjection> includedModels = new ArrayList<>(); for (AnnotationMirror mirror : element.getAnnotationMirrors()) { if (typeUtils.isSameType(mirror.getAnnotationType(), spaceMirror)) { Set<? extends Map.Entry<? extends ExecutableElement, ? extends AnnotationValue>> items = mirror .getElementValues().entrySet(); for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : items) { if ("models".equals(entry.getKey().getSimpleName().toString())) { List l = (List) entry.getValue().getValue(); if (l.size() == 0) { error(element, "@%s models must not be empty. (%s)", Space.class.getSimpleName(), element.getQualifiedName()); return; } Set<String> modelIds = new LinkedHashSet<>(); for (Object model : l) { TypeElement e = (TypeElement) ((Type) ((Attribute) model).getValue()).asElement(); ModelInjection modelInjection = models.get(e); if (modelInjection == null) { return; } else { String rid = modelInjection.remoteId; if (!modelIds.add(rid)) { error(element, "@%s includes multiple models with the same id \"%s\". (%s)", Space.class.getSimpleName(), rid, element.getQualifiedName()); return; } includedModels.add(modelInjection); } } } } } } List<String> locales = Arrays.asList(annotation.locales()); Set<String> checked = new HashSet<>(); for (int i = locales.size() - 1; i >= 0; i--) { String code = locales.get(i); if (!checked.add(code)) { error(element, "@%s contains duplicate locale code '%s'. (%s)", Space.class.getSimpleName(), code, element.getQualifiedName()); return; } else if (code.contains(" ") || code.isEmpty()) { error(element, "Invalid locale code '%s', must not be empty and may not contain spaces. (%s)", code, element.getQualifiedName()); return; } } if (checked.size() == 0) { error(element, "@%s at least one locale must be configured. (%s)", Space.class.getSimpleName(), element.getQualifiedName()); return; } ClassName injectionClassName = getInjectionClassName(element, SUFFIX_SPACE); String dbName = "space_" + SqliteUtils.hashForId(id); String copyPath = StringUtils.defaultIfBlank(annotation.copyPath(), null); spaces.put(element, new SpaceInjection(id, injectionClassName, element, includedModels, dbName, annotation.dbVersion(), copyPath, locales)); } private void parseContentType(TypeElement element, Map<TypeElement, ModelInjection> models) { String id = element.getAnnotation(ContentType.class).value(); if (id.isEmpty()) { error(element, "@%s id may not be empty. (%s)", ContentType.class.getSimpleName(), element.getQualifiedName()); return; } if (!isSubtypeOfType(element.asType(), Resource.class.getName())) { error(element, "Classes annotated with @%s must extend \"" + Resource.class.getName() + "\". (%s)", ContentType.class.getSimpleName(), element.getQualifiedName()); return; } Set<FieldMeta> fields = new LinkedHashSet<>(); Set<String> memberIds = new LinkedHashSet<>(); for (Element enclosedElement : element.getEnclosedElements()) { Field field = enclosedElement.getAnnotation(Field.class); if (field == null) { continue; } String fieldId = field.value(); if (fieldId.isEmpty()) { fieldId = enclosedElement.getSimpleName().toString(); } Set<Modifier> modifiers = enclosedElement.getModifiers(); if (modifiers.contains(Modifier.STATIC)) { error(element, "@%s elements must not be static. (%s.%s)", Field.class.getSimpleName(), element.getQualifiedName(), enclosedElement.getSimpleName()); return; } if (modifiers.contains(Modifier.PRIVATE)) { error(element, "@%s elements must not be private. (%s.%s)", Field.class.getSimpleName(), element.getQualifiedName(), enclosedElement.getSimpleName()); return; } if (!memberIds.add(fieldId)) { error(element, "@%s for the same id (\"%s\") was used multiple times in the same class. (%s)", Field.class.getSimpleName(), fieldId, element.getQualifiedName()); return; } FieldMeta.Builder fieldBuilder = FieldMeta.builder(); if (isList(enclosedElement)) { DeclaredType declaredType = (DeclaredType) enclosedElement.asType(); List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() == 0) { error(element, "Array fields must have a type parameter specified. (%s.%s)", element.getQualifiedName(), enclosedElement.getSimpleName()); return; } TypeMirror arrayType = typeArguments.get(0); if (!isValidListType(arrayType)) { error(element, "Invalid list type \"%s\" specified. (%s.%s)", arrayType.toString(), element.getQualifiedName(), enclosedElement.getSimpleName()); return; } String sqliteType = null; if (String.class.getName().equals(arrayType.toString())) { sqliteType = SqliteUtils.typeForClass(List.class.getName()); } fieldBuilder.setSqliteType(sqliteType).setArrayType(arrayType.toString()); } else { TypeMirror enclosedType = enclosedElement.asType(); String linkType = getLinkType(enclosedType); String sqliteType = null; if (linkType == null) { sqliteType = SqliteUtils.typeForClass(enclosedType.toString()); if (sqliteType == null) { error(element, "@%s specified for unsupported type (\"%s\"). (%s.%s)", Field.class.getSimpleName(), enclosedType.toString(), element.getQualifiedName(), enclosedElement.getSimpleName()); return; } } fieldBuilder.setSqliteType(sqliteType).setLinkType(linkType); } fields.add(fieldBuilder.setId(fieldId).setName(enclosedElement.getSimpleName().toString()) .setType(enclosedElement.asType()).build()); } if (fields.size() == 0) { error(element, "Model must contain at least one @%s element. (%s)", Field.class.getSimpleName(), element.getQualifiedName()); return; } ClassName injectionClassName = getInjectionClassName(element, SUFFIX_MODEL); String tableName = "entry_" + SqliteUtils.hashForId(id); models.put(element, new ModelInjection(id, injectionClassName, element, tableName, fields)); } private boolean isValidListType(TypeMirror typeMirror) { return isSubtypeOfType(typeMirror, String.class.getName()) || isSubtypeOfType(typeMirror, Resource.class.getName()); } private boolean isList(Element element) { TypeMirror typeMirror = element.asType(); if (List.class.getName().equals(typeMirror.toString())) { return true; } return typeMirror instanceof DeclaredType && List.class.getName().equals(((DeclaredType) typeMirror).asElement().toString()); } private ClassName getInjectionClassName(TypeElement typeElement, String suffix) { ClassName specClassName = ClassName.get(typeElement); return ClassName.get(specClassName.packageName(), Joiner.on('$').join(specClassName.simpleNames()) + suffix); } private String getLinkType(TypeMirror typeMirror) { if (isSubtypeOfType(typeMirror, Resource.class.getName())) { if (isSubtypeOfType(typeMirror, FQ_ASSET)) { return ASSET.toString(); } else { return ENTRY.toString(); } } return null; } private void parsingError(Element element, Class<? extends Annotation> annotation, Exception e) { StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); error(element, "Unable to parse @%s injection.\n\n%s", annotation.getSimpleName(), stackTrace); } private void error(Element element, String message, Object... args) { if (args.length > 0) { message = String.format(message, args); } processingEnv.getMessager().printMessage(ERROR, message, element); } private boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) { if (otherType.equals(typeMirror.toString())) { return true; } if (!(typeMirror instanceof DeclaredType)) { return false; } DeclaredType declaredType = (DeclaredType) typeMirror; List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() > 0) { StringBuilder typeString = new StringBuilder(declaredType.asElement().toString()); typeString.append('<'); for (int i = 0; i < typeArguments.size(); i++) { if (i > 0) { typeString.append(','); } typeString.append('?'); } typeString.append('>'); if (typeString.toString().equals(otherType)) { return true; } } Element element = declaredType.asElement(); if (!(element instanceof TypeElement)) { return false; } TypeElement typeElement = (TypeElement) element; TypeMirror superType = typeElement.getSuperclass(); if (isSubtypeOfType(superType, otherType)) { return true; } for (TypeMirror interfaceType : typeElement.getInterfaces()) { if (isSubtypeOfType(interfaceType, otherType)) { return true; } } return false; } }