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.dev.shell;
017:
018: import com.google.gwt.core.ext.GeneratorContext;
019: import com.google.gwt.core.ext.PropertyOracle;
020: import com.google.gwt.core.ext.TreeLogger;
021: import com.google.gwt.core.ext.UnableToCompleteException;
022: import com.google.gwt.core.ext.typeinfo.CompilationUnitProvider;
023: import com.google.gwt.core.ext.typeinfo.JClassType;
024: import com.google.gwt.core.ext.typeinfo.NotFoundException;
025: import com.google.gwt.core.ext.typeinfo.TypeOracle;
026: import com.google.gwt.dev.cfg.PublicOracle;
027: import com.google.gwt.dev.jdt.CacheManager;
028: import com.google.gwt.dev.jdt.StaticCompilationUnitProvider;
029: import com.google.gwt.dev.jdt.TypeOracleBuilder;
030: import com.google.gwt.dev.jdt.URLCompilationUnitProvider;
031: import com.google.gwt.dev.util.Util;
032:
033: import java.io.ByteArrayOutputStream;
034: import java.io.CharArrayWriter;
035: import java.io.File;
036: import java.io.OutputStream;
037: import java.io.PrintWriter;
038: import java.net.MalformedURLException;
039: import java.net.URL;
040: import java.util.ArrayList;
041: import java.util.HashSet;
042: import java.util.IdentityHashMap;
043: import java.util.Iterator;
044: import java.util.List;
045: import java.util.Map;
046: import java.util.Set;
047:
048: /**
049: * An abstract implementation of a generator context in terms of a
050: * {@link com.google.gwt.dev.jdt.MutableCompilationServiceHost}, a
051: * {@link com.google.gwt.dev.jdt.PropertyOracle}, and a
052: * {@link com.google.gwt.core.server.typeinfo.TypeOracle}. The generator
053: * interacts with the mutable source oracle by increasing the available
054: * compilation units as they are generated.
055: */
056: public class StandardGeneratorContext implements GeneratorContext {
057:
058: /**
059: * This compilation unit provider acts as a normal compilation unit provider
060: * as well as a buffer into which generators can write their source. A
061: * controller should ensure that source isn't requested until the generator
062: * has finished writing it.
063: */
064: private static class GeneratedCompilationUnitProvider extends
065: StaticCompilationUnitProvider {
066:
067: public CharArrayWriter caw;
068:
069: public PrintWriter pw;
070:
071: public char[] source;
072:
073: public GeneratedCompilationUnitProvider(String packageName,
074: String simpleTypeName) {
075: super (packageName, simpleTypeName, null);
076: caw = new CharArrayWriter();
077: pw = new PrintWriter(caw, true);
078: }
079:
080: /**
081: * Finalizes the source and adds this compilation unit to the host.
082: */
083: public void commit() {
084: source = caw.toCharArray();
085: pw.close();
086: pw = null;
087: caw.close();
088: caw = null;
089: }
090:
091: @Override
092: public char[] getSource() {
093: if (source == null) {
094: throw new IllegalStateException("source not committed");
095: }
096: return source;
097: }
098: }
099:
100: /**
101: * {@link CompilationUnitProvider} used to represent generated source code
102: * which is stored on disk. This class is only used if the -gen flag is
103: * specified.
104: */
105: private static final class GeneratedCUP extends
106: URLCompilationUnitProvider {
107: private GeneratedCUP(URL url, String name) {
108: super (url, name);
109: }
110:
111: @Override
112: public long getLastModified() throws UnableToCompleteException {
113: // Make it seem really old so it won't cause recompiles.
114: //
115: return 0L;
116: }
117:
118: @Override
119: public boolean isTransient() {
120: return true;
121: }
122: }
123:
124: /**
125: * Manages a resource that is in the process of being created by a generator.
126: */
127: private static class PendingResource {
128:
129: private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
130: private final String partialPath;
131: private final File pendingFile;
132:
133: public PendingResource(File outDir, String partialPath) {
134: this .partialPath = partialPath;
135: this .pendingFile = new File(outDir, partialPath);
136: }
137:
138: public void commit(TreeLogger logger)
139: throws UnableToCompleteException {
140: logger = logger
141: .branch(TreeLogger.TRACE,
142: "Writing generated resource '"
143: + pendingFile.getAbsolutePath()
144: + "'", null);
145:
146: Util.writeBytesToFile(logger, pendingFile, baos
147: .toByteArray());
148: }
149:
150: public File getFile() {
151: return pendingFile;
152: }
153:
154: public OutputStream getOutputStream() {
155: return baos;
156: }
157:
158: public String getPartialPath() {
159: return partialPath;
160: }
161: }
162:
163: private final CacheManager cacheManager;
164:
165: private final Set<GeneratedCompilationUnitProvider> committedGeneratedCups = new HashSet<GeneratedCompilationUnitProvider>();
166:
167: private final File genDir;
168:
169: private final Set<String> generatedTypeNames = new HashSet<String>();
170:
171: private final File outDir;
172:
173: private final Map<OutputStream, PendingResource> pendingResourcesByOutputStream = new IdentityHashMap<OutputStream, PendingResource>();
174:
175: private final PropertyOracle propOracle;
176:
177: private final PublicOracle publicOracle;
178:
179: private final TypeOracle typeOracle;
180:
181: private final Map<PrintWriter, GeneratedCompilationUnitProvider> uncommittedGeneratedCupsByPrintWriter = new IdentityHashMap<PrintWriter, GeneratedCompilationUnitProvider>();
182:
183: /**
184: * Normally, the compiler host would be aware of the same types that are
185: * available in the supplied type oracle although it isn't strictly required.
186: */
187: public StandardGeneratorContext(TypeOracle typeOracle,
188: PropertyOracle propOracle, PublicOracle publicOracle,
189: File genDir, File outDir, CacheManager cacheManager) {
190: this .typeOracle = typeOracle;
191: this .propOracle = propOracle;
192: this .publicOracle = publicOracle;
193: this .genDir = genDir;
194: this .outDir = outDir;
195: this .cacheManager = cacheManager;
196: }
197:
198: /**
199: * Commits a pending generated type.
200: */
201: public final void commit(TreeLogger logger, PrintWriter pw) {
202: GeneratedCompilationUnitProvider gcup = uncommittedGeneratedCupsByPrintWriter
203: .get(pw);
204: if (gcup != null) {
205: gcup.commit();
206: uncommittedGeneratedCupsByPrintWriter.remove(pw);
207: committedGeneratedCups.add(gcup);
208: } else {
209: logger
210: .log(
211: TreeLogger.WARN,
212: "Generator attempted to commit an unknown PrintWriter",
213: null);
214: }
215: }
216:
217: public void commitResource(TreeLogger logger, OutputStream os)
218: throws UnableToCompleteException {
219:
220: // Find the pending resource using its output stream as a key.
221: PendingResource pendingResource = pendingResourcesByOutputStream
222: .get(os);
223: if (pendingResource != null) {
224: // Actually write the bytes to disk.
225: pendingResource.commit(logger);
226: cacheManager.addGeneratedResource(pendingResource
227: .getPartialPath());
228:
229: // The resource is now no longer pending, so remove it from the map.
230: // If the commit above throws an exception, it's okay to leave the entry
231: // in the map because it will be reported later as not having been
232: // committed, which is accurate.
233: pendingResourcesByOutputStream.remove(os);
234: } else {
235: logger
236: .log(
237: TreeLogger.WARN,
238: "Generator attempted to commit an unknown OutputStream",
239: null);
240: throw new UnableToCompleteException();
241: }
242: }
243:
244: /**
245: * Call this whenever generators are known to not be running to clear out
246: * uncommitted compilation units and to force committed compilation units to
247: * be parsed and added to the type oracle.
248: *
249: * @return types generated during this object's lifetime
250: */
251: public final JClassType[] finish(TreeLogger logger)
252: throws UnableToCompleteException {
253:
254: abortUncommittedResources(logger);
255:
256: // Process pending generated types.
257: List<String> genTypeNames = new ArrayList<String>();
258:
259: try {
260: TreeLogger branch;
261: if (!committedGeneratedCups.isEmpty()) {
262: // Assimilate the new types into the type oracle.
263: //
264: String msg = "Assimilating generated source";
265: branch = logger.branch(TreeLogger.DEBUG, msg, null);
266:
267: TreeLogger subBranch = null;
268: if (branch.isLoggable(TreeLogger.DEBUG)) {
269: subBranch = branch.branch(TreeLogger.DEBUG,
270: "Generated source files...", null);
271: }
272:
273: assert (cacheManager.getTypeOracle() == typeOracle);
274: TypeOracleBuilder builder = new TypeOracleBuilder(
275: cacheManager);
276: for (Iterator<GeneratedCompilationUnitProvider> iter = committedGeneratedCups
277: .iterator(); iter.hasNext();) {
278: GeneratedCompilationUnitProvider gcup = iter.next();
279: String typeName = gcup.getTypeName();
280: String genTypeName = gcup.getPackageName() + "."
281: + typeName;
282: genTypeNames.add(genTypeName);
283: CompilationUnitProvider cup = writeSource(logger,
284: gcup, typeName);
285: builder.addCompilationUnit(cup);
286: cacheManager.addGeneratedCup(cup);
287:
288: if (subBranch != null) {
289: subBranch.log(TreeLogger.DEBUG, cup
290: .getLocation(), null);
291: }
292: }
293:
294: builder.build(branch);
295: }
296:
297: // Return the generated types.
298: JClassType[] genTypes = new JClassType[genTypeNames.size()];
299: int next = 0;
300: for (Iterator<String> iter = genTypeNames.iterator(); iter
301: .hasNext();) {
302: String genTypeName = iter.next();
303: try {
304: genTypes[next++] = typeOracle.getType(genTypeName);
305: } catch (NotFoundException e) {
306: String msg = "Unable to find recently-generated type '"
307: + genTypeName;
308: logger.log(TreeLogger.ERROR, msg, null);
309: throw new UnableToCompleteException();
310: }
311: }
312: return genTypes;
313: } finally {
314:
315: // Remind the user if there uncommitted cups.
316: if (!uncommittedGeneratedCupsByPrintWriter.isEmpty()) {
317: String msg = "For the following type(s), generated source was never committed (did you forget to call commit()?)";
318: logger = logger.branch(TreeLogger.WARN, msg, null);
319:
320: for (Iterator<GeneratedCompilationUnitProvider> iter = uncommittedGeneratedCupsByPrintWriter
321: .values().iterator(); iter.hasNext();) {
322: StaticCompilationUnitProvider cup = iter.next();
323: String typeName = cup.getPackageName() + "."
324: + cup.getTypeName();
325: logger.log(TreeLogger.WARN, typeName, null);
326: }
327: }
328:
329: uncommittedGeneratedCupsByPrintWriter.clear();
330: committedGeneratedCups.clear();
331: generatedTypeNames.clear();
332: }
333: }
334:
335: public File getOutputDir() {
336: return outDir;
337: }
338:
339: public final PropertyOracle getPropertyOracle() {
340: return propOracle;
341: }
342:
343: public final TypeOracle getTypeOracle() {
344: return typeOracle;
345: }
346:
347: public final PrintWriter tryCreate(TreeLogger logger,
348: String packageName, String simpleTypeName) {
349: String typeName = packageName + "." + simpleTypeName;
350:
351: // Is type already known to the host?
352: JClassType existingType = typeOracle.findType(packageName,
353: simpleTypeName);
354: if (existingType != null) {
355: logger.log(TreeLogger.DEBUG, "Type '" + typeName
356: + "' already exists and will not be re-created ",
357: null);
358: return null;
359: }
360:
361: // Has anybody tried to create this type during this iteration?
362: if (generatedTypeNames.contains(typeName)) {
363: final String msg = "A request to create type '"
364: + typeName
365: + "' was received while the type itself was being created; this might be a generator or configuration bug";
366: logger.log(TreeLogger.WARN, msg, null);
367: return null;
368: }
369:
370: // The type isn't there, so we can let the caller create it. Remember that
371: // it is pending so another attempt to create the same type will fail.
372: GeneratedCompilationUnitProvider gcup = new GeneratedCompilationUnitProvider(
373: packageName, simpleTypeName);
374: uncommittedGeneratedCupsByPrintWriter.put(gcup.pw, gcup);
375: generatedTypeNames.add(typeName);
376:
377: return gcup.pw;
378: }
379:
380: public OutputStream tryCreateResource(TreeLogger logger,
381: String partialPath) throws UnableToCompleteException {
382:
383: logger = logger.branch(TreeLogger.DEBUG,
384: "Preparing pending output resource '" + partialPath
385: + "'", null);
386:
387: // Disallow null or empty names.
388: if (partialPath == null || partialPath.trim().equals("")) {
389: logger.log(TreeLogger.ERROR,
390: "The resource name must be a non-empty string",
391: null);
392: throw new UnableToCompleteException();
393: }
394:
395: // Disallow absolute paths.
396: File f = new File(partialPath);
397: if (f.isAbsolute()) {
398: logger
399: .log(
400: TreeLogger.ERROR,
401: "Resource paths are intended to be relative to the compiled output directory and cannot be absolute",
402: null);
403: throw new UnableToCompleteException();
404: }
405:
406: // Disallow backslashes (to promote consistency in calling code).
407: if (partialPath.indexOf('\\') >= 0) {
408: logger
409: .log(
410: TreeLogger.ERROR,
411: "Resource paths must contain forward slashes (not backslashes) to denote subdirectories",
412: null);
413: throw new UnableToCompleteException();
414: }
415:
416: // Check for public path collision.
417: if (publicOracle.findPublicFile(partialPath) != null) {
418: logger.log(TreeLogger.WARN, "Cannot create resource '"
419: + partialPath
420: + "' because it already exists on the public path",
421: null);
422: return null;
423: }
424:
425: // See if the file is already committed.
426: if (cacheManager.hasGeneratedResource(partialPath)) {
427: return null;
428: }
429:
430: // See if the file is pending.
431: for (Iterator<PendingResource> iter = pendingResourcesByOutputStream
432: .values().iterator(); iter.hasNext();) {
433: PendingResource pendingResource = iter.next();
434: if (pendingResource.getPartialPath().equals(partialPath)) {
435: // It is already pending.
436: logger.log(TreeLogger.WARN, "The file '" + partialPath
437: + "' is already a pending resource", null);
438: return null;
439: }
440: }
441:
442: // Record that this file is pending.
443: PendingResource pendingResource = new PendingResource(outDir,
444: partialPath);
445: OutputStream os = pendingResource.getOutputStream();
446: pendingResourcesByOutputStream.put(os, pendingResource);
447:
448: return os;
449: }
450:
451: private void abortUncommittedResources(TreeLogger logger) {
452: if (pendingResourcesByOutputStream.isEmpty()) {
453: // Nothing to do.
454: return;
455: }
456:
457: // Warn the user about uncommitted resources.
458: logger = logger
459: .branch(
460: TreeLogger.WARN,
461: "The following resources will not be created because they were never committed (did you forget to call commit()?)",
462: null);
463:
464: try {
465: for (Iterator<PendingResource> iter = pendingResourcesByOutputStream
466: .values().iterator(); iter.hasNext();) {
467: PendingResource pendingResource = iter.next();
468: logger.log(TreeLogger.WARN, pendingResource.getFile()
469: .getAbsolutePath(), null);
470: }
471: } finally {
472: pendingResourcesByOutputStream.clear();
473: }
474: }
475:
476: /**
477: * Writes the source of the specified compilation unit to disk if a gen
478: * directory is specified.
479: *
480: * @param cup the compilation unit whose contents might need to be written
481: * @param simpleTypeName the fully-qualified type name
482: * @return a wrapper for the existing cup with a proper location
483: */
484: private CompilationUnitProvider writeSource(TreeLogger logger,
485: CompilationUnitProvider cup, String simpleTypeName)
486: throws UnableToCompleteException {
487:
488: if (genDir == null) {
489: // No place to write it.
490: return cup;
491: }
492:
493: if (Util.isCompilationUnitOnDisk(cup.getLocation())) {
494: // Already on disk.
495: return cup;
496: }
497:
498: // Let's do write it.
499: String typeName = cup.getPackageName() + "." + simpleTypeName;
500: String relativePath = typeName.replace('.', '/') + ".java";
501: File srcFile = new File(genDir, relativePath);
502: Util.writeCharsAsFile(logger, srcFile, cup.getSource());
503:
504: // Update the location of the cup
505: Throwable caught = null;
506: try {
507: URL fileURL = srcFile.toURI().toURL();
508: URLCompilationUnitProvider fileBaseCup = new GeneratedCUP(
509: fileURL, cup.getPackageName());
510: return fileBaseCup;
511: } catch (MalformedURLException e) {
512: caught = e;
513: }
514: logger.log(TreeLogger.ERROR,
515: "Internal error: cannot build URL from synthesized file name '"
516: + srcFile.getAbsolutePath() + "'", caught);
517: throw new UnableToCompleteException();
518: }
519: }
|