001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.rebind.ui;
017:
018: import com.google.gwt.core.ext.Generator;
019: import com.google.gwt.core.ext.GeneratorContext;
020: import com.google.gwt.core.ext.TreeLogger;
021: import com.google.gwt.core.ext.UnableToCompleteException;
022: import com.google.gwt.core.ext.typeinfo.JClassType;
023: import com.google.gwt.core.ext.typeinfo.JMethod;
024: import com.google.gwt.core.ext.typeinfo.NotFoundException;
025: import com.google.gwt.core.ext.typeinfo.TypeOracle;
026: import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
027: import com.google.gwt.user.rebind.SourceWriter;
028:
029: import java.io.PrintWriter;
030: import java.net.URL;
031: import java.util.HashMap;
032: import java.util.Map;
033:
034: /**
035: * Generates an implementation of a user-defined interface <code>T</code> that
036: * extends {@link com.google.gwt.user.client.ui.ImageBundle}.
037: *
038: * Each method in <code>T</code> must be declared to return
039: * {@link com.google.gwt.user.client.ui.AbstractImagePrototype}, take no
040: * parameters, and optionally specify the metadata tag <code>gwt.resource</code>
041: * as the name of an image that can be found in the classpath. In the absence of
042: * the metatadata tag, the method name with an extension of
043: * <code>.png, .jpg, or .gif</code> defines the name of the image, and the
044: * image file must be located in the same package as <code>T</code>.
045: */
046: public class ImageBundleGenerator extends Generator {
047:
048: private static final String ABSTRACTIMAGEPROTOTYPE_QNAME = "com.google.gwt.user.client.ui.AbstractImagePrototype";
049:
050: private static final String CLIPPEDIMAGEPROTOTYPE_QNAME = "com.google.gwt.user.client.ui.impl.ClippedImagePrototype";
051:
052: private static final String GWT_QNAME = "com.google.gwt.core.client.GWT";
053:
054: private static final String[] IMAGE_FILE_EXTENSIONS = { "png",
055: "gif", "jpg" };
056:
057: private static final String IMAGEBUNDLE_QNAME = "com.google.gwt.user.client.ui.ImageBundle";
058:
059: private static final String METADATA_TAG = "gwt.resource";
060:
061: public ImageBundleGenerator() {
062: }
063:
064: @Override
065: public String generate(TreeLogger logger, GeneratorContext context,
066: String typeName) throws UnableToCompleteException {
067:
068: TypeOracle typeOracle = context.getTypeOracle();
069:
070: // Get metadata describing the user's class.
071: JClassType userType = getValidUserType(logger, typeName,
072: typeOracle);
073:
074: // Get the methods that correspond to constituent images.
075: JMethod[] imgMethods = getValidImageMethods(logger, userType);
076:
077: // Write the new class.
078: String resultName = generateImpl(logger, context, userType,
079: imgMethods);
080:
081: // Return the complete name of the generated class.
082: return resultName;
083: }
084:
085: private String computeSubclassName(JClassType userType) {
086: String baseName = userType.getName().replace('.', '_');
087: return baseName + "_generatedBundle";
088: }
089:
090: private void generateImageMethod(TreeLogger logger,
091: ImageBundleBuilder compositeImage, SourceWriter sw,
092: JMethod method) throws UnableToCompleteException {
093:
094: String imageName = getImageUrlFromMetaDataOrMethodName(logger,
095: method);
096: String decl = method.getReadableDeclaration(false, true, true,
097: true, true);
098:
099: {
100: sw.indent();
101:
102: // Create a singleton that this method can return. There is no need to
103: // create a new instance every time this method is called, since
104: // ClippedImagePrototype is immutable
105:
106: ImageBundleBuilder.ImageRect imageRect = compositeImage
107: .getMapping(imageName);
108: String singletonName = method.getName() + "_SINGLETON";
109:
110: sw.print("private static final ClippedImagePrototype ");
111: sw.print(singletonName);
112: sw.print(" = new ClippedImagePrototype(IMAGE_BUNDLE_URL, ");
113: sw.print(Integer.toString(imageRect.left));
114: sw.print(", 0, ");
115: sw.print(Integer.toString(imageRect.width));
116: sw.print(", ");
117: sw.print(Integer.toString(imageRect.height));
118: sw.println(");");
119:
120: sw.print(decl);
121: sw.println(" {");
122:
123: {
124: sw.indent();
125: sw.print("return ");
126: sw.print(singletonName);
127: sw.println(";");
128: sw.outdent();
129: }
130:
131: sw.println("}");
132: sw.outdent();
133: }
134: }
135:
136: private String generateImpl(TreeLogger logger,
137: GeneratorContext context, JClassType userType,
138: JMethod[] imageMethods) throws UnableToCompleteException {
139: // Compute the package and class names of the generated class.
140: String pkgName = userType.getPackage().getName();
141: String subName = computeSubclassName(userType);
142:
143: // Begin writing the generated source.
144: ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
145: pkgName, subName);
146: f.addImport(ABSTRACTIMAGEPROTOTYPE_QNAME);
147: f.addImport(CLIPPEDIMAGEPROTOTYPE_QNAME);
148: f.addImport(GWT_QNAME);
149: f.addImplementedInterface(userType.getQualifiedSourceName());
150:
151: PrintWriter pw = context.tryCreate(logger, pkgName, subName);
152: if (pw != null) {
153: SourceWriter sw = f.createSourceWriter(context, pw);
154:
155: // Build a compound image from each individual image.
156: ImageBundleBuilder bulder = new ImageBundleBuilder();
157:
158: for (JMethod method : imageMethods) {
159: String imageUrl = getImageUrlFromMetaDataOrMethodName(
160: logger, method);
161: assert (imageUrl != null);
162: bulder.assimilate(logger, imageUrl);
163: }
164:
165: // Write the compound image into the output directory.
166: String bundledImageUrl = bulder.writeBundledImage(logger,
167: context);
168:
169: // Emit a constant for the composite URL. Note that we prepend the
170: // module's base URL so that the module can reference its own resources
171: // independently of the host HTML page.
172: sw
173: .print("private static final String IMAGE_BUNDLE_URL = GWT.getModuleBaseURL() + \"");
174: sw.print(escape(bundledImageUrl));
175: sw.println("\";");
176:
177: // Generate an implementation of each method.
178: for (JMethod method : imageMethods) {
179: generateImageMethod(logger, bulder, sw, method);
180: }
181:
182: // Finish.
183: sw.commit(logger);
184: }
185:
186: return f.getCreatedClassName();
187: }
188:
189: // Assume this is only called for valid methods.
190: private String getImageUrlFromMetaDataOrMethodName(
191: TreeLogger logger, JMethod method)
192: throws UnableToCompleteException {
193:
194: String[][] md = method.getMetaData(METADATA_TAG);
195:
196: if (md.length == 1) {
197: // Metadata is available, so get the image url from the metadata
198: int lastTagIndex = md.length - 1;
199: int lastValueIndex = md[lastTagIndex].length - 1;
200: String imageNameFromMetaData = md[lastTagIndex][lastValueIndex];
201:
202: // Make sure the name is either absolute or package-relative.
203: if (imageNameFromMetaData.indexOf("/") == -1) {
204: String pkgName = method.getEnclosingType().getPackage()
205: .getName();
206: // This construction handles the default package correctly, too.
207: imageNameFromMetaData = pkgName.replace('.', '/') + "/"
208: + imageNameFromMetaData;
209: }
210:
211: // Make sure that the resource exists on the classpath. In the future,
212: // this code will have to be changed if images are loaded from the
213: // source path or public path.
214: URL imageResourceURL = getClass().getClassLoader()
215: .getResource(imageNameFromMetaData);
216: if (imageResourceURL == null) {
217: logger
218: .log(
219: TreeLogger.ERROR,
220: "Resource "
221: + imageNameFromMetaData
222: + " not found on classpath (is the name specified as Class.getResource() would expect?)",
223: null);
224: throw new UnableToCompleteException();
225: }
226:
227: return imageNameFromMetaData;
228: }
229:
230: String imageNameFromMethod = null;
231: String packageAndMethodName = method.getEnclosingType()
232: .getPackage().getName().replace('.', '/')
233: + '/' + method.getName();
234: // There is no metadata available, so the image url will be generated from
235: // the method name with an image file extension.
236: for (int i = 0; i < IMAGE_FILE_EXTENSIONS.length; i++) {
237: String possibleImageName = packageAndMethodName + '.'
238: + IMAGE_FILE_EXTENSIONS[i];
239: // Check to see if the resource exists on the classpath for each possible
240: // image file extension. This code will have to be changed if images are
241: // loaded from the source path or the public path.
242: URL imageResourceURL = getClass().getClassLoader()
243: .getResource(possibleImageName);
244: if (imageResourceURL != null) {
245: imageNameFromMethod = possibleImageName;
246: break;
247: }
248: }
249:
250: if (imageNameFromMethod == null) {
251:
252: StringBuffer errorStringBuf = new StringBuffer();
253: for (int i = 0; i < IMAGE_FILE_EXTENSIONS.length; i++) {
254:
255: errorStringBuf.append(IMAGE_FILE_EXTENSIONS[i]);
256:
257: if (i != IMAGE_FILE_EXTENSIONS.length - 1) {
258: errorStringBuf.append(", ");
259: }
260: }
261:
262: logger
263: .log(
264: TreeLogger.ERROR,
265: "Resource "
266: + packageAndMethodName
267: + ".("
268: + errorStringBuf.toString()
269: + ") not found on classpath (is the name specified as Class.getResource() would expect?)",
270: null);
271: throw new UnableToCompleteException();
272: }
273:
274: return imageNameFromMethod;
275: }
276:
277: private JMethod[] getValidImageMethods(TreeLogger logger,
278: JClassType userType) throws UnableToCompleteException {
279:
280: logger = logger.branch(TreeLogger.TRACE,
281: "Analyzing methods on "
282: + userType.getQualifiedSourceName(), null);
283:
284: final JClassType imageClass;
285: try {
286: imageClass = userType.getOracle().getType(
287: ABSTRACTIMAGEPROTOTYPE_QNAME);
288: } catch (NotFoundException e) {
289: logger.log(TreeLogger.ERROR, "GWT "
290: + ABSTRACTIMAGEPROTOTYPE_QNAME
291: + "class is not available", e);
292: throw new UnableToCompleteException();
293: }
294:
295: Map<JMethod, String> rejectedMethodsAndWhy = new HashMap<JMethod, String>();
296: JMethod[] leafMethods = userType.getOverridableMethods();
297: for (JMethod method : leafMethods) {
298: if (method.getReturnType() != imageClass) {
299: rejectedMethodsAndWhy.put(method,
300: "Return type must be "
301: + ABSTRACTIMAGEPROTOTYPE_QNAME);
302: continue;
303: }
304:
305: if (method.getParameters().length > 0) {
306: rejectedMethodsAndWhy.put(method,
307: "Method cannot take parameters");
308: continue;
309: }
310:
311: String[][] md = method.getMetaData(METADATA_TAG);
312: if ((md.length > 1)
313: || (md.length == 1 && md[0].length != 1)) {
314: rejectedMethodsAndWhy
315: .put(
316: method,
317: "Expecting either no metadata tags, or one metadata tag of the form '@gwt.resource <resource-name>'");
318: }
319: }
320:
321: // Make sure there aren't any invalid methods.
322: if (!rejectedMethodsAndWhy.isEmpty()) {
323: logger = logger
324: .branch(
325: TreeLogger.ERROR,
326: "The following methods are invalid on an image bundle:",
327: null);
328: for (Map.Entry<JMethod, String> entry : rejectedMethodsAndWhy
329: .entrySet()) {
330: JMethod badMethod = entry.getKey();
331: String reason = entry.getValue();
332: TreeLogger branch = logger.branch(TreeLogger.ERROR,
333: badMethod.getReadableDeclaration(), null);
334: branch.log(TreeLogger.ERROR, reason, null);
335: }
336: throw new UnableToCompleteException();
337: }
338:
339: return leafMethods;
340: }
341:
342: private JClassType getValidUserType(TreeLogger logger,
343: String typeName, TypeOracle typeOracle)
344: throws UnableToCompleteException {
345: try {
346: // Get the type that the user is introducing.
347: JClassType userType = typeOracle.getType(typeName);
348:
349: // Get the type this generator is designed to support.
350: JClassType magicType = typeOracle
351: .findType(IMAGEBUNDLE_QNAME);
352:
353: // Ensure it's an interface.
354: if (userType.isInterface() == null) {
355: logger.log(TreeLogger.ERROR, userType
356: .getQualifiedSourceName()
357: + " must be an interface", null);
358: throw new UnableToCompleteException();
359: }
360:
361: // Ensure proper derivation.
362: if (!userType.isAssignableTo(magicType)) {
363: logger.log(TreeLogger.ERROR, userType
364: .getQualifiedSourceName()
365: + " must be assignable to "
366: + magicType.getQualifiedSourceName(), null);
367: throw new UnableToCompleteException();
368: }
369:
370: return userType;
371:
372: } catch (NotFoundException e) {
373: logger.log(TreeLogger.ERROR,
374: "Unable to find required type(s)", e);
375: throw new UnableToCompleteException();
376: }
377: }
378:
379: }
|