/* * Smart GWT (GWT for SmartClient) * Copyright 2008 and beyond, Isomorphic Software, Inc. * * Smart GWT is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 * is published by the Free Software Foundation. Smart GWT is also * available under typical commercial license terms - see * http://smartclient.com/license * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package com.smartgwt.rebind; import com.google.gwt.core.ext.Generator; import com.google.gwt.core.ext.GeneratorContext; 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.JType; import com.google.gwt.core.ext.typeinfo.JMethod; import com.google.gwt.core.ext.typeinfo.JConstructor; import com.google.gwt.core.ext.typeinfo.JPrimitiveType; import com.google.gwt.core.ext.typeinfo.NotFoundException; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; import com.google.gwt.user.rebind.SourceWriter; import com.smartgwt.rebind.BeanProperty; import com.smartgwt.rebind.BeanMethod; import com.smartgwt.rebind.BeanValueType; import com.smartgwt.client.widgets.BaseWidget; import com.smartgwt.client.core.DataClass; import com.smartgwt.client.bean.BeanFactory; import com.smartgwt.client.bean.BeanFactoryForBaseWidget; import com.smartgwt.client.bean.BeanFactoryForDataClass; import java.util.Set; import java.util.List; import java.util.Arrays; import java.util.LinkedList; import java.util.HashSet; import java.util.HashMap; import java.util.TreeMap; import java.util.Map; import java.util.Iterator; import java.util.Map.Entry; import java.util.Collection; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.io.PrintWriter; // Class which represents a BeanClass for which a factory is being generated. // This is separated from the actual generator because we sometimes want to // generate more than one of these (i.e. for superclasses). public class BeanClass { private JClassType beanClassType; private TypeOracle typeOracle; private BeanClass superclass; private JClassType factoryClass; private SourceWriter source; private boolean hasStaticInitMethod; private Map<String, BeanProperty> properties; // A global list of our methods ... we write them out as JSNI functions, // so that properties can refer to them (and call them) by index. private LinkedList<BeanMethod> methods; // A set of property names which we don't want to include in the factory private static final Set<String> excludedPropertyNames = new HashSet<String> ( Arrays.asList( "logicalStructure", "orCreateJsObj", // containerWidget causes problems in FormItem because there is a // getter but no setter, and the process for creating FormItems // sometimes calls the setter. By excluding it here, we fall // through to setting a JavaScript attribute, which is what we // want. The alternative would be to *generally* fall through // to setting a JavaScript attribute in situations where there // is a getter but no setter. Or, doing so in particular cases // that we would identify somehow. "containerWidget" ) ); public BeanClass (JClassType beanClassType) { this.beanClassType = beanClassType; this.typeOracle = beanClassType.getOracle(); final JClassType baseWidgetType = typeOracle.findType(BaseWidget.class.getCanonicalName()); final JClassType dataClassType = typeOracle.findType(DataClass.class.getCanonicalName()); if (beanClassType.isAssignableTo(baseWidgetType)) { this.factoryClass = typeOracle.findType(BeanFactoryForBaseWidget.class.getCanonicalName()); } else if (beanClassType.isAssignableTo(dataClassType)) { this.factoryClass = typeOracle.findType(BeanFactoryForDataClass.class.getCanonicalName()); } // We'll look for superclasses up to BaseWidget or DataClass, and make sure that we // generate factories for them as well. That way, we can keep track of // just our own properties. if (beanClassType != baseWidgetType && beanClassType != dataClassType) { JClassType beanSuperclassType = this.beanClassType.getSuperclass(); if (beanSuperclassType != null) { this.superclass = new BeanClass(beanSuperclassType); } } // We use a TreeMap to sort the properties ... not really necessary, // but it's nice for debugging! properties = new TreeMap<String, BeanProperty>(); methods = new LinkedList<BeanMethod>(); // Iterate through our methods, and create properties from them for (JMethod method : beanClassType.getMethods()) { BeanMethod beanMethod = new BeanMethod(method, typeOracle); String propertyName = beanMethod.getName(); if (beanMethod.isStaticInitMethod()) hasStaticInitMethod = true; if (!excludedPropertyNames.contains(propertyName)) { if (beanMethod.isGetter() || beanMethod.isSetter()) { BeanProperty property = properties.get(propertyName); if (property == null) { property = new BeanProperty(propertyName, this); properties.put(propertyName, property); } // This will call back to add the method to our global list as well property.addMethod(beanMethod); } } } // Now that we have our properties, iterate through them and make a few // adjustments We create a list for new properties, since we can't // modify the properties HashMap while iterating through it. LinkedList<BeanProperty> additionalProperties = new LinkedList<BeanProperty>(); for (BeanProperty property : properties.values()) { // Check if a superclass has the same property. If so, copy its // methods here if they are not overridden. This simplifies the // run-time logic -- if the property is here, we can just use it, // and if it's not, we check the superclasses. But we don't have to // consolidate methods at run-time. // // Essentially, this deals with the case where a subclass overrides // one overloaded method but not others. BeanProperty superProperty = findPropertyInSuperclass(property.getName()); if (superProperty != null) property.mergeProperty(superProperty); // If the name might be overloaded (e.g. getWidthAsString), then // check whether we have the unoverloaded property, or a superclass // does. If so, merge the overloaded property into the main // property. Note that we don't delete the overloaded property, so // it still can be used separately (though with limited types -- we // don't merge the main property into the overloaded property). if (property.getMaybeOverloadedGetter()) { BeanProperty mainProperty = properties.get(property.getNameWithoutOverload()); if (mainProperty == null) { // If no main property, check superclasses as well. superProperty = findPropertyInSuperclass(property.getNameWithoutOverload()); if (superProperty != null) { // If there is a superclass property, then create a new // property here for the overload and then merge the // superProperty. If there is no superclass property // either, then we just leave things alone. mainProperty = new BeanProperty(property.getNameWithoutOverload(), this); // We need to add the first getter of the super property first, // because that will be the default getter. mainProperty.addMethod(superProperty.firstGetter()); // For the rest, merge the overloaded property first, // so it has priority over the superProperty mainProperty.mergeProperty(property); mainProperty.mergeProperty(superProperty); // Add it to the list of additional properties, since we can't modify the // properties HashMap while we're iterating over it. additionalProperties.add(mainProperty); } } else { // If there already was a mainProperty, then just merge the // overloaded methods mainProperty.mergeProperty(property); } } } // If we created any additionalProperties, add them now that iteration is done for (BeanProperty additionalProperty : additionalProperties) { properties.put(additionalProperty.getName(), additionalProperty); } } // This finds the first superclass with the specified property. We only // need one, because we're consolidating at each level -- it will already // consolidate further superclasses. public BeanProperty findPropertyInSuperclass (String propertyName) { BeanClass iterator = superclass; while (iterator != null) { BeanProperty superProperty = iterator.getProperty(propertyName); if (superProperty != null) return superProperty; iterator = iterator.superclass; } return null; } public BeanProperty getProperty (String propertyName) { return properties.get(propertyName); } public void addMethod (BeanMethod method) { // This would be more efficient if methods were a set, but we need // the indexOf method, which the Set variants don't seem to have. if (methods.contains(method)) return; methods.add(method); } public void removeMethod (BeanMethod method) { methods.remove(method); } public int indexOfMethod (BeanMethod method) { return methods.indexOf(method); } public String generateFactory (TreeLogger logger, GeneratorContext context) throws UnableToCompleteException { // Must extend BaseWidget or DataCLass if (factoryClass == null) { logger.log(TreeLogger.ERROR, "Cannot generate a BeanFactory for " + beanClassType.getQualifiedSourceName() + " because it does not extend BaseWidget or DataClass" ); throw new UnableToCompleteException(); } // We only generate factories for classes that have a no-arg constructor JConstructor constructor = beanClassType.findConstructor(new JType[]{}); if (constructor == null) { logger.log(TreeLogger.ERROR, "Cannot generate a BeanFactory for " + beanClassType.getQualifiedSourceName() + " because it does not have a no-arg constructor" ); throw new UnableToCompleteException(); } final String packageName = beanClassType.getPackage().getName(); final String factoryName = getSimpleFactoryName(); ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, factoryName); // We use class literals so that we fail early if the class isn't found composer.addImport("com.smartgwt.client.bean.types.*"); composer.addImport(com.smartgwt.client.bean.BeanFactory.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanFactoryForBaseWidget.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanFactoryForDataClass.class.getCanonicalName()); composer.addImport(com.google.gwt.core.client.JavaScriptObject.class.getCanonicalName()); composer.addImport(com.google.gwt.core.client.JsArray.class.getCanonicalName()); composer.addImport(com.google.gwt.core.client.UnsafeNativeLong.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanProperty1Getter1Setter.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanProperty1Getter.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanProperty1Setter.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanPropertyMultiple.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanProperty.class.getCanonicalName()); composer.addImport(com.smartgwt.client.bean.BeanMethod.class.getCanonicalName()); // Import our beanClass ... makes the generated code prettier ... composer.addImport(getQualifiedBeanClassName()); // We inherit from just one class -- we will have a reference to an object for the // superclass, rather than it being our superclass. composer.setSuperclass(factoryClass.getSimpleSourceName() + "<" + beanClassType.getSimpleSourceName() + ">"); PrintWriter printWriter = context.tryCreate(logger, packageName, factoryName); if (printWriter != null) { source = composer.createSourceWriter(context, printWriter); source.println("// This class lovingly generated by com.smartgwt.rebind.BeanFactoryGenerator"); writeBlankLine(); writeStaticCreate(); writeBlankLine(); writeGetBeanClass(); writeBlankLine(); if (superclass == null) { source.println("// No relevant superclass, so not generating getSuperclassName or createSuperclassFactory"); writeBlankLine(); } else { writeGetSuperclass(); writeBlankLine(); writeCreateSuperclassFactory(); writeBlankLine(); } if (hasStaticInitMethod) { writeTriggerStaticInitializers(); writeBlankLine(); } writeDoNewInstance(); writeBlankLine(); writeMetadata(); writeRegisterValueTypes(logger, context); writeBlankLine(); writeGetPropertiesAndGetMethods(); source.commit(logger); // Note that this won't do much if the factory has already been // generated, which will quite often be the case. Also, note that // we only need to generate our immediate superclass, since it will // in turn generate its superclass. if (superclass != null) { superclass.generateFactory(logger, context); } } return composer.getCreatedClassName(); } public String getSimpleBeanClassName () { return beanClassType.getSimpleSourceName(); } public String getQualifiedBeanClassName () { return beanClassType.getParameterizedQualifiedSourceName(); } public String getSimpleFactoryName () { StringBuilder builder = new StringBuilder(); JClassType iterator = beanClassType; while (iterator != null) { if (iterator != beanClassType) builder.insert(0, "_"); builder.insert(0, iterator.getSimpleSourceName()); iterator = iterator.getEnclosingType(); } builder.append("BeanFactory"); return builder.toString(); } public String getQualifiedFactoryName () { return beanClassType.getPackage().getName() + "." + getSimpleFactoryName(); } private void writeBlankLine () { source.println(""); } private void writeStaticCreate () { // We check whether a BeanFactory has already been registered for our // beanClass. This can happen if GWT.create is called first on a // subclass of our beanClass and then later on our beanClass as well // (since the subclass will have generated factories for its // superclasses). // // If a factory has already been registered, we can leave this one // uninitialized. We don't do anything with the result of the // GWT.create, so there's no need to actually initialize it. source.println("public static BeanFactory create (boolean forSuperclass) {"); source.indent(); source.println("BeanFactory factory = BeanFactory.getFactory(" + getSimpleBeanClassName() + ".class);"); source.println("if (factory == null) factory = new " + getSimpleFactoryName() + "();"); // We register our class name with SGWTFactory only if we're not for a superclass source.println("if (!forSuperclass) factory.registerClassNameWithSGWTFactory();"); source.println("return factory;"); source.outdent(); source.println("}"); } private void writeGetBeanClass () { String beanClassName = getSimpleBeanClassName(); source.println("@Override public Class<" + beanClassName + "> getBeanClass () {"); source.indent(); source.println("return " + beanClassName + ".class;"); source.outdent(); source.println("}"); } private void writeMetadata () { if (beanClassType.isAnnotationPresent(BeanFactory.FrameworkClass.class)) { source.println("@Override public boolean isFrameworkClass () {"); source.indent(); source.println("return true;"); source.outdent(); source.println("}"); writeBlankLine(); } BeanFactory.ScClassName scClass = beanClassType.getAnnotation(BeanFactory.ScClassName.class); if (scClass != null) { String className = scClass.value(); if (className != null && !className.equals("")) { source.println("@Override public String getDefaultScClassName () {"); source.indent(); source.println("return \"" + className + "\";"); source.outdent(); source.println("}"); writeBlankLine(); } } } private void writeGetPropertiesAndGetMethods () { String beanClassName = getSimpleBeanClassName(); source.println("@Override protected BeanProperty<" + beanClassName + ">[] getProperties (JsArray<JavaScriptObject> methods) {"); source.indent(); source.println("// Note that we cast to the generic array type, since you can't create generic array types directly."); source.println("return (BeanProperty<" + beanClassName + ">[]) new BeanProperty[] {"); source.indent(); Iterator<BeanProperty> prop = properties.values().iterator(); while (prop.hasNext()) { BeanProperty property = prop.next(); property.writeConstructor(source, prop.hasNext()); } source.outdent(); source.println("};"); source.outdent(); source.println("}"); writeBlankLine(); source.println("// Any native longs are safe because we're not actually manipulating them in Javascript"); source.println("// We will reference some deprecated methods ... we could exclude them if desired"); source.println("@Override @UnsafeNativeLong @SuppressWarnings(\"deprecation\")"); source.println("protected native JsArray<JavaScriptObject> getMethods () /*-{"); source.indent(); source.println("return ["); source.indent(); Iterator<BeanMethod> i = methods.iterator(); while (i.hasNext()) { BeanMethod method = i.next(); method.writeJSNIFunction(source, i.hasNext()); } source.outdent(); source.println("];"); source.outdent(); source.println("}-*/;"); } public Collection<BeanValueType> getValueTypes () { HashMap<JType, BeanValueType> result = new HashMap<JType, BeanValueType>(); for (BeanMethod method : methods) { JType type = method.getValueType(); if (!result.containsKey(type)) { result.put(type, new BeanValueType(type, typeOracle)); } } return result.values(); } private void writeRegisterValueTypes (TreeLogger logger, GeneratorContext context) { source.println("@Override protected void registerValueTypes () {"); source.indent(); for (BeanValueType valueType : getValueTypes()) { // Create the types ... the constructor won't register more than one valueType.writeRegisterValueType(source, logger, context); } source.outdent(); source.println("}"); } private void writeGetSuperclass () { if (superclass == null) return; source.println("@Override protected Class getSuperclass () {"); source.indent(); source.println("return " + superclass.getQualifiedBeanClassName() + ".class" + ";"); source.outdent(); source.println("}"); } private void writeCreateSuperclassFactory () { if (superclass == null) return; source.println("@Override protected BeanFactory<?> createSuperclassFactory () {"); source.indent(); source.println("return " + superclass.getQualifiedFactoryName() + ".create(true);"); source.outdent(); source.println("}"); } private void writeTriggerStaticInitializers () { source.println("// Trigger static initialization before creating new instance, in order to avoid bug"); source.println("@Override protected void triggerStaticInitializers () {"); source.indent(); source.println(getSimpleBeanClassName() + ".beanFactoryInit();"); source.outdent(); source.println("}"); } private void writeDoNewInstance() { source.println("@Override protected " + getSimpleBeanClassName() + " doNewInstance() {"); source.indent(); if (beanClassType.isAbstract()) { source.println("// The base class is abstract, so we'll return null."); source.println("return null;"); } else { source.println("return new " + getSimpleBeanClassName() + "();"); } source.outdent(); source.println("}"); } }