Java tutorial
/** * Sencha GXT 4.0.0 - Sencha for GWT * Copyright (c) 2006-2015, Sencha Inc. * * licensing@sencha.com * http://www.sencha.com/products/gxt/license/ * * ================================================================================ * Open Source License * ================================================================================ * This version of Sencha GXT is licensed under the terms of the Open Source GPL v3 * license. You may use this license only if you are prepared to distribute and * share the source code of your application under the GPL v3 license: * http://www.gnu.org/licenses/gpl.html * * If you are NOT prepared to distribute and share the source code of your * application under the GPL v3 license, other commercial and oem licenses * are available for an alternate download of Sencha GXT. * * Please see the Sencha GXT Licensing page at: * http://www.sencha.com/products/gxt/license/ * * For clarification or additional options, please contact: * licensing@sencha.com * ================================================================================ * * * ================================================================================ * Disclaimer * ================================================================================ * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY, * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING. * ================================================================================ */ package com.sencha.gxt.core.rebind; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.gwt.core.client.GWT; 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.JParameter; import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.core.ext.typeinfo.TypeOracle; import com.google.gwt.dev.resource.Resource; import com.google.gwt.dev.util.Name; import com.google.gwt.dev.util.Util; import com.google.gwt.editor.rebind.model.ModelUtils; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; import com.google.gwt.user.rebind.SourceWriter; import com.sencha.gxt.core.client.XTemplates; import com.sencha.gxt.core.client.XTemplates.XTemplate; import com.sencha.gxt.core.rebind.ConditionParser.Token; import com.sencha.gxt.core.rebind.XTemplateParser.ContainerTemplateChunk; import com.sencha.gxt.core.rebind.XTemplateParser.ContentChunk; import com.sencha.gxt.core.rebind.XTemplateParser.ContentChunk.ContentType; import com.sencha.gxt.core.rebind.XTemplateParser.ControlChunk; import com.sencha.gxt.core.rebind.XTemplateParser.TemplateChunk; import com.sencha.gxt.core.rebind.XTemplateParser.TemplateModel; public class XTemplatesGenerator extends Generator { private JClassType xTemplatesInterface; private JClassType listInterface; private TreeLogger logger; @Override public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException { // make sure it is an interface TypeOracle oracle = context.getTypeOracle(); this.logger = logger; this.xTemplatesInterface = oracle.findType(Name.getSourceNameForClass(XTemplates.class)); this.listInterface = oracle.findType(Name.getSourceNameForClass(List.class)); JClassType toGenerate = oracle.findType(typeName).isInterface(); if (toGenerate == null) { logger.log(TreeLogger.ERROR, typeName + " is not an interface type"); throw new UnableToCompleteException(); } if (!toGenerate.isAssignableTo(xTemplatesInterface)) { logger.log(Type.ERROR, "This isn't a XTemplates subtype..."); throw new UnableToCompleteException(); } // Get the name of the new type String packageName = toGenerate.getPackage().getName(); String simpleSourceName = toGenerate.getName().replace('.', '_') + "Impl"; PrintWriter pw = context.tryCreate(logger, packageName, simpleSourceName); if (pw == null) { return packageName + "." + simpleSourceName; } ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(packageName, simpleSourceName); factory.addImplementedInterface(typeName); // imports factory.addImport(Name.getSourceNameForClass(GWT.class)); factory.addImport(Name.getSourceNameForClass(SafeHtml.class)); factory.addImport(Name.getSourceNameForClass(SafeHtmlBuilder.class)); // Loop through the formatters declared for this type and supertypes FormatCollector formatters = new FormatCollector(context, logger, toGenerate); MethodCollector invokables = new MethodCollector(context, logger, toGenerate); SourceWriter sw = factory.createSourceWriter(context, pw); for (JMethod method : toGenerate.getOverridableMethods()) { TreeLogger l = logger.branch(Type.DEBUG, "Creating XTemplate method " + method.getName()); final String template; XTemplate marker = method.getAnnotation(XTemplate.class); if (marker == null) { l.log(Type.ERROR, "Unable to create template for method " + method.getReadableDeclaration() + ", this may cause other failures."); continue; } else { if (marker.source().length() != 0) { if (marker.value().length() != 0) { l.log(Type.WARN, "Found both source file and inline template, using source file"); } InputStream stream = getTemplateResource(context, method.getEnclosingType(), l, marker.source()); if (stream == null) { l.log(Type.ERROR, "No data could be loaded - no data at path " + marker.source()); throw new UnableToCompleteException(); } template = Util.readStreamAsString(stream); } else if (marker.value().length() != 0) { template = marker.value(); } else { l.log(Type.ERROR, "XTemplate annotation found with no contents, cannot generate method " + method.getName() + ", this may cause other failures."); continue; } } XTemplateParser p = new XTemplateParser( l.branch(Type.DEBUG, "Parsing provided template for " + method.getReadableDeclaration())); TemplateModel m = p.parse(template); SafeHtmlTemplatesCreator safeHtml = new SafeHtmlTemplatesCreator(context, l.branch(Type.DEBUG, "Building SafeHtmlTemplates"), method); sw.println(method.getReadableDeclaration(false, true, true, false, true) + "{"); sw.indent(); Map<String, JType> params = new HashMap<String, JType>(); for (JParameter param : method.getParameters()) { params.put(param.getName(), param.getType()); } Context scopeContext = new Context(context, l, params, formatters); // if there is only one parameter, wrap the scope up so that properties // can be accessed directly if (method.getParameters().length == 1) { JParameter param = method.getParameters()[0]; scopeContext = new Context(scopeContext, param.getName(), param.getType()); } String outerSHVar = scopeContext.declareLocalVariable("outer"); sw.println("SafeHtml %1$s;", outerSHVar); buildSafeHtmlTemplates(outerSHVar, sw, m, safeHtml, scopeContext, invokables); sw.println("return %1$s;", outerSHVar); sw.outdent(); sw.println("}"); safeHtml.create(); } // Save the file and return its type name sw.commit(logger); return factory.getCreatedClassName(); } protected InputStream getTemplateResource(GeneratorContext context, JClassType toGenerate, TreeLogger l, String markerPath) throws UnableToCompleteException { // look for a local file first // TODO remove this assumption String path = slashify(toGenerate.getPackage().getName()) + "/" + markerPath; Resource res = context.getResourcesOracle().getResource(path); // if not a local path, try an absolute one if (res == null) { URL url = Thread.currentThread().getContextClassLoader().getResource(markerPath); if (url == null) { return null; } try { return url.openStream(); } catch (IOException e) { logger.log(Type.ERROR, "IO Exception occured", e); throw new UnableToCompleteException(); } } try { return res.openContents(); } catch (Exception e) { logger.log(Type.ERROR, "Exception occured reading " + path, e); throw new UnableToCompleteException(); } } private static String slashify(String s) { return s.replace(".", "/"); } /** * Handles a given template chunk container by creating a method in the * safeHtmlTemplates impl * * @param sw the current sourcewriter * @param wrapper the chunk container to act recursively on * @param safeHtml creator to add new SafeHtml calls to * @param scopeContext current scope to make method calls to * @param invokables * @throws UnableToCompleteException */ private void buildSafeHtmlTemplates(String safeHtmlVar, SourceWriter sw, ContainerTemplateChunk wrapper, SafeHtmlTemplatesCreator safeHtml, Context scopeContext, MethodCollector invokables) throws UnableToCompleteException { // debugging section to see what is about to be printed sw.beginJavaDocComment(); sw.print(wrapper.toString()); sw.endJavaDocComment(); // make a new interface method for this content StringBuilder sb = new StringBuilder(); List<String> paramTypes = new ArrayList<String>(); List<String> params = new ArrayList<String>(); // write out children to local vars or to the template int argCount = 0; for (TemplateChunk chunk : wrapper.children) { if (chunk instanceof ContentChunk) { ContentChunk contentChunk = (ContentChunk) chunk; // build up the template if (contentChunk.type == ContentType.LITERAL) { sb.append(contentChunk.content); } else if (contentChunk.type == ContentType.CODE) { sb.append("{").append(argCount++).append("}"); paramTypes.add("java.lang.String"); StringBuffer expr = new StringBuffer("\"\" + ("); // parse out the quoted string literals first Matcher str = Pattern.compile("\"[^\"]+\"").matcher(contentChunk.content); TreeLogger code = logger.branch(Type.DEBUG, "Parsing code segment: \"" + contentChunk.content + "\""); int lastMatchEnd = 0; while (str.find()) { int begin = str.start(), end = str.end(); String escapedString = str.group(); String unmatched = contentChunk.content.substring(lastMatchEnd, begin); appendCodeBlockOperatorOrIdentifier(scopeContext, expr, code, unmatched); expr.append(escapedString); lastMatchEnd = end; } //finish rest of non-string-lit expression appendCodeBlockOperatorOrIdentifier(scopeContext, expr, code, contentChunk.content.substring(lastMatchEnd)); params.add(expr.append(")").toString()); code.log(Type.DEBUG, "Final compiled expression: " + expr); } else if (contentChunk.type == ContentType.REFERENCE) { sb.append("{").append(argCount++).append("}"); JType argType = scopeContext.getType(contentChunk.content); if (argType == null) { logger.log(Type.ERROR, "Reference could not be found: '" + contentChunk.content + "'. Please fix the expression in your template."); throw new UnableToCompleteException(); } paramTypes.add(argType.getParameterizedQualifiedSourceName()); params.add(scopeContext.deref(contentChunk.content)); } else { assert false : "Content type not supported + " + contentChunk.type; } } else if (chunk instanceof ControlChunk) { ControlChunk controlChunk = (ControlChunk) chunk; // build logic, get scoped name boolean hasIf = controlChunk.controls.containsKey("if"); boolean hasFor = controlChunk.controls.containsKey("for"); if (!hasIf && !hasFor) { logger.log(Type.ERROR, "<tpl> tag did not define a 'for' or 'if' attribute!"); throw new UnableToCompleteException(); } // declare a sub-template, and stash content in there, interleaving it // into the current template String subTemplate = scopeContext.declareLocalVariable("subTemplate"); String templateInBlock = scopeContext.declareLocalVariable("innerTemplate"); sb.append("{").append(argCount++).append("}"); paramTypes.add("com.google.gwt.safehtml.shared.SafeHtml"); params.add(subTemplate); sw.println("SafeHtml %1$s;", subTemplate); sw.println("SafeHtmlBuilder %1$s_builder = new SafeHtmlBuilder();", subTemplate); // find the context that should be passed to the child template final Context childScope; // if we have both for and if, if needs to wrap the for if (hasIf) { ConditionParser p = new ConditionParser(logger); List<Token> tokens = p.parse(controlChunk.controls.get("if")); StringBuilder condition = new StringBuilder(); for (Token t : tokens) { switch (t.type) { case ExpressionLiteral: condition.append(t.contents); break; case MethodInvocation: Matcher invoke = Pattern.compile("([a-zA-Z0-9\\._]+)\\:([a-zA-Z0-9_]+)\\(([^\\)]*)\\)") .matcher(t.contents); invoke.matches(); String deref = scopeContext.deref(invoke.group(1)); String methodName = invoke.group(2); String args = ""; for (String a : invoke.group(3).split(",")) { String possible = scopeContext.deref(a); args += possible == null ? a : possible; } condition.append(invokables.getMethodInvocation(methodName, deref, args)); break; case Reference: condition.append("(").append(scopeContext.deref(t.contents)).append(")"); break; default: logger.log(Type.ERROR, "Unexpected token type: " + t.type); throw new UnableToCompleteException(); } } sw.println("if (%1$s) {", condition.toString()); sw.indent(); } // if there is a for, print it out, and change scope if (hasFor) { String loopRef = controlChunk.controls.get("for"); JType collectionType = scopeContext.getType(loopRef); if (collectionType == null) { logger.log(Type.ERROR, "Reference in 'for' attribute could not be found: '" + loopRef + "'. Please fix the expression in your template."); throw new UnableToCompleteException(); } final JType localType;// type accessed within the loop final String localAccessor;// expr to access looped instance, where // %1$s is the loop obj, and %2$s is the // int index if (collectionType.isArray() != null) { localType = collectionType.isArray().getComponentType(); localAccessor = "%1$s[%2$s]"; } else {// List subtype localType = ModelUtils.findParameterizationOf(listInterface, collectionType.isClassOrInterface())[0]; localAccessor = "%1$s.get(%2$s)"; } String loopVar = scopeContext.declareLocalVariable("i"); // make sure the collection isnt null sw.println("if (%1$s != null) {", scopeContext.deref(loopRef)); sw.indent(); sw.println("for (int %1$s = 0; %1$s < %2$s; %1$s++) {", loopVar, scopeContext.derefCount(loopRef)); String itemExpr = String.format(localAccessor, scopeContext.deref(loopRef), loopVar); childScope = new Context(scopeContext, itemExpr, localType); childScope.setCountVar(loopVar); sw.indent(); } else { // if no for, use the same scope as the outer content childScope = scopeContext; } // generate a subtemplate, insert that sw.println("SafeHtml %1$s;", templateInBlock); buildSafeHtmlTemplates(templateInBlock, sw, controlChunk, safeHtml, childScope, invokables); sw.println("%1$s_builder.append(%2$s);", subTemplate, templateInBlock); // close up the blocks if (hasFor) { sw.outdent(); sw.println("}"); sw.outdent(); sw.println("}"); } if (hasIf) { sw.outdent(); sw.println("}"); } sw.println("%1$s = %1$s_builder.toSafeHtml();", subTemplate); } else { assert false : "Unsupported chunk type: " + chunk.getClass(); } } String methodName = safeHtml.addTemplate(sb.toString(), paramTypes); sw.beginJavaDocComment(); sw.println("safehtml content:"); sw.indent(); sw.println(sb.toString()); sw.outdent(); sw.println("params:"); sw.indent(); sw.print(args(params)); sw.outdent(); sw.endJavaDocComment(); sw.println("%4$s = %1$s.%2$s(%3$s);", safeHtml.getInstanceExpression(), methodName, args(params), safeHtmlVar); } /** * Walks the code block string given and replaces all possible variables (identified by * {@code [a-zA-Z_]+[a-zA-Z0-9_]*(:?\.[a-zA-Z_]+[a-zA-Z0-9_]*)*}) with the deref'd java expression. * Any other content (operators, and possibly numeric literals) are appended as is. * * This potentially will have an issue with {@code null}, {@code true}, {@code false} etc values. * However, no earlier version correctly handled those cases, so not going to worry about it for * now. The real fix is to stop using just regular expressions and switch to something a little * more powerful. * * @param context the current scope context * @param expr the expression being built up, that java content should be appended to * @param logger * @param nonStringLit the current expression from the xtemplate * @throws UnableToCompleteException if something looking like a variable appears that can't be deref'd */ private static void appendCodeBlockOperatorOrIdentifier(Context context, StringBuffer expr, TreeLogger logger, String nonStringLit) throws UnableToCompleteException { Matcher m = Pattern.compile("(:?[a-zA-Z_]+[a-zA-Z0-9_]*(:?\\.[a-zA-Z_]+[a-zA-Z0-9_]*)*|#)") .matcher(nonStringLit); while (m.find()) { String ref = m.group(); String deref = context.deref(ref); if (deref == null) { logger.log(Type.ERROR, "Reference could not be found: '" + ref + "'."); throw new UnableToCompleteException(); } logger.log(Type.DEBUG, "Replaced " + ref + " with " + deref); m.appendReplacement(expr, deref); } m.appendTail(expr); } /** * Builds an arg list ready to be passed into a method invocation. Effectively * is params.join(', ') * * @param params */ private String args(List<String> params) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < params.size(); i++) { if (i != 0) { sb.append(", "); } sb.append(params.get(i)); } return sb.toString(); } }