package com.arellomobile.mvp.compiler; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import com.arellomobile.mvp.MvpProcessor; import com.arellomobile.mvp.viewstate.strategy.AddToEndStrategy; import com.arellomobile.mvp.viewstate.strategy.StateStrategyType; 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.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.tools.Diagnostic; import static com.arellomobile.mvp.compiler.Util.fillGenerics; /** * Date: 18.12.2015 * Time: 13:24 * * @author Yuri Shmakov */ final class ViewStateClassGenerator extends ClassGenerator<TypeElement> { public static final String STATE_STRATEGY_TYPE_ANNOTATION = StateStrategyType.class.getName(); public static final String DEFAULT_STATE_STRATEGY = AddToEndStrategy.class.getName() + ".class"; private static final String DEFAULT_STATE_STRATEGY_OPTION = "defaultStateStrategy"; private String mViewClassName; private Set<String> mStrategyClasses; public ViewStateClassGenerator() { mStrategyClasses = new HashSet<>(); } public boolean generate(TypeElement typeElement, List<ClassGeneratingParams> classGeneratingParamsList) { String generic = Util.getClassGenerics(typeElement); String interfaceGeneric = ""; if (!typeElement.getTypeParameters().isEmpty()) { interfaceGeneric = "<" + join(",", typeElement.getTypeParameters()) + ">"; } String fullClassName = Util.getFullClassName(typeElement); ClassGeneratingParams classGeneratingParams = new ClassGeneratingParams(); classGeneratingParams.setName(fullClassName + MvpProcessor.VIEW_STATE_SUFFIX); mViewClassName = getClassName(typeElement) + interfaceGeneric; String builder = "package " + fullClassName.substring(0, fullClassName.lastIndexOf(".")) + ";\n" + "\n" + "import java.util.Set;\n" + "\n" + "import com.arellomobile.mvp.viewstate.MvpViewState;\n" + "import com.arellomobile.mvp.viewstate.ViewCommand;\n" + "import com.arellomobile.mvp.viewstate.ViewCommands;\n" + "import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy;\n" + "import com.arellomobile.mvp.viewstate.strategy.AddToEndStrategy;\n" + "import com.arellomobile.mvp.viewstate.strategy.StateStrategy;\n" + "\n" + "public class " + fullClassName.substring(fullClassName.lastIndexOf(".") + 1) + "$$State" + generic + " extends MvpViewState<" + mViewClassName + "> implements " + mViewClassName + " {\n" + "\n"; List<Method> methods = new ArrayList<>(); String stateStrategyType = getStateStrategyType(typeElement); Map<String, String> types = new HashMap<>(); if (!typeElement.getTypeParameters().isEmpty()) { for (TypeParameterElement typeParameterElement : typeElement.getTypeParameters()) { types.put(typeParameterElement.toString(), typeParameterElement.toString()); } } // Get methods for input class getMethods(types, typeElement, stateStrategyType, new ArrayList<Method>(), methods); // Add methods from super intefaces methods.addAll(iterateInterfaces(0, typeElement, stateStrategyType, new HashMap<String, String>(), methods, new ArrayList<Method>())); // Allow methods be with same names Map<String, Integer> methodsCounter = new HashMap<>(); for (Method method : methods) { Integer counter = methodsCounter.get(method.name); if (counter == null || counter == 0) { counter = 0; method.uniqueName = method.name; } else { method.uniqueName = method.name + counter; } method.commandClassName = method.uniqueName.substring(0, 1).toUpperCase() + method.uniqueName.substring(1) + "Command"; counter++; methodsCounter.put(method.name, counter); } for (Method method : methods) { String throwTypesString = join(", ", method.thrownTypes); if (throwTypesString.length() > 0) { throwTypesString = " throws " + throwTypesString; } String fieldName = "params"; String argumentsString = ""; List<String> argumentNames = new ArrayList<>(); int index = 0; for (Argument argument : method.arguments) { if (argument.name.equals(fieldName)) { fieldName = "params" + index; } if (argumentsString.length() > 0) { argumentsString += ", "; } argumentsString += argument.name; argumentNames.add(argument.name); index++; } String commandFieldName = decapitalizeString(method.commandClassName); String commandClassName = method.commandClassName; String commandWrapperNewInstance = "new " + method.commandClassName + "(" + argumentsString + ");\n"; // Add salt if contains argument with same name Random random = new Random(); while (argumentNames.contains(commandFieldName)) { commandFieldName += random.nextInt(10); } builder += "\t@Override\n" + "\tpublic " + method.genericType + " void " + method.name + "(" + join(", ", method.arguments) + ")" + throwTypesString + " {\n" + "\t\t" + commandClassName + " " + commandFieldName + " = " + commandWrapperNewInstance + "\t\tmViewCommands.beforeApply(" + commandFieldName + ");\n" + "\n" + "\t\tif (mViews == null || mViews.isEmpty()) {\n" + "\t\t\treturn;\n" + "\t\t}\n" + "\n" + "\t\tfor(" + mViewClassName + " view : mViews) {\n" + "\t\t\tview." + method.name + "(" + argumentsString + ");\n" + "\t\t}\n" + "\n" + "\t\tmViewCommands.afterApply(" + commandFieldName + ");\n" + "\t}\n" + "\n"; } if (!methods.isEmpty()) { builder = generateLocalViewCommand(mViewClassName, builder, methods); } builder += "}\n"; classGeneratingParams.setBody(builder); classGeneratingParamsList.add(classGeneratingParams); return true; } private List<Method> iterateInterfaces(int level, TypeElement parentElement, String parentDefaultStrategy, Map<String, String> parentTypes, List<Method> rootMethods, List<Method> superinterfacesMethods) { for (TypeMirror typeMirror : parentElement.getInterfaces()) { final TypeElement anInterface = (TypeElement) ((DeclaredType) typeMirror).asElement(); final List<? extends TypeMirror> typeArguments = ((DeclaredType) typeMirror).getTypeArguments(); final List<? extends TypeParameterElement> typeParameters = anInterface.getTypeParameters(); if (typeArguments.size() > typeParameters.size()) { throw new IllegalArgumentException("Code generation for interface " + anInterface.getSimpleName() + " failed. Simplify your generics."); } Map<String, String> types = new HashMap<>(); for (int i = 0; i < typeArguments.size(); i++) { types.put(typeParameters.get(i).toString(), typeArguments.get(i).toString()); } Map<String, String> totalInterfaceTypes = new HashMap<>(typeParameters.size()); for (int i = 0; i < typeArguments.size(); i++) { totalInterfaceTypes.put(typeParameters.get(i).toString(), fillGenerics(parentTypes, typeArguments.get(i))); } for (int i = typeArguments.size(); i < typeParameters.size(); i++) { if (typeParameters.get(i).getBounds().size() != 1) { throw new IllegalArgumentException("Code generation for interface " + anInterface.getSimpleName() + " failed. Simplify your generics."); } totalInterfaceTypes.put(typeParameters.get(i).toString(), typeParameters.get(i).getBounds().get(0).toString()); } String defaultStrategy = parentDefaultStrategy != null ? parentDefaultStrategy : getStateStrategyType(anInterface); getMethods(totalInterfaceTypes, anInterface, defaultStrategy, rootMethods, superinterfacesMethods); iterateInterfaces(level + 1, anInterface, defaultStrategy, types, rootMethods, superinterfacesMethods); } return superinterfacesMethods; } private List<Method> getMethods(Map<String, String> types, TypeElement typeElement, String defaultStrategy, List<Method> rootMethods, List<Method> superinterfacesMethods) { for (Element element : typeElement.getEnclosedElements()) { if (!(element instanceof ExecutableElement)) { continue; } final ExecutableElement methodElement = (ExecutableElement) element; if (!methodElement.getReturnType().toString().equals("void")) { MvpCompiler.getMessager().printMessage(Diagnostic.Kind.ERROR, "You are trying generate ViewState for " + typeElement.getSimpleName() + ". But " + typeElement.getSimpleName() + " contains non-void method \"" + methodElement.getSimpleName() + "\" that return type is " + methodElement.getReturnType() + ". See more here: https://github.com/Arello-Mobile/Moxy/issues/2"); } String strategyClass = defaultStrategy != null ? defaultStrategy : getDefaultStateStrategy(); String methodTag = "\"" + methodElement.getSimpleName() + "\""; for (AnnotationMirror annotationMirror : methodElement.getAnnotationMirrors()) { if (!annotationMirror.getAnnotationType().asElement().toString().equals(STATE_STRATEGY_TYPE_ANNOTATION)) { continue; } final Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues(); final Set<? extends ExecutableElement> keySet = elementValues.keySet(); for (ExecutableElement executableElement : keySet) { String key = executableElement.getSimpleName().toString(); if ("value".equals(key)) { strategyClass = elementValues.get(executableElement).toString(); } else if ("tag".equals(key)) { methodTag = elementValues.get(executableElement).toString(); } } } Map<String, String> methodTypes = new HashMap<>(types); final ExecutableType executableType = (ExecutableType) methodElement.asType(); final List<? extends TypeVariable> typeVariables = executableType.getTypeVariables(); if (!typeVariables.isEmpty()) { for (TypeVariable typeVariable : typeVariables) { methodTypes.put(typeVariable.asElement().toString(), typeVariable.asElement().toString()); } } String generics = ""; if (!typeVariables.isEmpty()) { generics += "<"; for (TypeVariable typeVariable : typeVariables) { if (generics.length() > 1) { generics += ", "; } final TypeMirror upperBound = typeVariable.getUpperBound(); if (upperBound.toString().equals(Object.class.getCanonicalName())) { generics += typeVariable.asElement(); continue; } final String filledGeneric = fillGenerics(methodTypes, upperBound); if (filledGeneric.startsWith("?")) { generics += filledGeneric.replaceFirst("\\?", typeVariable.asElement().toString()); } else { generics += typeVariable.asElement() + " extends " + filledGeneric; } } generics += "> "; } final List<? extends VariableElement> parameters = methodElement.getParameters(); List<Argument> arguments = new ArrayList<>(); for (VariableElement parameter : parameters) { arguments.add(new Argument(fillGenerics(methodTypes, parameter.asType()), parameter.toString(), parameter.getAnnotationMirrors())); } List<String> throwTypes = new ArrayList<>(); for (TypeMirror typeMirror : methodElement.getThrownTypes()) { throwTypes.add(fillGenerics(methodTypes, typeMirror)); } mStrategyClasses.add(strategyClass); final Method method = new Method(generics, methodElement.getSimpleName().toString(), arguments, throwTypes, strategyClass, methodTag, getClassName(typeElement)); if (rootMethods.contains(method)) { continue; } if (superinterfacesMethods.contains(method)) { final Method existingMethod = superinterfacesMethods.get(superinterfacesMethods.indexOf(method)); if (!existingMethod.stateStrategy.equals(method.stateStrategy)) { throw new IllegalStateException("Both " + existingMethod.enclosedClass + " and " + method.enclosedClass + " has method " + method.name + "(" + method.arguments.toString().substring(1, method.arguments.toString().length() - 1) + ") with difference strategies. Override this method in " + mViewClassName + " or make strategies equals"); } if (!existingMethod.tag.equals(method.tag)) { throw new IllegalStateException("Both " + existingMethod.enclosedClass + " and " + method.enclosedClass + " has method " + method.name + "(" + method.arguments.toString().substring(1, method.arguments.toString().length() - 1) + ") with difference tags. Override this method in " + mViewClassName + " or make tags equals"); } continue; } superinterfacesMethods.add(method); } return superinterfacesMethods; } private String getClassName(TypeElement typeElement) { return typeElement.getQualifiedName().toString(); } private String generateLocalViewCommand(String viewClassName, String builder, List<Method> methods) { for (Method method : methods) { String argumentsString = ""; for (Argument argument : method.arguments) { if (argumentsString.length() > 0) { argumentsString += ", "; } argumentsString += argument.name; } String argumentsInit = ""; String argumentsBind = ""; for (Argument argument : method.arguments) { argumentsInit += "\t\tpublic final " + argument.type + " " + argument.name + ";\n"; argumentsBind += "\t\t\tthis." + argument.name + " = " + argument.name + ";\n"; } if (!argumentsInit.isEmpty()) { argumentsInit += "\n"; } builder += "\n\tpublic class " + method.commandClassName + method.genericType + " extends ViewCommand<" + viewClassName + "> {\n" + argumentsInit + "\t\t" + method.commandClassName + "(" + join(", ", method.arguments) + ") {\n" + "\t\t\tsuper(" + method.tag + ", " + method.stateStrategy + ");\n" + argumentsBind + "\t\t}\n" + "\n" + "\t\t@Override\n" + "\t\tpublic void apply(" + viewClassName + " mvpView) {\n" + "\t\t\tmvpView." + method.name + "(" + argumentsString + ");\n" + "\t\t}\n" + "\t}\n"; } return builder; } private String getDefaultStateStrategy() { return DEFAULT_STATE_STRATEGY; } public String getStateStrategyType(TypeElement typeElement) { for (AnnotationMirror annotationMirror : typeElement.getAnnotationMirrors()) { if (!annotationMirror.getAnnotationType().asElement().toString().equals(STATE_STRATEGY_TYPE_ANNOTATION)) { continue; } final Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues(); final Set<? extends ExecutableElement> keySet = elementValues.keySet(); for (ExecutableElement key : keySet) { if ("value".equals(key.getSimpleName().toString())) { return elementValues.get(key).toString(); } } } return null; } private static class Method { String genericType; String name; String uniqueName; // required for methods with same name but difference params String commandClassName; List<Argument> arguments; List<String> thrownTypes; String stateStrategy; String tag; String enclosedClass; Method(String genericType, String name, List<Argument> arguments, List<String> thrownTypes, String stateStrategy, String methodTag, String enclosedClass) { this.genericType = genericType; this.name = name; this.arguments = arguments; this.thrownTypes = thrownTypes; this.stateStrategy = stateStrategy; this.tag = methodTag; this.enclosedClass = enclosedClass; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Method method = (Method) o; //noinspection SimplifiableIfStatement if (name != null ? !name.equals(method.name) : method.name != null) { return false; } return !(arguments != null ? !arguments.equals(method.arguments) : method.arguments != null); } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (arguments != null ? arguments.hashCode() : 0); return result; } @Override public String toString() { return "Method { " + genericType + " void " + name + '(' + arguments + ") throws " + thrownTypes + '}'; } } private static class Argument { String type; String name; List<? extends AnnotationMirror> annotations; public Argument(String type, String name, List<? extends AnnotationMirror> annotations) { this.type = type; this.name = name; this.annotations = annotations; } @Override public String toString() { return join(" ", annotations) + " " + type + " " + name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Argument argument = (Argument) o; return !(type != null ? !type.equals(argument.type) : argument.type != null); } @Override public int hashCode() { return type != null ? type.hashCode() : 0; } } public static String join(CharSequence delimiter, Iterable tokens) { StringBuilder sb = new StringBuilder(); boolean firstTime = true; for (Object token : tokens) { if (firstTime) { firstTime = false; } else { sb.append(delimiter); } sb.append(token); } return sb.toString(); } public static String decapitalizeString(String string) { return string == null || string.isEmpty() ? "" : string.length() == 1 ? string.toLowerCase() : Character.toLowerCase(string.charAt(0)) + string.substring(1); } public Set<String> getStrategyClasses() { return mStrategyClasses; } }