diff options
Diffstat (limited to 'client-compiler/src/main/java/com')
23 files changed, 4448 insertions, 0 deletions
diff --git a/client-compiler/src/main/java/com/vaadin/sass/linker/SassLinker.java b/client-compiler/src/main/java/com/vaadin/sass/linker/SassLinker.java new file mode 100644 index 0000000000..dda6733384 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/sass/linker/SassLinker.java @@ -0,0 +1,233 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.sass.linker; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.w3c.css.sac.CSSException; + +import com.google.gwt.core.ext.LinkerContext; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.linker.AbstractLinker; +import com.google.gwt.core.ext.linker.ArtifactSet; +import com.google.gwt.core.ext.linker.EmittedArtifact; +import com.google.gwt.core.ext.linker.LinkerOrder; +import com.google.gwt.core.ext.linker.LinkerOrder.Order; +import com.google.gwt.core.ext.linker.Shardable; +import com.vaadin.sass.internal.ScssStylesheet; + +/** + * Pre-linker that checks for the existence of SASS files in public folders, + * compiles them to CSS files with the SassCompiler from Vaadin and adds the CSS + * back into the artifact. + * + */ +@LinkerOrder(Order.PRE) +@Shardable +public class SassLinker extends AbstractLinker { + + @Override + public String getDescription() { + return "Compiling SCSS files in public folders to standard CSS"; + } + + @Override + public ArtifactSet link(TreeLogger logger, LinkerContext context, + ArtifactSet artifacts, boolean onePermutation) + throws UnableToCompleteException { + + if (!onePermutation) { + // The artifact to return + ArtifactSet toReturn = new ArtifactSet(artifacts); + + // The temporary scss files provided from the artefacts + List<FileInfo> scssFiles = new ArrayList<FileInfo>(); + + // The public files are provided as inputstream, but the compiler + // needs real files, as they can contain references to other + // files. They will be stored here, with their relative paths intact + String tempFolderName = new Date().getTime() + File.separator; + File tempFolder = createTempDir(tempFolderName); + + // Can't search here specifically for public resources, as the type + // is different during compilation. This means we have to loop + // through all the artifacts + for (EmittedArtifact resource : artifacts + .find(EmittedArtifact.class)) { + + // Create the temporary files. + String partialPath = resource.getPartialPath(); + if (partialPath.endsWith(".scss")) { + // In my opinion, the SCSS file does not need to be + // output to the web content folder, as they can't + // be used there + toReturn.remove(resource); + + String fileName = partialPath; + File path = tempFolder; + + int separatorIndex = fileName.lastIndexOf(File.separator); + if (-1 != separatorIndex) { + fileName = fileName.substring(separatorIndex + 1); + + String filePath = partialPath.substring(0, + separatorIndex); + path = createTempDir(tempFolderName + filePath); + } + + File tempfile = new File(path, fileName); + try { + boolean fileCreated = tempfile.createNewFile(); + if (fileCreated) { + + // write the received inputstream to the temp file + writeFromInputStream(resource.getContents(logger), + tempfile); + + // Store the file info for the compilation + scssFiles.add(new FileInfo(tempfile, partialPath)); + } else { + logger.log(TreeLogger.WARN, "Duplicate file " + + tempfile.getPath()); + } + } catch (IOException e) { + logger.log(TreeLogger.ERROR, + "Could not write temporary file " + fileName, e); + } + } + } + + // Compile the files and store them in the artifact + logger.log(TreeLogger.INFO, "Processing " + scssFiles.size() + + " Sass file(s)"); + for (FileInfo fileInfo : scssFiles) { + logger.log(TreeLogger.INFO, " " + fileInfo.originalScssPath + + " -> " + fileInfo.getOriginalCssPath()); + + try { + ScssStylesheet scss = ScssStylesheet.get(fileInfo + .getAbsolutePath()); + if (!fileInfo.isMixin()) { + scss.compile(); + InputStream is = new ByteArrayInputStream(scss + .printState().getBytes()); + + toReturn.add(this.emitInputStream(logger, is, + fileInfo.getOriginalCssPath())); + } + + fileInfo.getFile().delete(); + } catch (CSSException e) { + logger.log(TreeLogger.ERROR, "SCSS compilation failed for " + + fileInfo.getOriginalCssPath(), e); + } catch (IOException e) { + logger.log( + TreeLogger.ERROR, + "Could not write CSS file for " + + fileInfo.getOriginalCssPath(), e); + } catch (Exception e) { + logger.log(TreeLogger.ERROR, "SCSS compilation failed for " + + fileInfo.getOriginalCssPath(), e); + } + } + + return toReturn; + } + + return artifacts; + } + + /** + * Writes the contents of an InputStream out to a file. + * + * @param contents + * @param tempfile + * @throws IOException + */ + private void writeFromInputStream(InputStream contents, File tempfile) + throws IOException { + // write the inputStream to a FileOutputStream + OutputStream out = new FileOutputStream(tempfile); + + int read = 0; + byte[] bytes = new byte[1024]; + + while ((read = contents.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + + contents.close(); + out.flush(); + out.close(); + } + + /** + * Create folder in temporary space on disk. + * + * @param partialPath + * @return + */ + private File createTempDir(String partialPath) { + String baseTempPath = System.getProperty("java.io.tmpdir"); + + File tempDir = new File(baseTempPath + File.separator + partialPath); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + + tempDir.deleteOnExit(); + return tempDir; + } + + /** + * Temporal storage for file info from Artifact. + */ + private class FileInfo { + private String originalScssPath; + private File file; + + public FileInfo(File file, String originalScssPath) { + this.file = file; + this.originalScssPath = originalScssPath; + } + + public boolean isMixin() { + return file.getName().startsWith("_"); + } + + public String getAbsolutePath() { + return file.getAbsolutePath(); + } + + public String getOriginalCssPath() { + return originalScssPath.substring(0, originalScssPath.length() - 5) + + ".css"; + } + + public File getFile() { + return file; + } + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/AcceptCriteriaFactoryGenerator.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/AcceptCriteriaFactoryGenerator.java new file mode 100644 index 0000000000..2d08329e9a --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/AcceptCriteriaFactoryGenerator.java @@ -0,0 +1,143 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Date; + +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.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; +import com.google.gwt.user.rebind.SourceWriter; +import com.vaadin.client.ui.dd.VAcceptCriterion; +import com.vaadin.client.ui.dd.VAcceptCriterionFactory; +import com.vaadin.server.widgetsetutils.metadata.ConnectorBundle; +import com.vaadin.shared.ui.dd.AcceptCriterion; + +/** + * GWT generator to build {@link VAcceptCriterionFactory} implementation + * dynamically based on {@link AcceptCriterion} annotations available in + * classpath. + * + */ +public class AcceptCriteriaFactoryGenerator extends Generator { + + private String packageName; + private String className; + + @Override + public String generate(TreeLogger logger, GeneratorContext context, + String typeName) throws UnableToCompleteException { + + try { + TypeOracle typeOracle = context.getTypeOracle(); + + // get classType and save instance variables + JClassType classType = typeOracle.getType(typeName); + packageName = classType.getPackage().getName(); + className = classType.getSimpleSourceName() + "Impl"; + // Generate class source code + generateClass(logger, context); + } catch (Exception e) { + logger.log(TreeLogger.ERROR, + "Accept criterion factory creation failed", e); + } + // return the fully qualifed name of the class generated + return packageName + "." + className; + } + + /** + * Generate source code for WidgetMapImpl + * + * @param logger + * Logger object + * @param context + * Generator context + */ + private void generateClass(TreeLogger logger, GeneratorContext context) { + // get print writer that receives the source code + PrintWriter printWriter = null; + printWriter = context.tryCreate(logger, packageName, className); + // print writer if null, source code has ALREADY been generated, + // return (WidgetMap is equal to all permutations atm) + if (printWriter == null) { + return; + } + logger.log(Type.INFO, "Detecting available criteria ..."); + Date date = new Date(); + + // init composer, set class properties, create source writer + ClassSourceFileComposerFactory composer = null; + composer = new ClassSourceFileComposerFactory(packageName, className); + composer.addImport("com.google.gwt.core.client.GWT"); + composer.setSuperclass("com.vaadin.client.ui.dd.VAcceptCriterionFactory"); + SourceWriter sourceWriter = composer.createSourceWriter(context, + printWriter); + + // generator constructor source code + generateInstantiatorMethod(sourceWriter, context, logger); + // close generated class + sourceWriter.outdent(); + sourceWriter.println("}"); + // commit generated class + context.commit(logger, printWriter); + logger.log(Type.INFO, + "Done. (" + (new Date().getTime() - date.getTime()) / 1000 + + "seconds)"); + + } + + private void generateInstantiatorMethod(SourceWriter sourceWriter, + GeneratorContext context, TreeLogger logger) { + + sourceWriter.println("public VAcceptCriterion get(String name) {"); + sourceWriter.indent(); + + sourceWriter.println("name = name.intern();"); + + JClassType criteriaType = context.getTypeOracle().findType( + VAcceptCriterion.class.getName()); + JClassType[] subtypes = criteriaType.getSubtypes(); + Arrays.sort(subtypes, ConnectorBundle.jClassComparator); + for (JClassType clientClass : subtypes) { + AcceptCriterion annotation = clientClass + .getAnnotation(AcceptCriterion.class); + if (annotation != null) { + String clientClassName = clientClass.getQualifiedSourceName(); + Class<?> serverClass = clientClass.getAnnotation( + AcceptCriterion.class).value(); + String serverClassName = serverClass.getCanonicalName(); + logger.log(Type.INFO, "creating mapping for " + serverClassName); + sourceWriter.print("if (\""); + sourceWriter.print(serverClassName); + sourceWriter.print("\" == name) return GWT.create("); + sourceWriter.print(clientClassName); + sourceWriter.println(".class );"); + sourceWriter.print("else "); + } + } + + sourceWriter.println("return null;"); + sourceWriter.outdent(); + sourceWriter.println("}"); + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java new file mode 100644 index 0000000000..2b8ccc87d0 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java @@ -0,0 +1,1306 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.RunAsyncCallback; +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.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +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.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.vaadin.client.JsArrayObject; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.communication.JsonDecoder; +import com.vaadin.client.metadata.ConnectorBundleLoader; +import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo; +import com.vaadin.client.metadata.InvokationHandler; +import com.vaadin.client.metadata.OnStateChangeMethod; +import com.vaadin.client.metadata.ProxyHandler; +import com.vaadin.client.metadata.TypeData; +import com.vaadin.client.metadata.TypeDataStore; +import com.vaadin.client.metadata.TypeDataStore.MethodAttribute; +import com.vaadin.client.ui.UnknownComponentConnector; +import com.vaadin.server.widgetsetutils.metadata.ClientRpcVisitor; +import com.vaadin.server.widgetsetutils.metadata.ConnectorBundle; +import com.vaadin.server.widgetsetutils.metadata.ConnectorInitVisitor; +import com.vaadin.server.widgetsetutils.metadata.GeneratedSerializer; +import com.vaadin.server.widgetsetutils.metadata.OnStateChangeVisitor; +import com.vaadin.server.widgetsetutils.metadata.Property; +import com.vaadin.server.widgetsetutils.metadata.RendererVisitor; +import com.vaadin.server.widgetsetutils.metadata.ServerRpcVisitor; +import com.vaadin.server.widgetsetutils.metadata.StateInitVisitor; +import com.vaadin.server.widgetsetutils.metadata.TypeVisitor; +import com.vaadin.server.widgetsetutils.metadata.WidgetInitVisitor; +import com.vaadin.shared.annotations.DelegateToWidget; +import com.vaadin.shared.annotations.NoLayout; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.tools.CvalAddonsChecker; +import com.vaadin.tools.CvalChecker; +import com.vaadin.tools.CvalChecker.InvalidCvalException; + +public class ConnectorBundleLoaderFactory extends Generator { + /** + * Special SourceWriter that approximates the number of written bytes to + * support splitting long methods into shorter chunks to avoid hitting the + * 65535 byte limit. + */ + private class SplittingSourceWriter implements SourceWriter { + private final SourceWriter target; + private final String baseName; + private final int splitSize; + private final List<String> methodNames; + + // Seems to be undercounted by about 15% + private int approximateChars = 0; + private int wrapCount = 0; + + public SplittingSourceWriter(SourceWriter target, String baseName, + int splitSize) { + this.target = target; + this.baseName = baseName; + this.splitSize = splitSize; + methodNames = new ArrayList<String>(); + methodNames.add(baseName); + } + + @Override + public void beginJavaDocComment() { + target.beginJavaDocComment(); + addChars(10); + } + + private void addChars(int i) { + approximateChars += i; + } + + private void addChars(String s) { + addChars(s.length()); + } + + private void addChars(String s, Object[] args) { + addChars(String.format(s, args)); + } + + @Override + public void commit(TreeLogger logger) { + target.commit(logger); + } + + @Override + public void endJavaDocComment() { + target.endJavaDocComment(); + addChars(10); + } + + @Override + public void indent() { + target.indent(); + addChars(10); + } + + @Override + public void indentln(String s) { + target.indentln(s); + addChars(s); + } + + @Override + public void indentln(String s, Object... args) { + target.indentln(s, args); + addChars(s, args); + } + + @Override + public void outdent() { + target.outdent(); + } + + @Override + public void print(String s) { + target.print(s); + addChars(s); + } + + @Override + public void print(String s, Object... args) { + target.print(s, args); + addChars(s, args); + } + + @Override + public void println() { + target.println(); + addChars(5); + } + + @Override + public void println(String s) { + target.println(s); + addChars(s); + } + + @Override + public void println(String s, Object... args) { + target.println(s, args); + addChars(s, args); + } + + public void splitIfNeeded() { + splitIfNeeded(false, null); + } + + public void splitIfNeeded(boolean isNative, String params) { + if (approximateChars > splitSize) { + String newMethod = baseName + wrapCount++; + String args = params == null ? "" : params; + if (isNative) { + outdent(); + println("}-*/;"); + // To support fields of type long (#13692) + println("@com.google.gwt.core.client.UnsafeNativeLong"); + println("private native void %s(%s) /*-{", newMethod, args); + } else { + println("%s();", newMethod); + outdent(); + println("}"); + println("private void %s(%s) {", newMethod, args); + } + methodNames.add(newMethod); + indent(); + + approximateChars = 0; + } + } + + public List<String> getMethodNames() { + return Collections.unmodifiableList(methodNames); + } + + } + + private CvalAddonsChecker cvalChecker = new CvalAddonsChecker(); + + @Override + public String generate(TreeLogger logger, GeneratorContext context, + String typeName) throws UnableToCompleteException { + TypeOracle typeOracle = context.getTypeOracle(); + + try { + JClassType classType = typeOracle.getType(typeName); + String packageName = classType.getPackage().getName(); + String className = classType.getSimpleSourceName() + "Impl"; + + generateClass(logger, context, packageName, className, typeName); + + return packageName + "." + className; + } catch (UnableToCompleteException e) { + // Just rethrow + throw e; + } catch (Exception e) { + logger.log(Type.ERROR, getClass() + " failed", e); + throw new UnableToCompleteException(); + } + } + + private void generateClass(TreeLogger logger, GeneratorContext context, + String packageName, String className, String requestedType) + throws Exception { + PrintWriter printWriter = context.tryCreate(logger, packageName, + className); + if (printWriter == null) { + return; + } + + List<CValUiInfo> cvalInfos = null; + try { + if (cvalChecker != null) { + cvalInfos = cvalChecker.run(); + // Don't run twice + cvalChecker = null; + } + } catch (InvalidCvalException e) { + System.err.println("\n\n\n\n" + CvalChecker.LINE); + for (String line : e.getMessage().split("\n")) { + System.err.println(line); + } + System.err.println(CvalChecker.LINE + "\n\n\n\n"); + System.exit(1); + throw new UnableToCompleteException(); + } + + List<ConnectorBundle> bundles = buildBundles(logger, + context.getTypeOracle()); + + ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory( + packageName, className); + composer.setSuperclass(requestedType); + + SourceWriter w = composer.createSourceWriter(context, printWriter); + + w.println("public void init() {"); + w.indent(); + + for (ConnectorBundle bundle : bundles) { + detectBadProperties(bundle, logger); + + String name = bundle.getName(); + boolean isEager = name + .equals(ConnectorBundleLoader.EAGER_BUNDLE_NAME); + + w.print("addAsyncBlockLoader(new AsyncBundleLoader(\""); + w.print(escape(name)); + w.print("\", "); + + w.print("new String[] {"); + for (Entry<JClassType, Set<String>> entry : bundle.getIdentifiers() + .entrySet()) { + Set<String> identifiers = entry.getValue(); + for (String id : identifiers) { + w.print("\""); + w.print(escape(id)); + w.print("\","); + } + } + w.println("}) {"); + w.indent(); + + w.print("protected void load(final "); + w.print(TypeDataStore.class.getName()); + w.println(" store) {"); + w.indent(); + + if (!isEager) { + w.print(GWT.class.getName()); + w.print(".runAsync("); + } + + w.println("new %s() {", RunAsyncCallback.class.getName()); + w.indent(); + + w.println("public void onSuccess() {"); + w.indent(); + + w.println("load();"); + w.println("%s.get().setLoaded(getName());", + ConnectorBundleLoader.class.getName()); + + // Close onSuccess method + w.outdent(); + w.println("}"); + + w.println("private void load() {"); + w.indent(); + + String loadNativeJsBundle = "loadJsBundle"; + printBundleData(logger, w, bundle, loadNativeJsBundle); + + // Close load method + w.outdent(); + w.println("}"); + + // Separate method for loading native JS stuff (e.g. callbacks) + String loadNativeJsMethodName = "loadNativeJs"; + // To support fields of type long (#13692) + w.println("@com.google.gwt.core.client.UnsafeNativeLong"); + w.println("private native void %s(%s store) /*-{", + loadNativeJsMethodName, TypeDataStore.class.getName()); + w.indent(); + List<String> jsMethodNames = printJsBundleData(logger, w, bundle, + loadNativeJsMethodName); + + w.outdent(); + w.println("}-*/;"); + + // Call all generated native method inside one Java method to avoid + // refercences inside native methods to each other + w.println("private void %s(%s store) {", loadNativeJsBundle, + TypeDataStore.class.getName()); + w.indent(); + printLoadJsBundleData(w, loadNativeJsBundle, jsMethodNames); + w.outdent(); + w.println("}"); + + // onFailure method declaration starts + w.println("public void onFailure(Throwable reason) {"); + w.indent(); + + w.println("%s.get().setLoadFailure(getName(), reason);", + ConnectorBundleLoader.class.getName()); + + w.outdent(); + w.println("}"); + + // Close new RunAsyncCallback() {} + w.outdent(); + w.print("}"); + + if (isEager) { + w.println(".onSuccess();"); + } else { + w.println(");"); + } + + // Close load method + w.outdent(); + w.println("}"); + + // Close add(new ... + w.outdent(); + w.println("});"); + } + + if (cvalInfos != null && !cvalInfos.isEmpty()) { + w.println("{"); + for (CValUiInfo c : cvalInfos) { + if ("evaluation".equals(c.type)) { + w.println("cvals.add(new CValUiInfo(\"" + c.product + + "\", \"" + c.version + "\", \"" + c.widgetset + + "\", null));"); + } + } + w.println("}"); + } + + w.outdent(); + w.println("}"); + + w.commit(logger); + } + + private void printLoadJsBundleData(SourceWriter w, String methodName, + List<String> methods) { + SplittingSourceWriter writer = new SplittingSourceWriter(w, methodName, + 30000); + + for (String method : methods) { + writer.println("%s(store);", method); + writer.splitIfNeeded(); + } + } + + private void detectBadProperties(ConnectorBundle bundle, TreeLogger logger) + throws UnableToCompleteException { + Map<JClassType, Set<String>> definedProperties = new HashMap<JClassType, Set<String>>(); + + for (Property property : bundle.getNeedsProperty()) { + JClassType beanType = property.getBeanType(); + Set<String> usedPropertyNames = definedProperties.get(beanType); + if (usedPropertyNames == null) { + usedPropertyNames = new HashSet<String>(); + definedProperties.put(beanType, usedPropertyNames); + } + + String name = property.getName(); + if (!usedPropertyNames.add(name)) { + logger.log(Type.ERROR, beanType.getQualifiedSourceName() + + " has multiple properties with the name " + name + + ". This can happen if there are multiple " + + "setters with identical names ignoring case."); + throw new UnableToCompleteException(); + } + if (!property.hasAccessorMethods()) { + logger.log(Type.ERROR, beanType.getQualifiedSourceName() + + " has the property '" + name + + "' without getter defined."); + throw new UnableToCompleteException(); + } + } + } + + private List<String> printJsBundleData(TreeLogger logger, SourceWriter w, + ConnectorBundle bundle, String methodName) { + SplittingSourceWriter writer = new SplittingSourceWriter(w, methodName, + 30000); + Set<Property> needsProperty = bundle.getNeedsProperty(); + for (Property property : needsProperty) { + writer.println("var data = {"); + writer.indent(); + + if (property.getAnnotation(NoLayout.class) != null) { + writer.println("noLayout: 1, "); + } + + writer.println("setter: function(bean, value) {"); + writer.indent(); + property.writeSetterBody(logger, writer, "bean", "value"); + writer.outdent(); + writer.println("},"); + + writer.println("getter: function(bean) {"); + writer.indent(); + property.writeGetterBody(logger, writer, "bean"); + writer.outdent(); + writer.println("}"); + + writer.outdent(); + writer.println("};"); + + // Method declaration + writer.print( + "store.@%s::setPropertyData(Ljava/lang/Class;Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)", + TypeDataStore.class.getName()); + writer.println("(@%s::class, '%s', data);", property.getBeanType() + .getQualifiedSourceName(), property.getName()); + writer.println(); + writer.splitIfNeeded(true, + String.format("%s store", TypeDataStore.class.getName())); + } + return writer.getMethodNames(); + } + + private void printBundleData(TreeLogger logger, SourceWriter sourceWriter, + ConnectorBundle bundle, String loadNativeJsMethodName) + throws UnableToCompleteException { + // Split into new load method when reaching approximately 30000 bytes + SplittingSourceWriter w = new SplittingSourceWriter(sourceWriter, + "load", 30000); + + writeSuperClasses(w, bundle); + writeIdentifiers(w, bundle); + writeGwtConstructors(w, bundle); + writeReturnTypes(w, bundle); + writeInvokers(logger, w, bundle); + writeParamTypes(w, bundle); + writeProxys(w, bundle); + writeMethodAttributes(logger, w, bundle); + + w.println("%s(store);", loadNativeJsMethodName); + + // Must use Java code to generate Type data (because of Type[]), doing + // this after the JS property data has been initialized + writePropertyTypes(logger, w, bundle); + writeSerializers(logger, w, bundle); + writePresentationTypes(w, bundle); + writeDelegateToWidget(logger, w, bundle); + writeOnStateChangeHandlers(logger, w, bundle); + } + + private void writeOnStateChangeHandlers(TreeLogger logger, + SplittingSourceWriter w, ConnectorBundle bundle) + throws UnableToCompleteException { + Map<JClassType, Set<JMethod>> needsOnStateChangeHandler = bundle + .getNeedsOnStateChangeHandler(); + for (Entry<JClassType, Set<JMethod>> entry : needsOnStateChangeHandler + .entrySet()) { + JClassType connector = entry.getKey(); + + TreeLogger typeLogger = logger.branch( + Type.DEBUG, + "Generating @OnStateChange support for " + + connector.getName()); + + // Build map to speed up error checking + HashMap<String, Property> stateProperties = new HashMap<String, Property>(); + JClassType stateType = ConnectorBundle + .findInheritedMethod(connector, "getState").getReturnType() + .isClassOrInterface(); + for (Property property : bundle.getProperties(stateType)) { + stateProperties.put(property.getName(), property); + } + + for (JMethod method : entry.getValue()) { + TreeLogger methodLogger = typeLogger.branch(Type.DEBUG, + "Processing method " + method.getName()); + + if (method.isPublic() || method.isProtected()) { + methodLogger + .log(Type.ERROR, + "@OnStateChange is only supported for methods with private or default visibility."); + throw new UnableToCompleteException(); + } + + OnStateChange onStateChange = method + .getAnnotation(OnStateChange.class); + + String[] properties = onStateChange.value(); + + if (properties.length == 0) { + methodLogger.log(Type.ERROR, + "There are no properties to listen to"); + throw new UnableToCompleteException(); + } + + // Verify that all properties do exist + for (String propertyName : properties) { + if (!stateProperties.containsKey(propertyName)) { + methodLogger.log(Type.ERROR, + "State class has no property named " + + propertyName); + throw new UnableToCompleteException(); + } + } + + if (method.getParameters().length != 0) { + methodLogger.log(Type.ERROR, + "Method should accept zero parameters"); + throw new UnableToCompleteException(); + } + + // new OnStateChangeMethod(Class declaringClass, String + // methodName, String[], properties) + w.print("store.addOnStateChangeMethod(%s, new %s(", + getClassLiteralString(connector), + OnStateChangeMethod.class.getName()); + if (!connector.equals(method.getEnclosingType())) { + w.print("%s, ", + getClassLiteralString(method.getEnclosingType())); + } + w.print("\"%s\", ", method.getName()); + + w.print("new String[] {"); + for (String propertyName : properties) { + w.print("\"%s\", ", propertyName); + } + w.print("}"); + + w.println("));"); + + w.splitIfNeeded(); + } + } + } + + private void writeSuperClasses(SplittingSourceWriter w, + ConnectorBundle bundle) { + List<JClassType> needsSuperclass = new ArrayList<JClassType>( + bundle.getNeedsSuperclass()); + // Emit in hierarchy order to ensure superclass is defined when + // referenced + Collections.sort(needsSuperclass, new Comparator<JClassType>() { + + @Override + public int compare(JClassType type1, JClassType type2) { + int depthDiff = getDepth(type1) - getDepth(type2); + if (depthDiff != 0) { + return depthDiff; + } else { + // Just something to get a stable compare + return type1.getName().compareTo(type2.getName()); + } + } + + private int getDepth(JClassType type) { + int depth = 0; + while (type != null) { + depth++; + type = type.getSuperclass(); + } + return depth; + } + }); + + for (JClassType jClassType : needsSuperclass) { + JClassType superclass = jClassType.getSuperclass(); + while (superclass != null && !superclass.isPublic()) { + superclass = superclass.getSuperclass(); + } + String classLiteralString; + if (superclass == null) { + classLiteralString = "null"; + } else { + classLiteralString = getClassLiteralString(superclass); + } + w.println("store.setSuperClass(%s, %s);", + getClassLiteralString(jClassType), classLiteralString); + } + } + + private void writeDelegateToWidget(TreeLogger logger, + SplittingSourceWriter w, ConnectorBundle bundle) { + Map<JClassType, Set<Property>> needsDelegateToWidget = bundle + .getNeedsDelegateToWidget(); + for (Entry<JClassType, Set<Property>> entry : needsDelegateToWidget + .entrySet()) { + JClassType beanType = entry.getKey(); + for (Property property : entry.getValue()) { + w.println( + "store.setDelegateToWidget(%s, \"%s\", \"%s\");", + getClassLiteralString(beanType),// property.getBeanType()), + property.getName(), + property.getAnnotation(DelegateToWidget.class).value()); + } + w.splitIfNeeded(); + } + } + + private void writeSerializers(TreeLogger logger, SplittingSourceWriter w, + ConnectorBundle bundle) throws UnableToCompleteException { + Map<JType, GeneratedSerializer> serializers = bundle.getSerializers(); + for (Entry<JType, GeneratedSerializer> entry : serializers.entrySet()) { + JType type = entry.getKey(); + GeneratedSerializer serializer = entry.getValue(); + + w.print("store.setSerializerFactory("); + writeClassLiteral(w, type); + w.print(", "); + w.println("new Invoker() {"); + w.indent(); + + w.println("public Object invoke(Object target, Object[] params) {"); + w.indent(); + + serializer.writeSerializerInstantiator(logger, w); + + w.outdent(); + w.println("}"); + + w.outdent(); + w.print("}"); + w.println(");"); + + w.splitIfNeeded(); + } + } + + private void writePresentationTypes(SplittingSourceWriter w, + ConnectorBundle bundle) { + Map<JClassType, JType> presentationTypes = bundle + .getPresentationTypes(); + for (Entry<JClassType, JType> entry : presentationTypes.entrySet()) { + + w.print("store.setPresentationType("); + writeClassLiteral(w, entry.getKey()); + w.print(", "); + writeClassLiteral(w, entry.getValue()); + w.println(");"); + w.splitIfNeeded(); + } + } + + private void writePropertyTypes(TreeLogger logger, SplittingSourceWriter w, + ConnectorBundle bundle) { + Set<Property> properties = bundle.getNeedsProperty(); + for (Property property : properties) { + w.print("store.setPropertyType("); + writeClassLiteral(w, property.getBeanType()); + w.print(", \""); + w.print(escape(property.getName())); + w.print("\", "); + writeTypeCreator(w, property.getPropertyType()); + w.println(");"); + + w.splitIfNeeded(); + } + } + + private void writeMethodAttributes(TreeLogger logger, + SplittingSourceWriter w, ConnectorBundle bundle) { + for (Entry<JClassType, Map<JMethod, Set<MethodAttribute>>> typeEntry : bundle + .getMethodAttributes().entrySet()) { + JClassType type = typeEntry.getKey(); + for (Entry<JMethod, Set<MethodAttribute>> methodEntry : typeEntry + .getValue().entrySet()) { + JMethod method = methodEntry.getKey(); + Set<MethodAttribute> attributes = methodEntry.getValue(); + for (MethodAttribute attribute : attributes) { + w.println("store.setMethodAttribute(%s, \"%s\", %s.%s);", + getClassLiteralString(type), method.getName(), + MethodAttribute.class.getCanonicalName(), + attribute.name()); + w.splitIfNeeded(); + } + } + } + } + + private void writeProxys(SplittingSourceWriter w, ConnectorBundle bundle) { + Set<JClassType> needsProxySupport = bundle.getNeedsProxySupport(); + for (JClassType type : needsProxySupport) { + w.print("store.setProxyHandler("); + writeClassLiteral(w, type); + w.print(", new "); + w.print(ProxyHandler.class.getCanonicalName()); + w.println("() {"); + w.indent(); + + w.println("public Object createProxy(final " + + InvokationHandler.class.getName() + " handler) {"); + w.indent(); + + w.print("return new "); + w.print(type.getQualifiedSourceName()); + w.println("() {"); + w.indent(); + + JMethod[] methods = type.getOverridableMethods(); + for (JMethod method : methods) { + if (method.isAbstract()) { + w.print("public "); + w.print(method.getReturnType().getQualifiedSourceName()); + w.print(" "); + w.print(method.getName()); + w.print("("); + + JType[] types = method.getParameterTypes(); + for (int i = 0; i < types.length; i++) { + if (i != 0) { + w.print(", "); + } + w.print(types[i].getQualifiedSourceName()); + w.print(" p"); + w.print(Integer.toString(i)); + } + + w.println(") {"); + w.indent(); + + if (!method.getReturnType().getQualifiedSourceName() + .equals("void")) { + w.print("return "); + } + + w.print("handler.invoke(this, "); + w.print(TypeData.class.getCanonicalName()); + w.print(".getType("); + writeClassLiteral(w, type); + w.print(").getMethod(\""); + w.print(escape(method.getName())); + w.print("\"), new Object [] {"); + for (int i = 0; i < types.length; i++) { + w.print("p" + i + ", "); + } + w.println("});"); + + w.outdent(); + w.println("}"); + } + } + + w.outdent(); + w.println("};"); + + w.outdent(); + w.println("}"); + + w.outdent(); + w.println("});"); + + w.splitIfNeeded(); + } + } + + private void writeParamTypes(SplittingSourceWriter w, ConnectorBundle bundle) { + Map<JClassType, Set<JMethod>> needsParamTypes = bundle + .getNeedsParamTypes(); + for (Entry<JClassType, Set<JMethod>> entry : needsParamTypes.entrySet()) { + JClassType type = entry.getKey(); + + Set<JMethod> methods = entry.getValue(); + for (JMethod method : methods) { + w.print("store.setParamTypes("); + writeClassLiteral(w, type); + w.print(", \""); + w.print(escape(method.getName())); + w.print("\", new Type[] {"); + + for (JType parameter : method.getParameterTypes()) { + ConnectorBundleLoaderFactory.writeTypeCreator(w, parameter); + w.print(", "); + } + + w.println("});"); + + w.splitIfNeeded(); + } + } + } + + private void writeInvokers(TreeLogger logger, SplittingSourceWriter w, + ConnectorBundle bundle) throws UnableToCompleteException { + Map<JClassType, Set<JMethod>> needsInvoker = bundle.getNeedsInvoker(); + for (Entry<JClassType, Set<JMethod>> entry : needsInvoker.entrySet()) { + JClassType type = entry.getKey(); + + TreeLogger typeLogger = logger.branch(Type.DEBUG, + "Creating invokers for " + type); + + Set<JMethod> methods = entry.getValue(); + for (JMethod method : methods) { + w.print("store.setInvoker("); + writeClassLiteral(w, type); + w.print(", \""); + w.print(escape(method.getName())); + w.print("\","); + + if (method.isPublic()) { + typeLogger.log(Type.DEBUG, "Invoking " + method.getName() + + " using java"); + + writeJavaInvoker(w, type, method); + } else { + TreeLogger methodLogger = typeLogger.branch(Type.DEBUG, + "Invoking " + method.getName() + " using jsni"); + // Must use JSNI to access non-public methods + writeJsniInvoker(methodLogger, w, type, method); + } + + w.println(");"); + + w.splitIfNeeded(); + } + } + } + + private void writeJsniInvoker(TreeLogger logger, SplittingSourceWriter w, + JClassType type, JMethod method) throws UnableToCompleteException { + w.println("new JsniInvoker() {"); + w.indent(); + + w.println( + "protected native Object jsniInvoke(Object target, %s<Object> params) /*-{ ", + JsArrayObject.class.getName()); + w.indent(); + + JType returnType = method.getReturnType(); + boolean hasReturnType = !"void".equals(returnType + .getQualifiedSourceName()); + + // Note that void is also a primitive type + boolean hasPrimitiveReturnType = hasReturnType + && returnType.isPrimitive() != null; + + if (hasReturnType) { + w.print("return "); + + if (hasPrimitiveReturnType) { + // Integer.valueOf(expression); + w.print("@%s::valueOf(%s)(", returnType.isPrimitive() + .getQualifiedBoxedSourceName(), returnType + .getJNISignature()); + + // Implementation tested briefly, but I don't dare leave it + // enabled since we are not using it in the framework and we + // have not tests for it. + logger.log(Type.ERROR, + "JSNI invocation is not yet supported for methods with " + + "primitive return types. Change your method " + + "to public to be able to use conventional" + + " Java invoking instead."); + throw new UnableToCompleteException(); + } + } + + JType[] parameterTypes = method.getParameterTypes(); + + w.print("target.@%s::" + method.getName() + "(*)(", method + .getEnclosingType().getQualifiedSourceName()); + for (int i = 0; i < parameterTypes.length; i++) { + if (i != 0) { + w.print(", "); + } + + w.print("params[" + i + "]"); + + JPrimitiveType primitive = parameterTypes[i].isPrimitive(); + if (primitive != null) { + // param.intValue(); + w.print(".@%s::%sValue()()", + primitive.getQualifiedBoxedSourceName(), + primitive.getQualifiedSourceName()); + } + } + + if (hasPrimitiveReturnType) { + assert hasReturnType; + w.print(")"); + } + + w.println(");"); + + if (!hasReturnType) { + w.println("return null;"); + } + + w.outdent(); + w.println("}-*/;"); + + w.outdent(); + w.print("}"); + } + + private void writeJavaInvoker(SplittingSourceWriter w, JClassType type, + JMethod method) { + w.println("new Invoker() {"); + w.indent(); + + w.println("public Object invoke(Object target, Object[] params) {"); + w.indent(); + + JType returnType = method.getReturnType(); + boolean hasReturnType = !"void".equals(returnType + .getQualifiedSourceName()); + if (hasReturnType) { + w.print("return "); + } + + JType[] parameterTypes = method.getParameterTypes(); + + w.print("((" + type.getQualifiedSourceName() + ") target)." + + method.getName() + "("); + for (int i = 0; i < parameterTypes.length; i++) { + JType parameterType = parameterTypes[i]; + if (i != 0) { + w.print(", "); + } + String parameterTypeName = getBoxedTypeName(parameterType); + + if (parameterTypeName.startsWith("elemental.json.Json")) { + // Need to pass through native method to allow casting Object to + // JSO if the value is a string + w.print("%s.<%s>obj2jso(params[%d])", + JsonDecoder.class.getCanonicalName(), + parameterTypeName, i); + } else { + w.print("(" + parameterTypeName + ") params[" + i + "]"); + } + } + w.println(");"); + + if (!hasReturnType) { + w.println("return null;"); + } + + w.outdent(); + w.println("}"); + + w.outdent(); + w.print("}"); + } + + private void writeReturnTypes(SplittingSourceWriter w, + ConnectorBundle bundle) { + Map<JClassType, Set<JMethod>> methodReturnTypes = bundle + .getMethodReturnTypes(); + for (Entry<JClassType, Set<JMethod>> entry : methodReturnTypes + .entrySet()) { + JClassType type = entry.getKey(); + + Set<JMethod> methods = entry.getValue(); + for (JMethod method : methods) { + // setReturnType(Class<?> type, String methodName, Type + // returnType) + w.print("store.setReturnType("); + writeClassLiteral(w, type); + w.print(", \""); + w.print(escape(method.getName())); + w.print("\", "); + writeTypeCreator(w, method.getReturnType()); + w.println(");"); + + w.splitIfNeeded(); + } + } + } + + private void writeGwtConstructors(SplittingSourceWriter w, + ConnectorBundle bundle) { + Set<JClassType> constructors = bundle.getGwtConstructors(); + for (JClassType type : constructors) { + w.print("store.setConstructor("); + writeClassLiteral(w, type); + w.println(", new Invoker() {"); + w.indent(); + + w.println("public Object invoke(Object target, Object[] params) {"); + w.indent(); + + w.print("return "); + w.print(GWT.class.getName()); + w.print(".create("); + writeClassLiteral(w, type); + w.println(");"); + + w.outdent(); + w.println("}"); + + w.outdent(); + w.println("});"); + + w.splitIfNeeded(); + } + } + + public static void writeClassLiteral(SourceWriter w, JType type) { + w.print(getClassLiteralString(type)); + } + + public static String getClassLiteralString(JType type) { + return type.getQualifiedSourceName() + ".class"; + } + + private void writeIdentifiers(SplittingSourceWriter w, + ConnectorBundle bundle) { + Map<JClassType, Set<String>> identifiers = bundle.getIdentifiers(); + for (Entry<JClassType, Set<String>> entry : identifiers.entrySet()) { + Set<String> ids = entry.getValue(); + JClassType type = entry.getKey(); + for (String id : ids) { + w.print("store.setClass(\""); + w.print(escape(id)); + w.print("\", "); + writeClassLiteral(w, type); + w.println(");"); + + w.splitIfNeeded(); + } + } + } + + private List<ConnectorBundle> buildBundles(TreeLogger logger, + TypeOracle typeOracle) throws NotFoundException, + UnableToCompleteException { + + Map<LoadStyle, Collection<JClassType>> connectorsByLoadStyle = new HashMap<LoadStyle, Collection<JClassType>>(); + for (LoadStyle loadStyle : LoadStyle.values()) { + connectorsByLoadStyle.put(loadStyle, new ArrayList<JClassType>()); + } + + // Find all types with a valid mapping + Collection<JClassType> selectedTypes = getConnectorsForWidgetset( + logger, typeOracle); + + // Group by load style + for (JClassType connectorSubtype : selectedTypes) { + LoadStyle loadStyle = getLoadStyle(connectorSubtype); + if (loadStyle != null) { + connectorsByLoadStyle.get(loadStyle).add(connectorSubtype); + } + } + + List<ConnectorBundle> bundles = new ArrayList<ConnectorBundle>(); + + Collection<TypeVisitor> visitors = getVisitors(typeOracle); + + ConnectorBundle eagerBundle = new ConnectorBundle( + ConnectorBundleLoader.EAGER_BUNDLE_NAME, visitors, typeOracle); + TreeLogger eagerLogger = logger.branch(Type.TRACE, + "Populating eager bundle"); + + // Eager connectors and all RPC interfaces are loaded by default + eagerBundle.processTypes(eagerLogger, + connectorsByLoadStyle.get(LoadStyle.EAGER)); + eagerBundle.processType(eagerLogger, typeOracle + .findType(UnknownComponentConnector.class.getCanonicalName())); + eagerBundle.processSubTypes(eagerLogger, + typeOracle.getType(ClientRpc.class.getName())); + eagerBundle.processSubTypes(eagerLogger, + typeOracle.getType(ServerRpc.class.getName())); + + bundles.add(eagerBundle); + + ConnectorBundle deferredBundle = new ConnectorBundle( + ConnectorBundleLoader.DEFERRED_BUNDLE_NAME, eagerBundle); + TreeLogger deferredLogger = logger.branch(Type.TRACE, + "Populating deferred bundle"); + deferredBundle.processTypes(deferredLogger, + connectorsByLoadStyle.get(LoadStyle.DEFERRED)); + + bundles.add(deferredBundle); + + Collection<JClassType> lazy = connectorsByLoadStyle.get(LoadStyle.LAZY); + for (JClassType type : lazy) { + ConnectorBundle bundle = new ConnectorBundle(type.getName(), + eagerBundle); + TreeLogger subLogger = logger.branch(Type.TRACE, "Populating " + + type.getName() + " bundle"); + bundle.processType(subLogger, type); + + bundles.add(bundle); + } + + return bundles; + } + + /** + * Returns the connector types that should be included in the widgetset. + * This method can be overridden to create a widgetset only containing + * selected connectors. + * <p> + * The default implementation finds all type implementing + * {@link ServerConnector} that have a @{@link Connect} annotation. It also + * checks that multiple connectors aren't connected to the same server-side + * class. + * + * @param logger + * the logger to which information can be logged + * @param typeOracle + * the type oracle that can be used for finding types + * @return a collection of all the connector types that should be included + * in the widgetset + * @throws UnableToCompleteException + * if the operation fails + */ + protected Collection<JClassType> getConnectorsForWidgetset( + TreeLogger logger, TypeOracle typeOracle) + throws UnableToCompleteException { + JClassType serverConnectorType; + try { + serverConnectorType = typeOracle.getType(ServerConnector.class + .getName()); + } catch (NotFoundException e) { + logger.log(Type.ERROR, + "Can't find " + ServerConnector.class.getName()); + throw new UnableToCompleteException(); + } + + JClassType[] types = serverConnectorType.getSubtypes(); + + Map<String, JClassType> mappings = new TreeMap<String, JClassType>(); + + // Keep track of what has happened to avoid logging intermediate state + Map<JClassType, List<JClassType>> replaced = new TreeMap<JClassType, List<JClassType>>( + ConnectorBundle.jClassComparator); + + for (JClassType type : types) { + Connect connectAnnotation = type.getAnnotation(Connect.class); + if (connectAnnotation == null) { + continue; + } + + String identifier = connectAnnotation.value().getCanonicalName(); + + JClassType previousMapping = mappings.put(identifier, type); + if (previousMapping != null) { + // There are multiple mappings, pick the subclass + JClassType subclass; + JClassType superclass; + if (previousMapping.isAssignableFrom(type)) { + subclass = type; + superclass = previousMapping; + } else if (type.isAssignableFrom(previousMapping)) { + subclass = previousMapping; + superclass = type; + } else { + // Neither inherits from the other - this is a conflict + logger.log( + Type.ERROR, + "Conflicting @Connect mappings detected for " + + identifier + + ": " + + type.getQualifiedSourceName() + + " and " + + previousMapping.getQualifiedSourceName() + + ". There can only be multiple @Connect mappings for the same server-side type if one is the subclass of the other."); + throw new UnableToCompleteException(); + } + + mappings.put(identifier, subclass); + + // Inherit any previous replacements + List<JClassType> previousReplacements = replaced + .remove(superclass); + if (previousReplacements == null) { + previousReplacements = new ArrayList<JClassType>(); + } + + previousReplacements.add(superclass); + replaced.put(subclass, previousReplacements); + } + } + + // Log the final set of replacements + for (Entry<JClassType, List<JClassType>> entry : replaced.entrySet()) { + String msg = entry.getKey().getQualifiedSourceName() + " replaces "; + + List<JClassType> list = entry.getValue(); + for (int i = 0; i < list.size(); i++) { + if (i != 0) { + msg += ", "; + } + msg += list.get(i).getQualifiedSourceName(); + } + + logger.log(Type.INFO, msg); + } + + // Return the types of the final mapping + return mappings.values(); + } + + private Collection<TypeVisitor> getVisitors(TypeOracle oracle) + throws NotFoundException { + List<TypeVisitor> visitors = Arrays.<TypeVisitor> asList( + new ConnectorInitVisitor(), new StateInitVisitor(), + new WidgetInitVisitor(), new RendererVisitor(), + new ClientRpcVisitor(), new ServerRpcVisitor(), + new OnStateChangeVisitor()); + for (TypeVisitor typeVisitor : visitors) { + typeVisitor.init(oracle); + } + return visitors; + } + + protected LoadStyle getLoadStyle(JClassType connectorType) { + Connect annotation = connectorType.getAnnotation(Connect.class); + return annotation.loadStyle(); + } + + public static String getBoxedTypeName(JType type) { + if (type.isPrimitive() != null) { + // Used boxed types for primitives + return type.isPrimitive().getQualifiedBoxedSourceName(); + } else { + return type.getErasedType().getQualifiedSourceName(); + } + } + + public static void writeTypeCreator(SourceWriter sourceWriter, JType type) { + String typeName = ConnectorBundleLoaderFactory.getBoxedTypeName(type); + JParameterizedType parameterized = type.isParameterized(); + if (parameterized != null) { + sourceWriter.print("new Type(\"" + typeName + "\", "); + sourceWriter.print("new Type[] {"); + JClassType[] typeArgs = parameterized.getTypeArgs(); + for (JClassType jClassType : typeArgs) { + writeTypeCreator(sourceWriter, jClassType); + sourceWriter.print(", "); + } + sourceWriter.print("}"); + } else { + sourceWriter.print("new Type(" + typeName + ".class"); + } + sourceWriter.print(")"); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java new file mode 100644 index 0000000000..0049ae9b50 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JArrayType; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.user.rebind.SourceWriter; +import com.vaadin.client.communication.JsonDecoder; +import com.vaadin.client.communication.JsonEncoder; +import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory; + +import elemental.json.Json; +import elemental.json.JsonArray; + +public class ArraySerializer extends JsonSerializer { + + private final JArrayType arrayType; + + public ArraySerializer(JArrayType arrayType) { + super(arrayType); + this.arrayType = arrayType; + } + + @Override + protected void printDeserializerBody(TreeLogger logger, SourceWriter w, + String type, String jsonValue, String connection) { + JType leafType = arrayType.getLeafType(); + int rank = arrayType.getRank(); + + w.println(JsonArray.class.getName() + " jsonArray = (" + + JsonArray.class.getName() + ")" + jsonValue + ";"); + + // Type value = new Type[jsonArray.size()][][]; + w.print(arrayType.getQualifiedSourceName() + " value = new " + + leafType.getQualifiedSourceName() + "[jsonArray.length()]"); + for (int i = 1; i < rank; i++) { + w.print("[]"); + } + w.println(";"); + + w.println("for(int i = 0 ; i < value.length; i++) {"); + w.indent(); + + JType componentType = arrayType.getComponentType(); + + w.print("value[i] = (" + + ConnectorBundleLoaderFactory.getBoxedTypeName(componentType) + + ") " + JsonDecoder.class.getName() + ".decodeValue("); + ConnectorBundleLoaderFactory.writeTypeCreator(w, componentType); + w.print(", jsonArray.get(i), null, " + connection + ")"); + + w.println(";"); + + w.outdent(); + w.println("}"); + + w.println("return value;"); + } + + @Override + protected void printSerializerBody(TreeLogger logger, SourceWriter w, + String value, String applicationConnection) { + JType componentType = arrayType.getComponentType(); + + w.println(JsonArray.class.getName() + " values = " + + Json.class.getName() + ".createArray();"); + // JPrimitiveType primitive = componentType.isPrimitive(); + w.println("for (int i = 0; i < " + value + ".length; i++) {"); + w.indent(); + w.print("values.set(i, "); + w.print(JsonEncoder.class.getName() + ".encode(" + value + "[i],"); + ConnectorBundleLoaderFactory.writeTypeCreator(w, componentType); + w.print(", " + applicationConnection + ")"); + w.println(");"); + w.outdent(); + w.println("}"); + w.println("return values;"); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java new file mode 100644 index 0000000000..992a012005 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.util.Set; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; +import com.vaadin.client.metadata.TypeDataStore.MethodAttribute; +import com.vaadin.shared.annotations.NoLayout; + +public class ClientRpcVisitor extends TypeVisitor { + @Override + public void visitClientRpc(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + checkGenericType(logger, type); + Set<? extends JClassType> hierarchy = type + .getFlattenedSupertypeHierarchy(); + for (JClassType subType : hierarchy) { + JMethod[] methods = subType.getMethods(); + for (JMethod method : methods) { + checkReturnType(logger, method); + + bundle.setNeedsInvoker(type, method); + bundle.setNeedsParamTypes(type, method); + if (method.getAnnotation(NoLayout.class) != null) { + bundle.setMethodAttribute(type, method, + MethodAttribute.NO_LAYOUT); + } + + JType[] parameterTypes = method.getParameterTypes(); + for (JType paramType : parameterTypes) { + bundle.setNeedsSerialize(paramType); + } + } + } + } + + public static void checkGenericType(TreeLogger logger, JClassType type) + throws UnableToCompleteException { + if (type.isGenericType() != null) { + logger.log(Type.ERROR, + "Type " + type.getParameterizedQualifiedSourceName() + + "is parameterizied generic. RPC proxy " + + "for parameterizied types is not supported."); + throw new UnableToCompleteException(); + } + } + + public static void checkReturnType(TreeLogger logger, JMethod method) + throws UnableToCompleteException { + if (!method.getReturnType().getQualifiedSourceName().equals("void")) { + logger.log( + Type.ERROR, + "The method " + + method.getEnclosingType() + .getQualifiedSourceName() + + "." + + method.getName() + + " returns " + + method.getReturnType().getQualifiedSourceName() + + " but only void is supported for methods in RPC interfaces."); + throw new UnableToCompleteException(); + } + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java new file mode 100644 index 0000000000..b4531eb08e --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java @@ -0,0 +1,737 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JArrayType; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JEnumType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JParameterizedType; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.core.ext.typeinfo.NotFoundException; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.communication.JSONSerializer; +import com.vaadin.client.connectors.AbstractRendererConnector; +import com.vaadin.client.metadata.TypeDataStore.MethodAttribute; +import com.vaadin.client.ui.UnknownComponentConnector; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.ui.Connect; + +import elemental.json.JsonValue; + +public class ConnectorBundle { + private static final String FAIL_IF_NOT_SERIALIZABLE = "vFailIfNotSerializable"; + + public static final Comparator<JClassType> jClassComparator = new Comparator<JClassType>() { + @Override + public int compare(JClassType o1, JClassType o2) { + return o1.getQualifiedSourceName().compareTo( + o2.getQualifiedSourceName()); + } + }; + + public static final Comparator<JMethod> jMethodComparator = new Comparator<JMethod>() { + @Override + public int compare(JMethod o1, JMethod o2) { + return o1.getReadableDeclaration().compareTo( + o2.getReadableDeclaration()); + } + }; + + private final String name; + private final ConnectorBundle previousBundle; + private final Collection<TypeVisitor> visitors; + private final Map<JType, JClassType> customSerializers; + + private final Set<JType> hasSerializeSupport = new HashSet<JType>(); + private final Set<JType> needsSerializeSupport = new HashSet<JType>(); + + private final Map<JType, GeneratedSerializer> serializers = new TreeMap<JType, GeneratedSerializer>( + new Comparator<JType>() { + @Override + public int compare(JType o1, JType o2) { + return o1.toString().compareTo(o2.toString()); + } + }); + + private final Map<JClassType, Map<JMethod, Set<MethodAttribute>>> methodAttributes = new TreeMap<JClassType, Map<JMethod, Set<MethodAttribute>>>( + jClassComparator); + private final Set<JClassType> needsSuperClass = new TreeSet<JClassType>( + jClassComparator); + private final Set<JClassType> needsGwtConstructor = new TreeSet<JClassType>( + jClassComparator); + private final Set<JClassType> visitedTypes = new HashSet<JClassType>(); + + private final Set<JClassType> needsProxySupport = new TreeSet<JClassType>( + jClassComparator); + + private final Map<JClassType, JType> presentationTypes = new TreeMap<JClassType, JType>( + jClassComparator); + private final Map<JClassType, Set<String>> identifiers = new TreeMap<JClassType, Set<String>>( + jClassComparator); + private final Map<JClassType, Set<JMethod>> needsReturnType = new TreeMap<JClassType, Set<JMethod>>( + jClassComparator); + private final Map<JClassType, Set<JMethod>> needsInvoker = new TreeMap<JClassType, Set<JMethod>>( + jClassComparator); + private final Map<JClassType, Set<JMethod>> needsParamTypes = new TreeMap<JClassType, Set<JMethod>>( + jClassComparator); + private final Map<JClassType, Set<JMethod>> needsOnStateChange = new TreeMap<JClassType, Set<JMethod>>( + jClassComparator); + + private final Set<Property> needsProperty = new TreeSet<Property>(); + private final Map<JClassType, Set<Property>> needsDelegateToWidget = new TreeMap<JClassType, Set<Property>>( + jClassComparator); + + private ConnectorBundle(String name, ConnectorBundle previousBundle, + Collection<TypeVisitor> visitors, + Map<JType, JClassType> customSerializers) { + this.name = name; + this.previousBundle = previousBundle; + this.visitors = visitors; + this.customSerializers = customSerializers; + } + + public ConnectorBundle(String name, ConnectorBundle previousBundle) { + this(name, previousBundle, previousBundle.visitors, + previousBundle.customSerializers); + } + + public ConnectorBundle(String name, Collection<TypeVisitor> visitors, + TypeOracle oracle) throws NotFoundException { + this(name, null, visitors, findCustomSerializers(oracle)); + } + + private static Map<JType, JClassType> findCustomSerializers( + TypeOracle oracle) throws NotFoundException { + Map<JType, JClassType> serializers = new HashMap<JType, JClassType>(); + + JClassType serializerInterface = oracle.findType(JSONSerializer.class + .getName()); + JType[] deserializeParamTypes = new JType[] { + oracle.findType(com.vaadin.client.metadata.Type.class.getName()), + oracle.findType(JsonValue.class.getName()), + oracle.findType(ApplicationConnection.class.getName()) }; + String deserializeMethodName = "deserialize"; + // Just test that the method exists + serializerInterface.getMethod(deserializeMethodName, + deserializeParamTypes); + + for (JClassType serializer : serializerInterface.getSubtypes()) { + JMethod deserializeMethod = serializer.findMethod( + deserializeMethodName, deserializeParamTypes); + if (deserializeMethod == null) { + continue; + } + JType returnType = deserializeMethod.getReturnType(); + + serializers.put(returnType, serializer); + } + return serializers; + } + + public void setNeedsGwtConstructor(JClassType type) { + if (!needsGwtConstructor(type)) { + needsGwtConstructor.add(type); + } + } + + private boolean needsGwtConstructor(JClassType type) { + if (needsGwtConstructor.contains(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.needsGwtConstructor(type); + } + } + + public void setIdentifier(JClassType type, String identifier) { + if (!hasIdentifier(type, identifier)) { + addMapping(identifiers, type, identifier); + } + } + + private boolean hasIdentifier(JClassType type, String identifier) { + if (hasMapping(identifiers, type, identifier)) { + return true; + } else { + return previousBundle != null + && previousBundle.hasIdentifier(type, identifier); + } + } + + public ConnectorBundle getPreviousBundle() { + return previousBundle; + } + + public String getName() { + return name; + } + + public Map<JClassType, Set<String>> getIdentifiers() { + return Collections.unmodifiableMap(identifiers); + } + + public Set<JClassType> getGwtConstructors() { + return Collections.unmodifiableSet(needsGwtConstructor); + } + + public void processTypes(TreeLogger logger, Collection<JClassType> types) + throws UnableToCompleteException { + for (JClassType type : types) { + processType(logger, type); + } + } + + public void processType(TreeLogger logger, JClassType type) + throws UnableToCompleteException { + if (!isTypeVisited(type)) { + for (TypeVisitor typeVisitor : visitors) { + invokeVisitor(logger, type, typeVisitor); + } + visitedTypes.add(type); + purgeSerializeSupportQueue(logger); + } + } + + private boolean isTypeVisited(JClassType type) { + if (visitedTypes.contains(type)) { + return true; + } else { + return previousBundle != null && previousBundle.isTypeVisited(type); + } + } + + private void purgeSerializeSupportQueue(TreeLogger logger) + throws UnableToCompleteException { + while (!needsSerializeSupport.isEmpty()) { + Iterator<JType> iterator = needsSerializeSupport.iterator(); + JType type = iterator.next(); + iterator.remove(); + + if (hasSerializeSupport(type)) { + continue; + } + + addSerializeSupport(logger, type); + } + } + + private void addSerializeSupport(TreeLogger logger, JType type) + throws UnableToCompleteException { + hasSerializeSupport.add(type); + + JParameterizedType parametrized = type.isParameterized(); + if (parametrized != null) { + for (JClassType parameterType : parametrized.getTypeArgs()) { + setNeedsSerialize(parameterType); + } + } + + if (serializationHandledByFramework(type)) { + return; + } + + JClassType customSerializer = customSerializers.get(type); + JClassType typeAsClass = type.isClass(); + JEnumType enumType = type.isEnum(); + JArrayType arrayType = type.isArray(); + + if (customSerializer != null) { + logger.log(Type.INFO, "Will serialize " + type + " using " + + customSerializer.getName()); + setSerializer(type, new CustomSerializer(customSerializer)); + } else if (arrayType != null) { + logger.log(Type.INFO, "Will serialize " + type + " as an array"); + setSerializer(type, new ArraySerializer(arrayType)); + setNeedsSerialize(arrayType.getComponentType()); + } else if (enumType != null) { + logger.log(Type.INFO, "Will serialize " + type + " as an enum"); + setSerializer(type, new EnumSerializer(enumType)); + } else if (typeAsClass != null) { + // Bean + checkSerializable(logger, typeAsClass); + + logger.log(Type.INFO, "Will serialize " + type + " as a bean"); + + JClassType needsSuperClass = typeAsClass; + while (needsSuperClass != null) { + if (needsSuperClass.isPublic()) { + setNeedsSuperclass(needsSuperClass); + } + needsSuperClass = needsSuperClass.getSuperclass(); + } + + setNeedsGwtConstructor(typeAsClass); + + for (Property property : getProperties(typeAsClass)) { + setNeedsProperty(property); + + JType propertyType = property.getPropertyType(); + setNeedsSerialize(propertyType); + } + } + } + + private void checkSerializable(TreeLogger logger, JClassType type) + throws UnableToCompleteException { + JClassType javaSerializable = type.getOracle().findType( + Serializable.class.getName()); + boolean serializable = type.isAssignableTo(javaSerializable); + if (!serializable) { + boolean abortCompile = "true".equals(System + .getProperty(FAIL_IF_NOT_SERIALIZABLE)); + logger.log( + abortCompile ? Type.ERROR : Type.WARN, + type + + " is used in RPC or shared state but does not implement " + + Serializable.class.getName() + + ". Communication will work but the Application on server side cannot be serialized if it refers to objects of this type. " + + "If the system property " + + FAIL_IF_NOT_SERIALIZABLE + + " is set to \"true\", this causes the compilation to fail instead of just emitting a warning."); + if (abortCompile) { + throw new UnableToCompleteException(); + } + } + } + + private void setSerializer(JType type, GeneratedSerializer serializer) { + if (!hasSerializer(type)) { + serializers.put(type, serializer); + } + } + + private boolean hasSerializer(JType type) { + if (serializers.containsKey(type)) { + return true; + } else { + return previousBundle != null && previousBundle.hasSerializer(type); + } + } + + public Map<JType, GeneratedSerializer> getSerializers() { + return Collections.unmodifiableMap(serializers); + } + + public void setPresentationType(JClassType type, JType presentationType) { + if (!hasPresentationType(type)) { + presentationTypes.put(type, presentationType); + } + } + + private boolean hasPresentationType(JClassType type) { + if (presentationTypes.containsKey(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.hasPresentationType(type); + } + } + + public Map<JClassType, JType> getPresentationTypes() { + return Collections.unmodifiableMap(presentationTypes); + } + + private void setNeedsSuperclass(JClassType typeAsClass) { + if (!isNeedsSuperClass(typeAsClass)) { + needsSuperClass.add(typeAsClass); + } + } + + private boolean isNeedsSuperClass(JClassType typeAsClass) { + if (needsSuperClass.contains(typeAsClass)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsSuperClass(typeAsClass); + } + } + + public Set<JClassType> getNeedsSuperclass() { + return Collections.unmodifiableSet(needsSuperClass); + } + + private void setNeedsProperty(Property property) { + if (!isNeedsProperty(property)) { + needsProperty.add(property); + } + } + + private boolean isNeedsProperty(Property property) { + if (needsProperty.contains(property)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsProperty(property); + } + } + + public Set<Property> getNeedsProperty() { + return Collections.unmodifiableSet(needsProperty); + } + + public Collection<Property> getProperties(JClassType type) { + Set<Property> properties = new TreeSet<Property>(); + + properties.addAll(MethodProperty.findProperties(type)); + properties.addAll(FieldProperty.findProperties(type)); + + return properties; + } + + private void invokeVisitor(TreeLogger logger, JClassType type, + TypeVisitor typeVisitor) throws UnableToCompleteException { + TreeLogger subLogger = logger.branch(Type.TRACE, + "Visiting " + type.getName() + " with " + + typeVisitor.getClass().getSimpleName()); + if (isConnectedConnector(type)) { + typeVisitor.visitConnector(subLogger, type, this); + } + if (isClientRpc(type)) { + typeVisitor.visitClientRpc(subLogger, type, this); + } + if (isServerRpc(type)) { + typeVisitor.visitServerRpc(subLogger, type, this); + } + } + + public void processSubTypes(TreeLogger logger, JClassType type) + throws UnableToCompleteException { + processTypes(logger, Arrays.asList(type.getSubtypes())); + } + + public void setNeedsReturnType(JClassType type, JMethod method) { + if (!isNeedsReturnType(type, method)) { + addMapping(needsReturnType, type, method); + } + } + + private boolean isNeedsReturnType(JClassType type, JMethod method) { + if (hasMapping(needsReturnType, type, method)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsReturnType(type, method); + } + } + + public Map<JClassType, Set<JMethod>> getMethodReturnTypes() { + return Collections.unmodifiableMap(needsReturnType); + } + + private static boolean isClientRpc(JClassType type) { + return isInterfaceType(type, ClientRpc.class); + } + + private static boolean isServerRpc(JClassType type) { + return isInterfaceType(type, ServerRpc.class); + } + + public static boolean isConnectedConnector(JClassType type) { + return isConnected(type) && isType(type, ServerConnector.class); + } + + private static boolean isConnected(JClassType type) { + return type.isAnnotationPresent(Connect.class) + || type.getQualifiedSourceName().equals( + UnknownComponentConnector.class.getCanonicalName()); + } + + public static boolean isConnectedComponentConnector(JClassType type) { + return isConnected(type) && isType(type, ComponentConnector.class); + } + + public static boolean isConnectedRendererConnector(JClassType type) { + return isConnected(type) + && isType(type, AbstractRendererConnector.class); + } + + private static boolean isInterfaceType(JClassType type, Class<?> class1) { + return type.isInterface() != null && isType(type, class1); + } + + private static boolean isType(JClassType type, Class<?> class1) { + try { + return type.getOracle().getType(class1.getName()) + .isAssignableFrom(type); + } catch (NotFoundException e) { + throw new RuntimeException("Could not find " + class1.getName(), e); + } + } + + public void setNeedsInvoker(JClassType type, JMethod method) { + if (!isNeedsInvoker(type, method)) { + addMapping(needsInvoker, type, method); + } + } + + private <K> void addMapping(Map<K, Set<String>> map, K key, String value) { + Set<String> set = map.get(key); + if (set == null) { + set = new TreeSet<String>(); + map.put(key, set); + } + set.add(value); + } + + private <K> void addMapping(Map<K, Set<JMethod>> map, K key, JMethod value) { + Set<JMethod> set = map.get(key); + if (set == null) { + set = new TreeSet<JMethod>(jMethodComparator); + map.put(key, set); + } + set.add(value); + } + + private <K, V> boolean hasMapping(Map<K, Set<V>> map, K key, V value) { + return map.containsKey(key) && map.get(key).contains(value); + } + + private boolean isNeedsInvoker(JClassType type, JMethod method) { + if (hasMapping(needsInvoker, type, method)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsInvoker(type, method); + } + } + + public Map<JClassType, Set<JMethod>> getNeedsInvoker() { + return Collections.unmodifiableMap(needsInvoker); + } + + public void setNeedsParamTypes(JClassType type, JMethod method) { + if (!isNeedsParamTypes(type, method)) { + addMapping(needsParamTypes, type, method); + } + } + + private boolean isNeedsParamTypes(JClassType type, JMethod method) { + if (hasMapping(needsParamTypes, type, method)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsParamTypes(type, method); + } + } + + public Map<JClassType, Set<JMethod>> getNeedsParamTypes() { + return Collections.unmodifiableMap(needsParamTypes); + } + + public void setNeedsProxySupport(JClassType type) { + if (!isNeedsProxySupport(type)) { + needsProxySupport.add(type); + } + } + + private boolean isNeedsProxySupport(JClassType type) { + if (needsProxySupport.contains(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsProxySupport(type); + } + } + + public Set<JClassType> getNeedsProxySupport() { + return Collections.unmodifiableSet(needsProxySupport); + } + + public void setMethodAttribute(JClassType type, JMethod method, + MethodAttribute methodAttribute) { + if (!hasMethodAttribute(type, method, methodAttribute)) { + Map<JMethod, Set<MethodAttribute>> typeData = methodAttributes + .get(type); + if (typeData == null) { + typeData = new TreeMap<JMethod, Set<MethodAttribute>>( + jMethodComparator); + methodAttributes.put(type, typeData); + } + + Map<JMethod, Set<MethodAttribute>> methods = methodAttributes + .get(type); + if (methods == null) { + methods = new TreeMap<JMethod, Set<MethodAttribute>>( + jMethodComparator); + methodAttributes.put(type, methods); + } + + Set<MethodAttribute> attributes = methods.get(method); + if (attributes == null) { + attributes = new TreeSet<MethodAttribute>(); + methods.put(method, attributes); + } + + attributes.add(methodAttribute); + } + } + + private boolean hasMethodAttribute(JClassType type, JMethod method, + MethodAttribute methodAttribute) { + Map<JMethod, Set<MethodAttribute>> typeData = methodAttributes + .get(type); + if (typeData != null && hasMapping(typeData, method, methodAttribute)) { + return true; + } else { + return previousBundle != null + && previousBundle.hasMethodAttribute(type, method, + methodAttribute); + } + } + + public Map<JClassType, Map<JMethod, Set<MethodAttribute>>> getMethodAttributes() { + return Collections.unmodifiableMap(methodAttributes); + } + + public void setNeedsSerialize(JType type) { + if (!hasSerializeSupport(type)) { + needsSerializeSupport.add(type); + } + } + + private static Set<Class<?>> frameworkHandledTypes = new LinkedHashSet<Class<?>>(); + { + frameworkHandledTypes.add(String.class); + frameworkHandledTypes.add(Boolean.class); + frameworkHandledTypes.add(Integer.class); + frameworkHandledTypes.add(Float.class); + frameworkHandledTypes.add(Double.class); + frameworkHandledTypes.add(Long.class); + frameworkHandledTypes.add(Enum.class); + frameworkHandledTypes.add(String[].class); + frameworkHandledTypes.add(Object[].class); + frameworkHandledTypes.add(Map.class); + frameworkHandledTypes.add(List.class); + frameworkHandledTypes.add(Set.class); + frameworkHandledTypes.add(Byte.class); + frameworkHandledTypes.add(Character.class); + frameworkHandledTypes.add(Void.class); + } + + private boolean serializationHandledByFramework(JType setterType) { + // Some types are handled by the framework at the moment. See #8449 + // This method should be removed at some point. + if (setterType.isPrimitive() != null) { + return true; + } + + String qualifiedName = setterType.getQualifiedSourceName(); + for (Class<?> cls : frameworkHandledTypes) { + if (qualifiedName.equals(cls.getName())) { + return true; + } + } + + return false; + } + + private boolean hasSerializeSupport(JType type) { + if (hasSerializeSupport.contains(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.hasSerializeSupport(type); + } + } + + public void setNeedsDelegateToWidget(Property property, JClassType type) { + if (!isNeedsDelegateToWidget(type)) { + TreeSet<Property> set = new TreeSet<Property>(); + set.add(property); + needsDelegateToWidget.put(type, set); + } else if (!needsDelegateToWidget.get(type).contains(property)) { + needsDelegateToWidget.get(type).add(property); + } + } + + private boolean isNeedsDelegateToWidget(JClassType type) { + if (needsDelegateToWidget.containsKey(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsDelegateToWidget(type); + } + } + + public Map<JClassType, Set<Property>> getNeedsDelegateToWidget() { + return Collections.unmodifiableMap(needsDelegateToWidget); + } + + public void setNeedsOnStateChangeHandler(JClassType type, JMethod method) { + if (!isNeedsOnStateChangeHandler(type, method)) { + addMapping(needsOnStateChange, type, method); + } + } + + private boolean isNeedsOnStateChangeHandler(JClassType type, JMethod method) { + if (hasMapping(needsOnStateChange, type, method)) { + return true; + } else { + return previousBundle != null + && previousBundle.isNeedsOnStateChangeHandler(type, method); + } + } + + public Map<JClassType, Set<JMethod>> getNeedsOnStateChangeHandler() { + return Collections.unmodifiableMap(needsOnStateChange); + } + + public static JMethod findInheritedMethod(JClassType type, + String methodName, JType... params) { + + JClassType currentType = type; + while (currentType != null) { + JMethod method = currentType.findMethod(methodName, params); + if (method != null) { + return method; + } + currentType = currentType.getSuperclass(); + } + + JClassType[] interfaces = type.getImplementedInterfaces(); + for (JClassType iface : interfaces) { + JMethod method = iface.findMethod(methodName, params); + if (method != null) { + return method; + } + } + + return null; + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorInitVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorInitVisitor.java new file mode 100644 index 0000000000..ea3b097fa2 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ConnectorInitVisitor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.vaadin.shared.ui.Connect; + +public class ConnectorInitVisitor extends TypeVisitor { + + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + Connect connectAnnotation = type.getAnnotation(Connect.class); + if (connectAnnotation != null) { + logger.log(Type.INFO, type.getName() + " will be in the " + + bundle.getName().replaceAll("^_*", "") + " bundle"); + String identifier = connectAnnotation.value().getCanonicalName(); + + bundle.setIdentifier(type, identifier); + bundle.setNeedsGwtConstructor(type); + } + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/CustomSerializer.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/CustomSerializer.java new file mode 100644 index 0000000000..bb3dd4f61d --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/CustomSerializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.client.GWT; +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.user.rebind.SourceWriter; +import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory; + +public class CustomSerializer implements GeneratedSerializer { + + private final JClassType serializerType; + + public CustomSerializer(JClassType serializerType) { + this.serializerType = serializerType; + } + + @Override + public void writeSerializerInstantiator(TreeLogger logger, SourceWriter w) + throws UnableToCompleteException { + w.print("return "); + w.print(GWT.class.getCanonicalName()); + w.print(".create("); + ConnectorBundleLoaderFactory.writeClassLiteral(w, serializerType); + w.println(");"); + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java new file mode 100644 index 0000000000..9876baf946 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JEnumConstant; +import com.google.gwt.core.ext.typeinfo.JEnumType; +import com.google.gwt.user.rebind.SourceWriter; + +import elemental.json.Json; + +public class EnumSerializer extends JsonSerializer { + + private final JEnumType enumType; + + public EnumSerializer(JEnumType type) { + super(type); + enumType = type; + } + + @Override + protected void printDeserializerBody(TreeLogger logger, SourceWriter w, + String type, String jsonValue, String connection) { + w.println("String enumIdentifier = " + jsonValue + ".asString();"); + for (JEnumConstant e : enumType.getEnumConstants()) { + w.println("if (\"" + e.getName() + "\".equals(enumIdentifier)) {"); + w.indent(); + w.println("return " + enumType.getQualifiedSourceName() + "." + + e.getName() + ";"); + w.outdent(); + w.println("}"); + } + w.println("return null;"); + } + + @Override + protected void printSerializerBody(TreeLogger logger, SourceWriter w, + String value, String applicationConnection) { + // return Json.create(castedValue.name()); + w.println("return " + Json.class.getName() + ".create(" + value + + ".name());"); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/FieldProperty.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/FieldProperty.java new file mode 100644 index 0000000000..a31dafe05c --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/FieldProperty.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JField; +import com.google.gwt.user.rebind.SourceWriter; + +public class FieldProperty extends Property { + + private final JField field; + + private FieldProperty(JClassType beanType, JField field) { + super(field.getName(), beanType, field.getType()); + this.field = field; + } + + @Override + public boolean hasAccessorMethods() { + return true; + } + + @Override + public void writeSetterBody(TreeLogger logger, SourceWriter w, + String beanVariable, String valueVariable) { + w.println("%s.@%s::%s = %s;", beanVariable, getBeanType() + .getQualifiedSourceName(), getName(), unboxValue(valueVariable)); + } + + @Override + public void writeGetterBody(TreeLogger logger, SourceWriter w, + String beanVariable) { + String value = String.format("%s.@%s::%s", beanVariable, getBeanType() + .getQualifiedSourceName(), getName()); + w.print("return "); + w.print(boxValue(value)); + w.println(";"); + } + + public static Collection<FieldProperty> findProperties(JClassType type) { + Collection<FieldProperty> properties = new ArrayList<FieldProperty>(); + + List<JField> fields = getPublicFields(type); + for (JField field : fields) { + properties.add(new FieldProperty(field.getEnclosingType(), field)); + } + + return properties; + } + + private static List<JField> getPublicFields(JClassType type) { + Set<String> names = new HashSet<String>(); + ArrayList<JField> fields = new ArrayList<JField>(); + for (JClassType subType : type.getFlattenedSupertypeHierarchy()) { + JField[] subFields = subType.getFields(); + for (JField field : subFields) { + if (field.isPublic() && !field.isStatic() + && names.add(field.getName())) { + fields.add(field); + } + } + } + return fields; + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + return field.getAnnotation(annotationClass); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/GeneratedSerializer.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/GeneratedSerializer.java new file mode 100644 index 0000000000..6afb172ea2 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/GeneratedSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.user.rebind.SourceWriter; + +public interface GeneratedSerializer { + public void writeSerializerInstantiator(TreeLogger logger, SourceWriter w) + throws UnableToCompleteException; +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java new file mode 100644 index 0000000000..a7a6c568da --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.user.rebind.SourceWriter; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.communication.JSONSerializer; +import elemental.json.JsonValue; + +public abstract class JsonSerializer implements GeneratedSerializer { + + private final JType type; + + public JsonSerializer(JType type) { + this.type = type; + } + + @Override + public void writeSerializerInstantiator(TreeLogger logger, SourceWriter w) + throws UnableToCompleteException { + + w.print("return new "); + w.print(JSONSerializer.class.getCanonicalName()); + w.print("<"); + w.print(type.getQualifiedSourceName()); + w.println(">() {"); + w.indent(); + + writeSerializerBody(logger, w); + + w.outdent(); + w.println("};"); + } + + protected void writeSerializerBody(TreeLogger logger, SourceWriter w) { + String qualifiedSourceName = type.getQualifiedSourceName(); + w.println("public " + JsonValue.class.getName() + " serialize(" + + qualifiedSourceName + " value, " + + ApplicationConnection.class.getName() + " connection) {"); + w.indent(); + // MouseEventDetails castedValue = (MouseEventDetails) value; + w.println(qualifiedSourceName + " castedValue = (" + + qualifiedSourceName + ") value;"); + + printSerializerBody(logger, w, "castedValue", "connection"); + + // End of serializer method + w.outdent(); + w.println("}"); + + // Deserializer + // T deserialize(Type type, JSONValue jsonValue, ApplicationConnection + // connection); + w.println("public " + qualifiedSourceName + " deserialize(Type type, " + + JsonValue.class.getName() + " jsonValue, " + + ApplicationConnection.class.getName() + " connection) {"); + w.indent(); + + printDeserializerBody(logger, w, "type", "jsonValue", "connection"); + + w.outdent(); + w.println("}"); + } + + protected abstract void printDeserializerBody(TreeLogger logger, + SourceWriter w, String type, String jsonValue, String connection); + + protected abstract void printSerializerBody(TreeLogger logger, + SourceWriter w, String value, String applicationConnection); + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/MethodProperty.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/MethodProperty.java new file mode 100644 index 0000000000..32aad92774 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/MethodProperty.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.user.rebind.SourceWriter; + +public class MethodProperty extends Property { + + private final JMethod setter; + + private final String getter; + + private MethodProperty(JClassType beanType, JMethod setter, String getter) { + super(getTransportFieldName(setter), beanType, setter + .getParameterTypes()[0]); + this.setter = setter; + this.getter = getter; + } + + @Override + public boolean hasAccessorMethods() { + return getter != null; + } + + public static Collection<MethodProperty> findProperties(JClassType type) { + Collection<MethodProperty> properties = new ArrayList<MethodProperty>(); + + Set<String> getters = new HashSet<String>(); + List<JMethod> setters = getSetters(type, getters); + for (JMethod setter : setters) { + String getter = findGetter(type, setter); + properties.add(new MethodProperty(setter.getEnclosingType(), + setter, getters.contains(getter) ? getter : null)); + } + + return properties; + } + + /** + * Returns a list of all setters found in the beanType or its parent class + * + * @param beanType + * The type to check + * @param getters + * Set that will be filled with names of getters. + * @return A list of setter methods from the class and its parents + */ + private static List<JMethod> getSetters(JClassType beanType, + Set<String> getters) { + List<JMethod> setterMethods = new ArrayList<JMethod>(); + + while (beanType != null + && !beanType.getQualifiedSourceName().equals( + Object.class.getName())) { + for (JMethod method : beanType.getMethods()) { + // Process all setters that have corresponding fields + if (!method.isPublic() || method.isStatic()) { + // Not getter/setter, skip to next method + continue; + } + String methodName = method.getName(); + if (methodName.startsWith("set") + && method.getParameterTypes().length == 1) { + setterMethods.add(method); + } else if (method.getParameterTypes().length == 0 + && methodName.startsWith("is") + || methodName.startsWith("get")) { + getters.add(methodName); + } + } + beanType = beanType.getSuperclass(); + } + + return setterMethods; + } + + @Override + public void writeGetterBody(TreeLogger logger, SourceWriter w, + String beanVariable) { + String value = String.format("%s.@%s::%s()()", beanVariable, + getBeanType().getQualifiedSourceName(), getter); + w.print("return "); + w.print(boxValue(value)); + w.println(";"); + } + + @Override + public void writeSetterBody(TreeLogger logger, SourceWriter w, + String beanVariable, String valueVariable) { + w.println("%s.@%s::%s(%s)(%s);", beanVariable, getBeanType() + .getQualifiedSourceName(), setter.getName(), setter + .getParameterTypes()[0].getJNISignature(), + unboxValue(valueVariable)); + + } + + private static String findGetter(JClassType beanType, JMethod setterMethod) { + JType setterParameterType = setterMethod.getParameterTypes()[0]; + String fieldName = setterMethod.getName().substring(3); + if (setterParameterType.getQualifiedSourceName().equals( + boolean.class.getName())) { + return "is" + fieldName; + } else { + return "get" + fieldName; + } + } + + private static String getTransportFieldName(JMethod setter) { + String baseName = setter.getName().substring(3); + return Character.toLowerCase(baseName.charAt(0)) + + baseName.substring(1); + } + + @Override + public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + return setter.getAnnotation(annotationClass); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/OnStateChangeVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/OnStateChangeVisitor.java new file mode 100644 index 0000000000..1c0da9d9e8 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/OnStateChangeVisitor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +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.JMethod; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.shared.ui.Connect; + +/** + * Visits Connector classes and check for methods with @OnStateChange + * annotations. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class OnStateChangeVisitor extends TypeVisitor { + + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + Connect connectAnnotation = type.getAnnotation(Connect.class); + if (connectAnnotation != null) { + // Find all the annotated methods in all the superclasses + JClassType connector = type; + while (connector != null) { + for (JMethod method : connector.getMethods()) { + if (method.getAnnotation(OnStateChange.class) != null) { + bundle.setNeedsInvoker(connector, method); + bundle.setNeedsOnStateChangeHandler(type, method); + } + } + + connector = connector.getSuperclass(); + } + } + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/Property.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/Property.java new file mode 100644 index 0000000000..0c849bead5 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/Property.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.lang.annotation.Annotation; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JPrimitiveType; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.user.rebind.SourceWriter; + +public abstract class Property implements Comparable<Property> { + private final String name; + private final JClassType beanType; + private final JType propertyType; + + protected Property(String name, JClassType beanType, JType propertyType) { + this.name = name; + this.beanType = beanType; + this.propertyType = propertyType; + } + + public String getName() { + return name; + } + + public JType getPropertyType() { + return propertyType; + } + + public String getUnboxedPropertyTypeName() { + JType propertyType = getPropertyType(); + JPrimitiveType primitive = propertyType.isPrimitive(); + if (primitive != null) { + return primitive.getQualifiedBoxedSourceName(); + } else { + return propertyType.getQualifiedSourceName(); + } + } + + public String boxValue(String codeSnippet) { + JPrimitiveType primitive = propertyType.isPrimitive(); + if (primitive == null) { + return codeSnippet; + } else { + return String.format("@%s::valueOf(%s)(%s)", + primitive.getQualifiedBoxedSourceName(), + propertyType.getJNISignature(), codeSnippet); + } + } + + public String unboxValue(String codeSnippet) { + JPrimitiveType primitive = propertyType.isPrimitive(); + if (primitive == null) { + return codeSnippet; + } else { + return String.format("%s.@%s::%sValue()()", codeSnippet, + primitive.getQualifiedBoxedSourceName(), + primitive.getSimpleSourceName()); + } + } + + public JClassType getBeanType() { + return beanType; + } + + public abstract void writeSetterBody(TreeLogger logger, SourceWriter w, + String beanVariable, String valueVariable); + + public abstract void writeGetterBody(TreeLogger logger, SourceWriter w, + String beanVariable); + + public abstract boolean hasAccessorMethods(); + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof Property) { + Property other = (Property) obj; + return other.getClass() == getClass() + && other.getBeanType().equals(getBeanType()) + && other.getName().equals(getName()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return getClass().hashCode() * 31 ^ 2 + getBeanType().hashCode() * 31 + + getName().hashCode(); + } + + @Override + public int compareTo(Property o) { + int comp = getName().compareTo(o.getName()); + if (comp == 0) { + comp = getBeanType().getQualifiedSourceName().compareTo( + o.getBeanType().getQualifiedSourceName()); + } + if (comp == 0) { + comp = getClass().getCanonicalName().compareTo( + o.getClass().getCanonicalName()); + } + return comp; + } + + public abstract <T extends Annotation> T getAnnotation( + Class<T> annotationClass); + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java new file mode 100644 index 0000000000..2e54d00aab --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java @@ -0,0 +1,119 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JParameterizedType; +import com.google.gwt.core.ext.typeinfo.JType; +import com.vaadin.client.connectors.AbstractRendererConnector; + +/** + * Generates type data for renderer connectors. + * <ul> + * <li>Stores the return type of the overridden + * {@link AbstractRendererConnector#getRenderer() getRenderer} method to enable + * automatic creation of an instance of the proper renderer type. + * <li>Stores the presentation type of the connector to enable the + * {@link AbstractRendererConnector#decode(elemental.json.JsonValue) decode} + * method to work without having to implement a "getPresentationType" method. + * </ul> + * + * @see WidgetInitVisitor + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class RendererVisitor extends TypeVisitor { + + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + if (ConnectorBundle.isConnectedRendererConnector(type)) { + doRendererType(logger, type, bundle); + doPresentationType(logger, type, bundle); + } + } + + private static void doRendererType(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + // The class in which createRenderer is implemented + JClassType createRendererClass = ConnectorBundle.findInheritedMethod( + type, "createRenderer").getEnclosingType(); + + // Needs GWT constructor if createRenderer is not overridden + if (createRendererClass.getQualifiedSourceName().equals( + AbstractRendererConnector.class.getCanonicalName())) { + + JMethod getRenderer = ConnectorBundle.findInheritedMethod(type, + "getRenderer"); + if (getRenderer.getEnclosingType().getQualifiedSourceName() + .equals(AbstractRendererConnector.class.getCanonicalName())) { + logger.log(Type.ERROR, type.getQualifiedSourceName() + + " must override either createRenderer or getRenderer"); + throw new UnableToCompleteException(); + } + JClassType rendererType = getRenderer.getReturnType().isClass(); + + bundle.setNeedsGwtConstructor(rendererType); + + // Also needs renderer type to find the right GWT constructor + bundle.setNeedsReturnType(type, getRenderer); + + logger.log(Type.DEBUG, "Renderer type of " + type + " is " + + rendererType); + } + } + + private void doPresentationType(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + JType presentationType = getPresentationType(type, logger); + bundle.setPresentationType(type, presentationType); + + bundle.setNeedsSerialize(presentationType); + + logger.log(Type.DEBUG, "Presentation type of " + type + " is " + + presentationType); + } + + private static JType getPresentationType(JClassType type, TreeLogger logger) + throws UnableToCompleteException { + JClassType originalType = type; + while (type != null) { + if (type.getQualifiedBinaryName().equals( + AbstractRendererConnector.class.getName())) { + JParameterizedType parameterized = type.isParameterized(); + if (parameterized == null) { + logger.log( + Type.ERROR, + type.getQualifiedSourceName() + + " must define the generic parameter of the inherited " + + AbstractRendererConnector.class + .getSimpleName()); + throw new UnableToCompleteException(); + } + return parameterized.getTypeArgs()[0]; + } + type = type.getSuperclass(); + } + throw new IllegalArgumentException("The type " + + originalType.getQualifiedSourceName() + " does not extend " + + AbstractRendererConnector.class.getName()); + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java new file mode 100644 index 0000000000..86ece28041 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.util.Set; + +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.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; +import com.vaadin.client.metadata.TypeDataStore.MethodAttribute; +import com.vaadin.shared.annotations.NoLoadingIndicator; +import com.vaadin.shared.annotations.Delayed; + +public class ServerRpcVisitor extends TypeVisitor { + @Override + public void visitServerRpc(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + ClientRpcVisitor.checkGenericType(logger, type); + bundle.setNeedsProxySupport(type); + + Set<? extends JClassType> superTypes = type + .getFlattenedSupertypeHierarchy(); + for (JClassType subType : superTypes) { + if (subType.isInterface() != null) { + JMethod[] methods = subType.getMethods(); + for (JMethod method : methods) { + ClientRpcVisitor.checkReturnType(logger, method); + + Delayed delayed = method.getAnnotation(Delayed.class); + if (delayed != null) { + bundle.setMethodAttribute(type, method, + MethodAttribute.DELAYED); + if (delayed.lastOnly()) { + bundle.setMethodAttribute(type, method, + MethodAttribute.LAST_ONLY); + } + } + + if (method.getAnnotation(NoLoadingIndicator.class) != null) { + bundle.setMethodAttribute(type, method, + MethodAttribute.NO_LOADING_INDICATOR); + } + + bundle.setNeedsParamTypes(type, method); + + JType[] parameterTypes = method.getParameterTypes(); + for (JType paramType : parameterTypes) { + bundle.setNeedsSerialize(paramType); + } + } + } + } + } +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/StateInitVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/StateInitVisitor.java new file mode 100644 index 0000000000..046c5c4611 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/StateInitVisitor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; + +public class StateInitVisitor extends TypeVisitor { + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) { + JMethod getState = ConnectorBundle + .findInheritedMethod(type, "getState"); + bundle.setNeedsReturnType(type, getState); + + bundle.setNeedsSerialize(getState.getReturnType()); + + JType stateType = getState.getReturnType(); + bundle.setNeedsGwtConstructor(stateType.isClass()); + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/TypeVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/TypeVisitor.java new file mode 100644 index 0000000000..028e4cc44d --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/TypeVisitor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +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.NotFoundException; +import com.google.gwt.core.ext.typeinfo.TypeOracle; + +public abstract class TypeVisitor { + public void init(TypeOracle oracle) throws NotFoundException { + // Default does nothing + } + + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + // Default does nothing + } + + public void visitClientRpc(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + // Default does nothing + } + + public void visitServerRpc(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + // Default does nothing + } + +} diff --git a/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/WidgetInitVisitor.java b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/WidgetInitVisitor.java new file mode 100644 index 0000000000..9a9cac18ba --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/server/widgetsetutils/metadata/WidgetInitVisitor.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.server.widgetsetutils.metadata; + +import java.util.Collection; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.annotations.DelegateToWidget; + +public class WidgetInitVisitor extends TypeVisitor { + + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) throws UnableToCompleteException { + if (ConnectorBundle.isConnectedComponentConnector(type)) { + // The class in which createWidget is implemented + JClassType createWidgetClass = ConnectorBundle.findInheritedMethod( + type, "createWidget").getEnclosingType(); + + JMethod getWidget = ConnectorBundle.findInheritedMethod(type, + "getWidget"); + JClassType widgetType = getWidget.getReturnType().isClass(); + + // Needs GWT constructor if createWidget is not overridden + if (createWidgetClass.getQualifiedSourceName().equals( + AbstractComponentConnector.class.getCanonicalName())) { + if (getWidget + .getEnclosingType() + .getQualifiedSourceName() + .equals(AbstractComponentConnector.class + .getCanonicalName())) { + logger.log(Type.ERROR, type.getQualifiedSourceName() + + " must override either createWidget or getWidget"); + throw new UnableToCompleteException(); + } + + bundle.setNeedsGwtConstructor(widgetType); + + // Also needs widget type to find the right GWT constructor + bundle.setNeedsReturnType(type, getWidget); + } + + // Check state properties for @DelegateToWidget + JMethod getState = ConnectorBundle.findInheritedMethod(type, + "getState"); + JClassType stateType = getState.getReturnType().isClass(); + + Collection<Property> properties = bundle.getProperties(stateType); + for (Property property : properties) { + DelegateToWidget delegateToWidget = property + .getAnnotation(DelegateToWidget.class); + if (delegateToWidget != null) { + // Generate meta data required for @DelegateToWidget + bundle.setNeedsDelegateToWidget(property, stateType); + + // Find the delegate target method + String methodName = DelegateToWidget.Helper + .getDelegateTarget(property.getName(), + delegateToWidget.value()); + JMethod delegatedSetter = ConnectorBundle + .findInheritedMethod(widgetType, methodName, + property.getPropertyType()); + if (delegatedSetter == null) { + logger.log( + Type.ERROR, + widgetType.getName() + + "." + + methodName + + "(" + + property.getPropertyType() + .getSimpleSourceName() + + ") required by @DelegateToWidget for " + + stateType.getName() + "." + + property.getName() + + " can not be found."); + throw new UnableToCompleteException(); + } + bundle.setNeedsInvoker(widgetType, delegatedSetter); + + // GWT code needs widget type to find the target method + bundle.setNeedsReturnType(type, getWidget); + } + } + + } + } +} diff --git a/client-compiler/src/main/java/com/vaadin/tools/CvalAddonsChecker.java b/client-compiler/src/main/java/com/vaadin/tools/CvalAddonsChecker.java new file mode 100644 index 0000000000..aab7231258 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/tools/CvalAddonsChecker.java @@ -0,0 +1,193 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.tools; + +import static com.vaadin.tools.CvalChecker.LINE; +import static com.vaadin.tools.CvalChecker.computeMajorVersion; +import static com.vaadin.tools.CvalChecker.getErrorMessage; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo; +import com.vaadin.tools.CvalChecker.CvalInfo; +import com.vaadin.tools.CvalChecker.CvalServer; +import com.vaadin.tools.CvalChecker.InvalidCvalException; +import com.vaadin.tools.CvalChecker.UnreachableCvalServerException; + +/** + * This class is able to visit all MANIFEST.MF files present in the classpath, + * filter by name, and check if the user has a valid license. + * + * Manifest files should have a few attributes indicating the license type of + * the addon: + * <ul> + * <li>Implementation-Version: 4.x.x + * <li>AdVaaName: addon_name + * <li>AdVaaLicen: cval, agpl, empty + * <li>AdVaaPkg: package of the widgets in this addon + * </ul> + * + * The class also have a method to check just one product. + * + * @since 7.3 + */ +public final class CvalAddonsChecker { + + // Manifest attributes + public static final String VAADIN_ADDON_LICENSE = "AdVaaLicen"; + public static final String VAADIN_ADDON_NAME = "AdVaaName"; + public static final String VAADIN_ADDON_WIDGETSET = "Vaadin-Widgetsets"; + public static final String VAADIN_ADDON_VERSION = "Implementation-Version"; + public static final String VAADIN_ADDON_TITLE = "Implementation-Title"; + + // License types + public static final String VAADIN_AGPL = "agpl"; + public static final String VAADIN_CVAL = "cval"; + + private CvalChecker cvalChecker = new CvalChecker(); + private String filterPattern; + + /** + * The constructor. + */ + public CvalAddonsChecker() { + setLicenseProvider(new CvalServer()); + setFilter(".*vaadin.*"); + } + + /** + * Visit all MANIFEST.MF files in the classpath validating licenses. + * + * Return a list of Cval licensed products in order to have enough info to + * generate nag messages in the UI. + */ + public List<CValUiInfo> run() throws InvalidCvalException { + List<CValUiInfo> ret = new ArrayList<CValUiInfo>(); + try { + // Visit all MANIFEST in our classpath + Enumeration<URL> manifests = Thread.currentThread() + .getContextClassLoader() + .getResources(JarFile.MANIFEST_NAME); + while (manifests.hasMoreElements()) { + try { + URL url = manifests.nextElement(); + // Discard manifests whose name does not match the filter + // pattern + if (!url.getPath().matches(filterPattern)) { + continue; + } + InputStream is = url.openStream(); + // Should never happen, but we don't want a NPE here + if (is == null) { + continue; + } + // Read manifest attributes + Manifest manifest = new Manifest(is); + Attributes attribs = manifest.getMainAttributes(); + String license = attribs.getValue(VAADIN_ADDON_LICENSE); + String name = attribs.getValue(VAADIN_ADDON_NAME); + String vers = attribs.getValue(VAADIN_ADDON_VERSION) == null ? "" + : attribs.getValue(VAADIN_ADDON_VERSION); + String title = attribs.getValue(VAADIN_ADDON_TITLE) == null ? name + : attribs.getValue(VAADIN_ADDON_TITLE); + + String widgetsets = attribs + .getValue(VAADIN_ADDON_WIDGETSET) == null ? name + : attribs.getValue(VAADIN_ADDON_WIDGETSET); + + if (name == null || license == null) { + continue; + } + if (VAADIN_AGPL.equals(license)) { + // For agpl version we print an info message + printAgplLicense(title, vers); + } else if (VAADIN_CVAL.equals(license)) { + // We only check cval licensed products + CvalInfo info; + try { + info = cvalChecker.validateProduct(name, vers, + title); + printValidLicense(info, title, vers); + } catch (UnreachableCvalServerException e) { + info = CvalChecker.parseJson("{'product':{'name':'" + + name + "'}}"); + printServerUnreachable(title, vers); + } + for (String w : widgetsets.split("[, ]+")) { + ret.add(new CValUiInfo(title, String + .valueOf(computeMajorVersion(vers)), w, + info.getType())); + } + } + } catch (IOException ignored) { + } + } + } catch (IOException ignored) { + } + return ret; + } + + /** + * Set the filter regexp of .jar names which we have to consider. + * + * default is '.*touchkit.*' + */ + public CvalAddonsChecker setFilter(String regexp) { + filterPattern = regexp; + return this; + } + + /* + * Change the license provider, only used in tests. + */ + protected CvalAddonsChecker setLicenseProvider(CvalServer p) { + cvalChecker.setLicenseProvider(p); + return this; + } + + private void printAgplLicense(String name, String version) { + System.out.println(LINE + "\n" + + getErrorMessage("agpl", name, computeMajorVersion(version)) + + "\n" + LINE); + } + + private void printServerUnreachable(String name, String version) { + System.out.println(LINE + + "\n" + + getErrorMessage("unreachable", name, + computeMajorVersion(version)) + "\n" + LINE); + } + + private void printValidLicense(CvalInfo info, String title, String version) { + String msg = info.getMessage(); + if (msg == null) { + String key = "evaluation".equals(info.getType()) ? "evaluation" + : "valid"; + msg = getErrorMessage(key, title, computeMajorVersion(version), + info.getLicensee()); + } + System.out.println("\n" + LINE + "\n" + msg + "\n" + LINE + "\n"); + } +} diff --git a/client-compiler/src/main/java/com/vaadin/tools/CvalChecker.java b/client-compiler/src/main/java/com/vaadin/tools/CvalChecker.java new file mode 100644 index 0000000000..9217781695 --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/tools/CvalChecker.java @@ -0,0 +1,510 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.tools; + +import static java.lang.Integer.parseInt; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.prefs.Preferences; + +import org.apache.commons.io.IOUtils; + +import elemental.json.JsonException; +import elemental.json.JsonNull; +import elemental.json.JsonObject; +import elemental.json.impl.JsonUtil; + +/** + * This class is able to validate the vaadin CVAL license. + * + * It reads the developer license file and asks the server to validate the + * licenseKey. If the license is invalid it throws an exception with the + * information about the problem and the server response. + * + * @since 7.3 + */ +public final class CvalChecker { + + /* + * Class used for binding the JSON gotten from server. + * + * It is not in a separate f le, so as it is easier to copy into any product + * which does not depend on vaadin core. + * + * We are using elemental.json in order not to use additional dependency + * like auto-beans, gson, etc. + */ + public static class CvalInfo { + + public static class Product { + private JsonObject o; + + public Product(JsonObject o) { + this.o = o; + } + + public String getName() { + return get(o, "name", String.class); + } + + public Integer getVersion() { + return get(o, "version", Integer.class); + } + } + + @SuppressWarnings("unchecked") + private static <T> T get(JsonObject o, String k, Class<T> clz) { + Object ret = null; + try { + if (o == null || o.get(k) == null + || o.get(k) instanceof JsonNull) { + return null; + } + if (clz == String.class) { + ret = o.getString(k); + } else if (clz == JsonObject.class) { + ret = o.getObject(k); + } else if (clz == Integer.class) { + ret = Integer.valueOf((int) o.getNumber(k)); + } else if (clz == Date.class) { + ret = new Date((long) o.getNumber(k)); + } else if (clz == Boolean.class) { + ret = o.getBoolean(k); + } + } catch (JsonException e) { + } + return (T) ret; + } + + private JsonObject o; + + private Product product; + + public CvalInfo(JsonObject o) { + this.o = o; + product = new Product(get(o, "product", JsonObject.class)); + } + + public Boolean getExpired() { + return get(o, "expired", Boolean.class); + } + + public Date getExpiredEpoch() { + return get(o, "expiredEpoch", Date.class); + } + + public String getLicensee() { + return get(o, "licensee", String.class); + } + + public String getLicenseKey() { + return get(o, "licenseKey", String.class); + } + + public String getMessage() { + return get(o, "message", String.class); + } + + public Product getProduct() { + return product; + } + + public String getType() { + return get(o, "type", String.class); + } + + public void setExpiredEpoch(Date expiredEpoch) { + o.put("expiredEpoch", expiredEpoch.getTime()); + } + + public void setMessage(String msg) { + o.put("message", msg); + } + + @Override + public String toString() { + return o.toString(); + } + + public boolean isLicenseExpired() { + return (getExpired() != null && getExpired()) + || (getExpiredEpoch() != null && getExpiredEpoch().before( + new Date())); + } + + public boolean isValidVersion(int majorVersion) { + return getProduct().getVersion() == null + || getProduct().getVersion() >= majorVersion; + + } + + private boolean isValidInfo(String name, String key) { + return getProduct() != null && getProduct().getName() != null + && getLicenseKey() != null + && getProduct().getName().equals(name) + && getLicenseKey().equals(key); + } + } + + /* + * The class with the method for getting json from server side. It is here + * and protected just for replacing it in tests. + */ + public static class CvalServer { + protected String licenseUrl = LICENSE_URL_PROD; + + String askServer(String productName, String productKey, int timeoutMs) + throws IOException { + String url = licenseUrl + productKey; + URLConnection con; + try { + // Send some additional info in the User-Agent string. + String ua = "Cval " + productName + " " + productKey + " " + + getFirstLaunch(); + for (String prop : Arrays.asList("java.vendor.url", + "java.version", "os.name", "os.version", "os.arch")) { + ua += " " + System.getProperty(prop, "-").replace(" ", "_"); + } + con = new URL(url).openConnection(); + con.setRequestProperty("User-Agent", ua); + con.setConnectTimeout(timeoutMs); + con.setReadTimeout(timeoutMs); + String r = IOUtils.toString(con.getInputStream()); + return r; + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } + } + + /* + * Get the GWT firstLaunch timestamp. + */ + String getFirstLaunch() { + try { + Class<?> clz = Class + .forName("com.google.gwt.dev.shell.CheckForUpdates"); + return Preferences.userNodeForPackage(clz).get("firstLaunch", + "-"); + } catch (ClassNotFoundException e) { + return "-"; + } + } + } + + /** + * Exception thrown when the user does not have a valid cval license. + */ + public static class InvalidCvalException extends Exception { + private static final long serialVersionUID = 1L; + public final CvalInfo info; + public final String name; + public final String key; + public final String version; + public final String title; + + public InvalidCvalException(String name, String version, String title, + String key, CvalInfo info) { + super(composeMessage(title, version, key, info)); + this.info = info; + this.name = name; + this.key = key; + this.version = version; + this.title = title; + } + + static String composeMessage(String title, String version, String key, + CvalInfo info) { + String msg = ""; + int majorVers = computeMajorVersion(version); + + if (info != null && !info.isValidVersion(majorVers)) { + msg = getErrorMessage("invalid", title, majorVers); + } else if (info != null && info.getMessage() != null) { + msg = info.getMessage().replace("\\n", "\n"); + } else if (info != null && info.isLicenseExpired()) { + String type = "evaluation".equals(info.getType()) ? "Evaluation license" + : "License"; + msg = getErrorMessage("expired", title, majorVers, type); + } else if (key == null) { + msg = getErrorMessage("none", title, majorVers); + } else { + msg = getErrorMessage("invalid", title, majorVers); + } + return msg; + } + } + + /** + * Exception thrown when the license server is unreachable + */ + public static class UnreachableCvalServerException extends Exception { + private static final long serialVersionUID = 1L; + public final String name; + + public UnreachableCvalServerException(String name, Exception e) { + super(e); + this.name = name; + } + } + + public static final String LINE = "----------------------------------------------------------------------------------------------------------------------"; + + static final int GRACE_DAYS_MSECS = 2 * 24 * 60 * 60 * 1000; + + private static final String LICENSE_URL_PROD = "https://tools.vaadin.com/vaadin-license-server/licenses/"; + + /* + * used in tests + */ + static void cacheLicenseInfo(CvalInfo info) { + if (info != null) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + if (info.toString().length() > Preferences.MAX_VALUE_LENGTH) { + // This should never happen since MAX_VALUE_LENGTH is big + // enough. + // But server could eventually send a very big message, so we + // discard it in cache and would use hard-coded messages. + info.setMessage(null); + } + p.put(info.getProduct().getName(), info.toString()); + } + } + + /* + * used in tests + */ + static void deleteCache(String productName) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + p.remove(productName); + } + + /** + * Given a product name returns the name of the file with the license key. + * + * Traditionally we have delivered license keys with a name like + * 'vaadin.touchkit.developer.license' but our database product name is + * 'vaadin-touchkit' so we have to replace '-' by '.' to maintain + * compatibility. + */ + static final String computeLicenseName(String productName) { + return productName.replace("-", ".") + ".developer.license"; + } + + static final int computeMajorVersion(String productVersion) { + return productVersion == null || productVersion.isEmpty() ? 0 + : parseInt(productVersion.replaceFirst("[^\\d]+.*$", "")); + } + + /* + * used in tests + */ + static CvalInfo parseJson(String json) { + if (json == null) { + return null; + } + try { + JsonObject o = JsonUtil.parse(json); + return new CvalInfo(o); + } catch (JsonException e) { + return null; + } + } + + private CvalServer provider; + + /** + * The constructor. + */ + public CvalChecker() { + setLicenseProvider(new CvalServer()); + } + + /** + * Validate whether there is a valid license key for a product. + * + * @param productName + * for example vaadin-touchkit + * @param productVersion + * for instance 4.0.1 + * @return CvalInfo Server response or cache response if server is offline + * @throws InvalidCvalException + * when there is no a valid license for the product + * @throws UnreachableCvalServerException + * when we have license key but server is unreachable + */ + public CvalInfo validateProduct(String productName, String productVersion, + String productTitle) throws InvalidCvalException, + UnreachableCvalServerException { + String key = getDeveloperLicenseKey(productName, productVersion, + productTitle); + + CvalInfo info = null; + if (key != null && !key.isEmpty()) { + info = getCachedLicenseInfo(productName); + if (info != null && !info.isValidInfo(productName, key)) { + deleteCache(productName); + info = null; + } + info = askLicenseServer(productName, key, productVersion, info); + if (info != null && info.isValidInfo(productName, key) + && info.isValidVersion(computeMajorVersion(productVersion)) + && !info.isLicenseExpired()) { + return info; + } + } + + throw new InvalidCvalException(productName, productVersion, + productTitle, key, info); + } + + /* + * Change the license provider, only used in tests. + */ + final CvalChecker setLicenseProvider(CvalServer p) { + provider = p; + return this; + } + + private CvalInfo askLicenseServer(String productName, String productKey, + String productVersion, CvalInfo info) + throws UnreachableCvalServerException { + + int majorVersion = computeMajorVersion(productVersion); + + // If we have a valid license info here, it means that we got it from + // cache. + // We add a grace time when so as if the server is unreachable + // we allow the user to use the product. + if (info != null && info.getExpiredEpoch() != null + && !"evaluation".equals(info.getType())) { + long ts = info.getExpiredEpoch().getTime() + GRACE_DAYS_MSECS; + info.setExpiredEpoch(new Date(ts)); + } + + boolean validCache = info != null + && info.isValidInfo(productName, productKey) + && info.isValidVersion(majorVersion) + && !info.isLicenseExpired(); + + // if we have a validCache we set the timeout smaller + int timeout = validCache ? 2000 : 10000; + + try { + CvalInfo srvinfo = parseJson(provider.askServer(productName + "-" + + productVersion, productKey, timeout)); + if (srvinfo != null && srvinfo.isValidInfo(productName, productKey) + && srvinfo.isValidVersion(majorVersion)) { + // We always cache the info if it is valid although it is + // expired + cacheLicenseInfo(srvinfo); + info = srvinfo; + } + } catch (FileNotFoundException e) { + // 404 + return null; + } catch (Exception e) { + if (info == null) { + throw new UnreachableCvalServerException(productName, e); + } + } + return info; + } + + private CvalInfo getCachedLicenseInfo(String productName) { + Preferences p = Preferences.userNodeForPackage(CvalInfo.class); + String json = p.get(productName, ""); + if (!json.isEmpty()) { + CvalInfo info = parseJson(json); + if (info != null) { + return info; + } + } + return null; + } + + private String getDeveloperLicenseKey(String productName, + String productVersion, String productTitle) + throws InvalidCvalException { + String licenseName = computeLicenseName(productName); + + String key = System.getProperty(licenseName); + if (key != null && !key.isEmpty()) { + return key; + } + + try { + String dotLicenseName = "." + licenseName; + String userHome = System.getProperty("user.home"); + for (URL url : new URL[] { + new File(userHome, dotLicenseName).toURI().toURL(), + new File(userHome, licenseName).toURI().toURL(), + URL.class.getResource("/" + dotLicenseName), + URL.class.getResource("/" + licenseName) }) { + if (url != null) { + try { + key = readKeyFromFile(url, + computeMajorVersion(productVersion)); + if (key != null && !(key = key.trim()).isEmpty()) { + return key; + } + } catch (IOException ignored) { + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + throw new InvalidCvalException(productName, productVersion, + productTitle, null, null); + } + + String readKeyFromFile(URL url, int majorVersion) throws IOException { + String majorVersionStr = String.valueOf(majorVersion); + List<String> lines = IOUtils.readLines(url.openStream()); + String defaultKey = null; + for (String line : lines) { + String[] parts = line.split("\\s*=\\s*"); + if (parts.length < 2) { + defaultKey = parts[0].trim(); + } + if (parts[0].equals(majorVersionStr)) { + return parts[1].trim(); + } + } + return defaultKey; + } + + static String getErrorMessage(String key, Object... pars) { + Locale loc = Locale.getDefault(); + ResourceBundle res = ResourceBundle.getBundle( + CvalChecker.class.getName(), loc); + String msg = res.getString(key); + return new MessageFormat(msg, loc).format(pars); + } +} diff --git a/client-compiler/src/main/java/com/vaadin/tools/WidgetsetCompiler.java b/client-compiler/src/main/java/com/vaadin/tools/WidgetsetCompiler.java new file mode 100755 index 0000000000..7c06e9d7af --- /dev/null +++ b/client-compiler/src/main/java/com/vaadin/tools/WidgetsetCompiler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.tools; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.server.widgetsetutils.WidgetSetBuilder; + +/** + * A wrapper for the GWT compiler that runs the compiler in a new thread after + * updating the widgetset file. + * + * This class originally existed to allow circumventing a J2SE 5.0 bug (6316197) + * that prevents setting the stack size for the main thread. + * + * This class takes the same command line arguments as the + * com.google.gwt.dev.Compiler class. + * + * A typical invocation would use e.g. the following arguments + * + * "-war WebContent/VAADIN/widgetsets com.vaadin.DefaultWidgetSet" + * + * In addition, larger memory usage settings for the VM should be used, e.g. + * + * "-Xms256M -Xmx512M -Xss8M" + * + * The source directory containing widgetset and related classes must be + * included in the classpath, as well as other relevant JARs. + * + * @deprecated with Java 6, can use com.google.gwt.dev.Compiler directly (also + * in Eclipse plug-in etc.) + */ +@Deprecated +public class WidgetsetCompiler { + + /** + * @param args + * same arguments as for com.google.gwt.dev.Compiler + */ + public static void main(final String[] args) { + try { + // run the compiler in a different thread to enable using the + // user-set stack size + + // on Windows, the default stack size is too small for the main + // thread and cannot be changed in JRE 1.5 (see + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6316197) + + Runnable runCompiler = new Runnable() { + @Override + public void run() { + try { + // GWTCompiler.main(args); + // avoid warnings + + String wsname = args[args.length - 1]; + + // TODO expecting this is launched via eclipse WTP + // project + System.out + .println("Updating GWT module description file..."); + WidgetSetBuilder.updateWidgetSet(wsname); + System.out.println("Done."); + + System.out.println("Starting GWT compiler"); + com.google.gwt.dev.Compiler.main(args); + } catch (Throwable thr) { + getLogger().log(Level.SEVERE, + "Widgetset compilation failed", thr); + } + } + }; + Thread runThread = new Thread(runCompiler); + runThread.start(); + runThread.join(); + System.out.println("Widgetset compilation finished"); + } catch (Throwable thr) { + getLogger().log(Level.SEVERE, "Widgetset compilation failed", thr); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(WidgetsetCompiler.class.getName()); + } +} |