| /* |
| * Copyright 2010 Google Inc. |
| * |
| * 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.google.gwt.editor.rebind.model; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.JField; |
| import com.google.gwt.core.ext.typeinfo.JGenericType; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.JParameterizedType; |
| import com.google.gwt.core.ext.typeinfo.JPrimitiveType; |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.editor.client.CompositeEditor; |
| import com.google.gwt.editor.client.Editor; |
| import com.google.gwt.editor.client.IsEditor; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Analyzes an Editor driver declaration. |
| */ |
| public class EditorModel { |
| private static final EditorData[] EMPTY_EDITOR_DATA = new EditorData[0]; |
| |
| /** |
| * Given type assignable to <code>ComposedEditor<Foo,Bar></code>, return |
| * <code>{Foo, Bar}</code>. It is an error to call this method with a type not |
| * assignable to {@link ComposedEditor}. |
| */ |
| static JClassType[] calculateCompositeTypes(JClassType editorType) { |
| JClassType compositeEditorIntf = editorType.getOracle().findType( |
| CompositeEditor.class.getName()); |
| assert compositeEditorIntf.isAssignableFrom(editorType) : editorType.getQualifiedSourceName() |
| + " is not a ComposedEditor"; |
| |
| for (JClassType supertype : editorType.getFlattenedSupertypeHierarchy()) { |
| JParameterizedType parameterized = supertype.isParameterized(); |
| if (parameterized != null) { |
| // Found the Editor<Foo> supertype |
| if (compositeEditorIntf.equals(parameterized.getBaseType())) { |
| JClassType[] typeArgs = parameterized.getTypeArgs(); |
| assert typeArgs.length == 3; |
| return new JClassType[]{typeArgs[1], typeArgs[2]}; |
| } |
| } |
| } |
| assert false : "Did not find ComposedEditor parameterization for " |
| + editorType.getQualifiedSourceName(); |
| throw new RuntimeException(); |
| } |
| |
| /** |
| * Given type assignable to <code>Editor<Foo></code>, return |
| * <code>Foo</code>. It is an error to call this method with a type not |
| * assignable to {@link Editor}. |
| */ |
| static JClassType calculateEditedType(TreeLogger logger, JClassType editorType) |
| throws UnableToCompleteException { |
| JClassType editorIntf = editorType.getOracle().findType( |
| Editor.class.getName()); |
| JClassType parameterization[] = ModelUtils.findParameterizationOf( |
| editorIntf, editorType); |
| if (parameterization != null) { |
| return parameterization[0]; |
| } |
| logger.log(TreeLogger.ERROR, |
| noEditorParameterizationMessage(editorIntf, editorType)); |
| throw new UnableToCompleteException(); |
| } |
| |
| /** |
| * Given type assignable to <code>IsEditor<Foo, FooEditor></code>, return |
| * <code>FooEditor</code>. It is an error to call this method with a type not |
| * assignable to {@link IsEditor}. |
| */ |
| static JClassType calculateIsEditedType(TreeLogger logger, |
| JClassType editorType) throws UnableToCompleteException { |
| JClassType editorIntf = editorType.getOracle().findType( |
| IsEditor.class.getName()); |
| JClassType[] parameterization = ModelUtils.findParameterizationOf( |
| editorIntf, editorType); |
| if (parameterization != null) { |
| return parameterization[0]; |
| } |
| logger.log(TreeLogger.ERROR, |
| noEditorParameterizationMessage(editorIntf, editorType)); |
| throw new UnableToCompleteException(); |
| } |
| |
| static String cycleErrorMessage(JType editorType, String originalPath, |
| String errorPath) { |
| return String.format( |
| "Cycle detected in editor graph. Editor type %s at path %s can" |
| + " be reached again at path %s", |
| editorType.getQualifiedSourceName(), originalPath, errorPath); |
| } |
| |
| static String foundPrimitiveMessage(JType type, String getterExpression, |
| String path) { |
| return String.format("Found unexpected type %s while evauating path" |
| + " \"%s\" using getter expression \"%s\"", |
| type.getQualifiedSourceName(), path, getterExpression); |
| } |
| |
| static String mustExtendMessage(JType driverType) { |
| return String.format( |
| "You must declare an interface that extends the %s type", |
| driverType.getSimpleSourceName()); |
| } |
| |
| static String noEditorParameterizationMessage(JClassType editorIntf, |
| JClassType type) { |
| return String.format("The type %s is assignable to the raw %s type, but" |
| + " a type parameterization is required.", |
| type.getParameterizedQualifiedSourceName(), editorIntf.getName()); |
| } |
| |
| static String noGetterMessage(String propertyName, JType proxyType) { |
| return String.format( |
| "Could not find a getter for path %s in proxy type %s", propertyName, |
| proxyType.getQualifiedSourceName()); |
| } |
| |
| static String poisonedMessage() { |
| return "Unable to create Editor model due to previous errors"; |
| } |
| |
| static String tooManyInterfacesMessage(JType intf) { |
| return String.format("The type %s extends more than one interface", |
| intf.getQualifiedSourceName()); |
| } |
| |
| static String unexpectedInputTypeMessage(JType driverType, JType intf) { |
| return String.format("Unexpected input type: %s is not assignable from %s", |
| driverType.getQualifiedSourceName(), intf.getQualifiedSourceName()); |
| } |
| |
| private final JGenericType compositeEditorIntf; |
| |
| /** |
| * The structural model. |
| */ |
| private final EditorData[] editorData; |
| /** |
| * A reference to {@link Editor}. |
| */ |
| private final JGenericType editorIntf; |
| |
| private final JClassType editorType; |
| |
| private final EditorData editorSoFar; |
| |
| /** |
| * A reference to {@link IsEditor}. |
| */ |
| private final JGenericType isEditorIntf; |
| |
| private final TreeLogger logger; |
| |
| private final EditorModel parentModel; |
| |
| private boolean poisoned; |
| |
| private final JClassType proxyType; |
| |
| /** |
| * Type-specific data. |
| */ |
| private final Map<JClassType, List<EditorData>> typeData; |
| |
| /** |
| * Constructor to use when starting with an EditorDriver interface. |
| */ |
| public EditorModel(TreeLogger logger, JClassType intf, JClassType driverType) |
| throws UnableToCompleteException { |
| assert logger != null : "logger was null"; |
| assert intf != null : "intf was null"; |
| assert driverType != null : "driver was null"; |
| |
| editorSoFar = null; |
| this.logger = logger.branch(TreeLogger.DEBUG, "Creating Editor model for " |
| + intf.getQualifiedSourceName()); |
| parentModel = null; |
| typeData = new HashMap<JClassType, List<EditorData>>(); |
| |
| if (!driverType.isAssignableFrom(intf)) { |
| die(unexpectedInputTypeMessage(driverType, intf)); |
| } else if (intf.equals(driverType)) { |
| die(mustExtendMessage(driverType)); |
| } |
| |
| TypeOracle oracle = intf.getOracle(); |
| editorIntf = oracle.findType(Editor.class.getName()).isGenericType(); |
| assert editorIntf != null : "No Editor type"; |
| isEditorIntf = oracle.findType(IsEditor.class.getName()).isGenericType(); |
| assert isEditorIntf != null : "No IsEditor type"; |
| compositeEditorIntf = oracle.findType(CompositeEditor.class.getName()).isGenericType(); |
| assert compositeEditorIntf != null : "No CompositeEditor type"; |
| |
| JClassType[] interfaces = intf.getImplementedInterfaces(); |
| if (interfaces.length != 1) { |
| die(tooManyInterfacesMessage(intf)); |
| } |
| |
| JClassType[] parameters = ModelUtils.findParameterizationOf(driverType, |
| intf); |
| assert parameters.length == 2 : "Unexpected number of type parameters"; |
| proxyType = parameters[0]; |
| editorType = parameters[1]; |
| editorData = calculateEditorData(); |
| |
| if (poisoned) { |
| die(poisonedMessage()); |
| } |
| } |
| |
| private EditorModel(EditorModel parent, JClassType editorType, |
| EditorData subEditor, JClassType proxyType) |
| throws UnableToCompleteException { |
| logger = parent.logger.branch(TreeLogger.DEBUG, "Descending into " |
| + subEditor.getPath()); |
| this.compositeEditorIntf = parent.compositeEditorIntf; |
| this.editorIntf = parent.editorIntf; |
| this.editorType = editorType; |
| this.editorSoFar = subEditor; |
| this.isEditorIntf = parent.isEditorIntf; |
| this.parentModel = parent; |
| this.proxyType = proxyType; |
| this.typeData = parent.typeData; |
| |
| editorData = calculateEditorData(); |
| } |
| |
| public EditorData[] getEditorData() { |
| return editorData; |
| } |
| |
| /** |
| * Guaranteed to never return null. |
| */ |
| public EditorData[] getEditorData(JClassType editor) { |
| List<EditorData> toReturn = typeData.get(editor); |
| if (toReturn == null) { |
| return EMPTY_EDITOR_DATA; |
| } |
| return toReturn.toArray(new EditorData[toReturn.size()]); |
| } |
| |
| public JClassType getEditorType() { |
| return editorType; |
| } |
| |
| public JClassType getProxyType() { |
| return proxyType; |
| } |
| |
| public EditorData getRootData() throws UnableToCompleteException { |
| TreeLogger rootLogger = logger.branch(TreeLogger.DEBUG, |
| "Calculating root data for " |
| + getEditorType().getParameterizedQualifiedSourceName()); |
| return new EditorData.Builder(rootLogger).access( |
| EditorAccess.root(getEditorType())).build(); |
| } |
| |
| private void accumulateEditorData(List<EditorData> data, |
| List<EditorData> flatData, List<EditorData> allData) |
| throws UnableToCompleteException { |
| flatData.addAll(data); |
| allData.addAll(data); |
| for (EditorData d : data) { |
| descendIntoSubEditor(allData, d); |
| } |
| } |
| |
| /** |
| * Create the EditorData objects for the {@link #editorData} type. |
| * Essentially, the point of this method is to calculate the paths of all |
| * Editor types referenced by {@link #editorType}. |
| */ |
| private EditorData[] calculateEditorData() throws UnableToCompleteException { |
| List<EditorData> flatData = new ArrayList<EditorData>(); |
| List<EditorData> toReturn = new ArrayList<EditorData>(); |
| |
| for (JClassType type : editorType.getFlattenedSupertypeHierarchy()) { |
| for (JField field : type.getFields()) { |
| if (field.isPrivate() || field.isStatic() |
| || field.getAnnotation(Editor.Ignore.class) != null) { |
| continue; |
| } |
| JType fieldClassType = field.getType(); |
| if (shouldExamine(fieldClassType)) { |
| List<EditorData> data = createEditorData(EditorAccess.via(field)); |
| accumulateEditorData(data, flatData, toReturn); |
| } |
| } |
| for (JMethod method : type.getMethods()) { |
| if (method.isPrivate() || method.isStatic() |
| || method.getAnnotation(Editor.Ignore.class) != null) { |
| continue; |
| } |
| JType methodReturnType = method.getReturnType(); |
| if (shouldExamine(methodReturnType) |
| && method.getParameters().length == 0) { |
| EditorAccess access = EditorAccess.via(method); |
| if (access.getPath().equals("as") |
| && isEditorIntf.isAssignableFrom(editorType)) { |
| // Ignore IsEditor.asEditor() |
| continue; |
| } else if (access.getPath().equals("createEditorForTraversal") |
| && compositeEditorIntf.isAssignableFrom(editorType)) { |
| // Ignore CompositeEditor.createEditorForTraversal(); |
| continue; |
| } |
| List<EditorData> data = createEditorData(access); |
| accumulateEditorData(data, flatData, toReturn); |
| } |
| } |
| type = type.getSuperclass(); |
| } |
| |
| if (compositeEditorIntf.isAssignableFrom(editorType)) { |
| JClassType subEditorType = calculateCompositeTypes(editorType)[1]; |
| EditorAccess access = EditorAccess.root(subEditorType); |
| EditorData subEditor = new EditorData.Builder(logger).access(access).parent( |
| editorSoFar).build(); |
| List<EditorData> accumulator = new ArrayList<EditorData>(); |
| descendIntoSubEditor(accumulator, subEditor); |
| |
| /* |
| * It's necessary to generate a sub-Model here so that any Editor types |
| * reachable only through the composite type will be added to the types |
| * map. The path data isn't actually useful, since we rely on |
| * CompositeEditor.getPathElement() at runtime. |
| */ |
| EditorModel subModel = new EditorModel(this, subEditor.getEditorType(), |
| subEditor, subEditor.getEditedType()); |
| poisoned |= subModel.poisoned; |
| } |
| |
| if (!typeData.containsKey(editorType)) { |
| typeData.put(editorType, flatData); |
| } |
| |
| return toReturn.toArray(new EditorData[toReturn.size()]); |
| } |
| |
| private String camelCase(String prefix, String name) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(prefix).append(Character.toUpperCase(name.charAt(0))).append( |
| name, 1, name.length()); |
| return sb.toString(); |
| } |
| |
| private List<EditorData> createEditorData(EditorAccess access) |
| throws UnableToCompleteException { |
| TreeLogger subLogger = logger.branch(TreeLogger.DEBUG, "Examining " |
| + access.toString()); |
| |
| List<EditorData> toReturn = new ArrayList<EditorData>(); |
| |
| // Are we looking at a view that implements IsEditor? |
| if (access.isEditor()) { |
| EditorAccess subAccess = EditorAccess.via(access, |
| calculateIsEditedType(subLogger, access.getEditorType())); |
| toReturn = createEditorData(subAccess); |
| |
| // If an object only implements IsEditor, return now |
| if (!editorIntf.isAssignableFrom(access.getEditorType())) { |
| return toReturn; |
| } |
| } |
| |
| // Determine the Foo in Editor<Foo> |
| JClassType expectedToEdit = calculateEditedType(subLogger, |
| access.getEditorType()); |
| |
| EditorData.Builder builder = new EditorData.Builder(subLogger); |
| builder.access(access); |
| builder.parent(editorSoFar); |
| |
| // Find the bean methods on the proxy interface |
| findBeanPropertyMethods(access.getPath(), expectedToEdit, builder); |
| |
| toReturn.add(builder.build()); |
| return toReturn; |
| } |
| |
| /** |
| * @param accumulator |
| * @param data |
| * @throws UnableToCompleteException |
| */ |
| private void descendIntoSubEditor(List<EditorData> accumulator, |
| EditorData data) throws UnableToCompleteException { |
| EditorModel superModel = parentModel; |
| while (superModel != null) { |
| if (superModel.editorType.isAssignableFrom(data.getEditorType()) |
| || data.getEditorType().isAssignableFrom(superModel.editorType)) { |
| poison(cycleErrorMessage(data.getEditorType(), superModel.getPath(), |
| data.getPath())); |
| return; |
| } |
| superModel = superModel.parentModel; |
| } |
| |
| if (data.isDelegateRequired()) { |
| EditorModel subModel = new EditorModel(this, data.getEditorType(), data, |
| calculateEditedType(logger, data.getEditorType())); |
| accumulator.addAll(accumulator.indexOf(data) + 1, |
| Arrays.asList(subModel.getEditorData())); |
| poisoned |= subModel.poisoned; |
| } |
| } |
| |
| private void die(String message) throws UnableToCompleteException { |
| logger.log(TreeLogger.ERROR, message); |
| throw new UnableToCompleteException(); |
| } |
| |
| /** |
| * Traverses a path to create expressions to access the getter and setter. |
| * <p> |
| * This method returns a three-element string array containing the |
| * interstitial getter expression specified by the path, the name of the |
| * getter method, and the name of the setter method. For example, the input |
| * <code>foo.bar.baz</code> might return |
| * <code>{ ".getFoo().getBar()", "getBaz", "setBaz" }</code>. |
| */ |
| private void findBeanPropertyMethods(String path, JClassType propertyType, |
| EditorData.Builder builder) { |
| StringBuilder interstitialGetters = new StringBuilder(); |
| StringBuilder interstitialGuard = new StringBuilder("true"); |
| String[] parts = path.split(Pattern.quote(".")); |
| String setterName = null; |
| |
| JClassType lookingAt = proxyType; |
| part : for (int i = 0, j = parts.length; i < j; i++) { |
| if (parts[i].length() == 0) { |
| continue; |
| } |
| String getterName = camelCase("get", parts[i]); |
| |
| for (JClassType search : lookingAt.getFlattenedSupertypeHierarchy()) { |
| // If looking at the last element of the path, also look for a setter |
| if (i == j - 1 && setterName == null) { |
| for (JMethod maybeSetter : search.getOverloads(camelCase("set", |
| parts[i]))) { |
| if (maybeSetter.getReturnType().equals(JPrimitiveType.VOID) |
| && maybeSetter.getParameters().length == 1 |
| && maybeSetter.getParameters()[0].getType().isClassOrInterface() != null |
| && maybeSetter.getParameters()[0].getType().isClassOrInterface().isAssignableFrom( |
| propertyType)) { |
| setterName = maybeSetter.getName(); |
| break; |
| } |
| } |
| } |
| |
| JMethod getter = search.findMethod(getterName, new JType[0]); |
| if (getter != null) { |
| JType returnType = getter.getReturnType(); |
| lookingAt = returnType.isClassOrInterface(); |
| if (lookingAt == null) { |
| poison(foundPrimitiveMessage(returnType, |
| interstitialGetters.toString(), path)); |
| return; |
| } |
| interstitialGetters.append(".").append(getterName).append("()"); |
| interstitialGuard.append(" && %1$s").append(interstitialGetters).append( |
| " != null"); |
| builder.propertyOwnerType(search); |
| continue part; |
| } |
| } |
| poison(noGetterMessage(path, proxyType)); |
| return; |
| } |
| |
| int idx = interstitialGetters.lastIndexOf("."); |
| builder.beanOwnerExpression(idx <= 0 ? "" : interstitialGetters.substring( |
| 0, idx)); |
| if (parts.length > 1) { |
| // Strip after last && since null is a valid value |
| interstitialGuard.delete(interstitialGuard.lastIndexOf(" &&"), |
| interstitialGuard.length()); |
| builder.beanOwnerGuard(interstitialGuard.substring(8)); |
| } |
| if (interstitialGetters.length() > 0) { |
| builder.getterExpression("." |
| + interstitialGetters.substring(idx + 1, |
| interstitialGetters.length() - 2) + "()"); |
| } else { |
| builder.getterExpression(""); |
| } |
| builder.setterName(setterName); |
| } |
| |
| private String getPath() { |
| if (editorSoFar != null) { |
| return editorSoFar.getPath(); |
| } else { |
| return "<Root Object>"; |
| } |
| } |
| |
| /** |
| * Record an error that is not immediately fatal. |
| */ |
| private void poison(String message) { |
| logger.log(TreeLogger.ERROR, message); |
| poisoned = true; |
| } |
| |
| /** |
| * Returns <code>true</code> if the given type participates in Editor |
| * hierarchies. |
| */ |
| private boolean shouldExamine(JType type) { |
| JClassType classType = type.isClassOrInterface(); |
| if (classType == null) { |
| return false; |
| } |
| return editorIntf.isAssignableFrom(classType) |
| || isEditorIntf.isAssignableFrom(classType); |
| } |
| } |