001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: * Free SoftwareFoundation, Inc.
023: * 59 Temple Place, Suite 330
024: * Boston, MA 02111-1307 USA
025: *
026: * @author Scott Ferguson
027: */
028:
029: package com.caucho.loader;
030:
031: import com.caucho.config.ConfigException;
032: import com.caucho.java.CompileClassNotFound;
033: import com.caucho.java.JavaCompiler;
034: import com.caucho.log.Log;
035: import com.caucho.make.AlwaysModified;
036: import com.caucho.make.Make;
037: import com.caucho.server.util.CauchoSystem;
038: import com.caucho.util.CharBuffer;
039: import com.caucho.util.L10N;
040: import com.caucho.vfs.Depend;
041: import com.caucho.vfs.Path;
042:
043: import javax.annotation.PostConstruct;
044: import java.io.IOException;
045: import java.net.URL;
046: import java.security.CodeSource;
047: import java.security.cert.Certificate;
048: import java.util.ArrayList;
049: import java.util.HashSet;
050: import java.util.logging.Level;
051: import java.util.logging.Logger;
052:
053: /**
054: * A class loader that automatically compiles Java.
055: */
056: public class CompilingLoader extends Loader implements Make {
057: private static final Logger log = Log.open(CompilingLoader.class);
058: private static final L10N L = new L10N(CompilingLoader.class);
059:
060: private static final char[] INNER_CLASS_SEPARATORS = new char[] {
061: '$', '+', '-' };
062:
063: // classpath
064: private String _classPath;
065:
066: private String _compiler;
067: private String _sourceExt = ".java";
068:
069: // source directory
070: private Path _sourceDir;
071: // directory where classes are stored
072: private Path _classDir;
073:
074: private CodeSource _codeSource;
075:
076: private ArrayList<String> _args;
077: private String _encoding;
078: private boolean _requireSource;
079:
080: private HashSet<String> _excludedDirectories = new HashSet<String>();
081:
082: private boolean _isBatch = true;
083:
084: public CompilingLoader() {
085: _excludedDirectories.add("CVS");
086: _excludedDirectories.add(".svn");
087: }
088:
089: /**
090: * Creates a new compiling class loader
091: *
092: * @param classDir generated class directory root
093: */
094: public CompilingLoader(Path classDir) {
095: this (classDir, classDir, null, null);
096: }
097:
098: /**
099: * Creates a new compiling class loader
100: *
101: * @param classDir generated class directory root
102: * @param sourceDir Java source directory root
103: * @param args Javac arguments
104: * @param encoding javac encoding
105: */
106: public CompilingLoader(Path classDir, Path sourceDir, String args,
107: String encoding) {
108: if (classDir.getScheme().equals("http")
109: || classDir.getScheme().equals("https"))
110: throw new RuntimeException(
111: "compiling class loader can't be http. Use compile=false.");
112:
113: _sourceDir = sourceDir;
114: _classDir = classDir;
115:
116: _encoding = encoding;
117:
118: // loader.addCodeBasePath(classDir.getFullPath());
119:
120: /*
121: try {
122: if (args != null)
123: _args = new Regexp("[\\s,]+").split(args);
124: } catch (Exception e) {
125: log.log(Level.FINER, e.toString(), e);
126: }
127: */
128: }
129:
130: /**
131: * Create a class loader based on the compiling loader
132: *
133: * @param path traditional classpath
134: *
135: * @return the new ClassLoader
136: */
137: public static DynamicClassLoader create(Path path) {
138: DynamicClassLoader loader = new DynamicClassLoader(null);
139:
140: CompilingLoader compilingLoader = new CompilingLoader(path);
141:
142: loader.addLoader(compilingLoader);
143:
144: loader.init();
145:
146: return loader;
147: }
148:
149: /**
150: * Sets the class path.
151: */
152: public void setPath(Path path) {
153: _classDir = path;
154:
155: if (_sourceDir == null)
156: _sourceDir = path;
157: }
158:
159: /**
160: * Gets the class path.
161: */
162: public Path getPath() {
163: return _classDir;
164: }
165:
166: /**
167: * Sets the source path.
168: */
169: public void setSource(Path path) {
170: _sourceDir = path;
171: }
172:
173: /**
174: * Sets the source extension.
175: */
176: public void setSourceExtension(String ext) throws ConfigException {
177: if (!ext.startsWith("."))
178: throw new ConfigException(L.l(
179: "source-extension '{0}' must begin with '.'", ext));
180:
181: _sourceExt = ext;
182: }
183:
184: /**
185: * Sets the compiler.
186: */
187: public void setCompiler(String compiler) throws ConfigException {
188: _compiler = compiler;
189: }
190:
191: /**
192: * Sets the source path.
193: */
194: public Path getSource() {
195: return _sourceDir;
196: }
197:
198: /**
199: * Sets the arguments.
200: */
201: public void setArgs(String arg) {
202: int i = 0;
203: int len = arg.length();
204:
205: CharBuffer cb = new CharBuffer();
206:
207: while (i < len) {
208: char ch;
209: for (; i < len
210: && Character.isWhitespace(ch = arg.charAt(i)); i++) {
211: }
212:
213: if (len <= i)
214: return;
215:
216: cb.clear();
217:
218: for (; i < len
219: && !Character.isWhitespace(ch = arg.charAt(i)); i++)
220: cb.append(ch);
221:
222: addArg(cb.toString());
223: }
224: }
225:
226: /**
227: * Adds an argument.
228: */
229: public void addArg(String arg) {
230: if (_args == null)
231: _args = new ArrayList<String>();
232:
233: _args.add(arg);
234: }
235:
236: /**
237: * Sets the encoding.
238: */
239: public void setEncoding(String encoding) {
240: _encoding = encoding;
241: }
242:
243: /**
244: * Sets true if source is required.
245: */
246: public void setRequireSource(boolean requireSource) {
247: _requireSource = requireSource;
248: }
249:
250: /**
251: * Sets true if compilation should batch as many files as possible.
252: */
253: public void setBatch(boolean isBatch) {
254: _isBatch = isBatch;
255: }
256:
257: /**
258: * Initialize.
259: */
260: @PostConstruct
261: public void init() throws ConfigException {
262: if (_classDir == null)
263: throw new ConfigException(
264: L
265: .l("`path' is a required attribute <compiling-loader>."));
266:
267: try {
268: _classDir.mkdirs();
269: } catch (IOException e) {
270: log.log(Level.FINE, e.toString(), e);
271: }
272:
273: try {
274: _codeSource = new CodeSource(new URL(_classDir.getURL()),
275: (Certificate[]) null);
276: } catch (Exception e) {
277: String scheme = _classDir.getScheme();
278:
279: if (scheme != null && !scheme.equals("memory"))
280: log.log(Level.FINE, e.toString(), e);
281: }
282: }
283:
284: /**
285: * Creates a new compiling class loader
286: *
287: * @param classDir generated class directory root
288: * @param sourceDir Java source directory root
289: * @param args Javac arguments
290: * @param encoding javac encoding
291: */
292: public static DynamicClassLoader create(ClassLoader parent,
293: Path classDir, Path sourceDir, String args, String encoding) {
294: DynamicClassLoader loader = new DynamicClassLoader(parent);
295: loader.addLoader(new CompilingLoader(classDir, sourceDir, args,
296: encoding));
297:
298: loader.init();
299:
300: return loader;
301: }
302:
303: /**
304: * Sets the owning class loader.
305: */
306: public void setLoader(DynamicClassLoader loader) {
307: super .setLoader(loader);
308:
309: loader.addURL(_classDir);
310: }
311:
312: public String getClassPath() {
313: if (_classPath == null)
314: _classPath = getLoader().getClassPath();
315:
316: return _classPath;
317: }
318:
319: /**
320: * Compiles all changed files in the class directory.
321: */
322: public void make() throws IOException, ClassNotFoundException {
323: if (_sourceDir.isDirectory() && !_classDir.isDirectory())
324: _classDir.mkdirs();
325:
326: String sourcePath = prefixClassPath(getClassPath());
327:
328: if (_isBatch) {
329: ArrayList<String> files = new ArrayList<String>();
330: findAllModifiedClasses("", _sourceDir, _classDir,
331: sourcePath, files);
332:
333: if (files.size() > 0) {
334: String[] paths = files
335: .toArray(new String[files.size()]);
336:
337: compileBatch(paths, true);
338: }
339: } else
340: makeAllSequential("", _sourceDir, _classDir, sourcePath);
341: }
342:
343: private void makeAllSequential(String name, Path sourceDir,
344: Path classDir, String sourcePath) throws IOException,
345: ClassNotFoundException {
346: String[] list;
347:
348: try {
349: list = sourceDir.list();
350: } catch (IOException e) {
351: return;
352: }
353:
354: for (int i = 0; list != null && i < list.length; i++) {
355: if (list[i].startsWith("."))
356: continue;
357:
358: if (_excludedDirectories.contains(list[i]))
359: continue;
360:
361: Path subSource = sourceDir.lookup(list[i]);
362:
363: if (subSource.isDirectory()) {
364: makeAllSequential(name + list[i] + "/", subSource,
365: classDir.lookup(list[i]), sourcePath);
366: } else if (list[i].endsWith(_sourceExt)) {
367: int tail = list[i].length() - _sourceExt.length();
368: String prefix = list[i].substring(0, tail);
369: Path subClass = classDir.lookup(prefix + ".class");
370:
371: if (subSource.getLastModified() <= subClass
372: .getLastModified())
373: continue;
374:
375: compileClass(subSource, subClass, sourcePath, true);
376: }
377: }
378:
379: if (!_requireSource)
380: return;
381:
382: try {
383: list = classDir.list();
384: } catch (IOException e) {
385: return;
386: }
387:
388: for (int i = 0; list != null && i < list.length; i++) {
389: if (list[i].startsWith("."))
390: continue;
391:
392: if (_excludedDirectories.contains(list[i]))
393: continue;
394:
395: Path subClass = classDir.lookup(list[i]);
396:
397: if (list[i].endsWith(".class")) {
398: String prefix = list[i].substring(0,
399: list[i].length() - 6);
400: Path subSource = sourceDir.lookup(prefix + _sourceExt);
401:
402: if (!subSource.exists()) {
403: String tail = subSource.getTail();
404: boolean doRemove = true;
405:
406: if (tail.indexOf('$') > 0) {
407: String subTail = tail.substring(0, tail
408: .indexOf('$'))
409: + _sourceExt;
410: Path subJava = subSource.getParent().lookup(
411: subTail);
412:
413: if (subJava.exists())
414: doRemove = false;
415: }
416:
417: if (doRemove) {
418: log.finer(L.l("removing obsolete class `{0}'.",
419: subClass.getPath()));
420:
421: subClass.remove();
422: }
423: }
424: }
425: }
426: }
427:
428: /**
429: * Returns the classes which need compilation.
430: */
431: private void findAllModifiedClasses(String name, Path sourceDir,
432: Path classDir, String sourcePath, ArrayList<String> sources)
433: throws IOException, ClassNotFoundException {
434: String[] list;
435:
436: try {
437: list = sourceDir.list();
438: } catch (IOException e) {
439: return;
440: }
441:
442: for (int i = 0; list != null && i < list.length; i++) {
443: if (list[i].startsWith("."))
444: continue;
445:
446: if (_excludedDirectories.contains(list[i]))
447: continue;
448:
449: Path subSource = sourceDir.lookup(list[i]);
450:
451: if (subSource.isDirectory()) {
452: findAllModifiedClasses(name + list[i] + "/", subSource,
453: classDir.lookup(list[i]), sourcePath, sources);
454: } else if (list[i].endsWith(_sourceExt)) {
455: int tail = list[i].length() - _sourceExt.length();
456: String prefix = list[i].substring(0, tail);
457: Path subClass = classDir.lookup(prefix + ".class");
458:
459: if (subClass.getLastModified() < subSource
460: .getLastModified()) {
461: sources.add(name + list[i]);
462: }
463: }
464: }
465:
466: if (!_requireSource)
467: return;
468:
469: try {
470: list = classDir.list();
471: } catch (IOException e) {
472: return;
473: }
474:
475: for (int i = 0; list != null && i < list.length; i++) {
476: if (list[i].startsWith("."))
477: continue;
478:
479: if (_excludedDirectories.contains(list[i]))
480: continue;
481:
482: Path subClass = classDir.lookup(list[i]);
483:
484: if (list[i].endsWith(".class")) {
485: String prefix = list[i].substring(0,
486: list[i].length() - 6);
487: Path subSource = sourceDir.lookup(prefix + _sourceExt);
488:
489: if (!subSource.exists()) {
490: String tail = subSource.getTail();
491: boolean doRemove = true;
492:
493: if (tail.indexOf('$') > 0) {
494: String subTail = tail.substring(0, tail
495: .indexOf('$'))
496: + _sourceExt;
497: Path subJava = subSource.getParent().lookup(
498: subTail);
499:
500: if (subJava.exists())
501: doRemove = false;
502: }
503:
504: if (doRemove) {
505: log.finer(L.l("removing obsolete class `{0}'.",
506: subClass.getPath()));
507:
508: subClass.remove();
509: }
510: }
511: }
512: }
513: }
514:
515: /**
516: * Loads the specified class, compiling if necessary.
517: */
518: @Override
519: protected ClassEntry getClassEntry(String name, String pathName)
520: throws ClassNotFoundException {
521: synchronized (this ) {
522: Path classFile = _classDir.lookup(pathName);
523: String javaName = name.replace('.', '/') + _sourceExt;
524: Path javaFile = _sourceDir.lookup(javaName);
525:
526: String tail = javaFile.getTail();
527:
528: for (int i = 0; i < INNER_CLASS_SEPARATORS.length; i++) {
529: char sep = INNER_CLASS_SEPARATORS[i];
530: if (name.indexOf(sep) > 0) {
531: String subName = name.substring(0, name
532: .indexOf(sep));
533: String subJavaName = subName.replace('.', '/')
534: + _sourceExt;
535: Path subJava = _sourceDir.lookup(subJavaName);
536:
537: if (subJava.exists()) {
538: javaFile = subJava;
539: }
540: }
541: }
542:
543: if (_requireSource && !javaFile.exists()) {
544: boolean doRemove = true;
545:
546: if (doRemove) {
547: log.finer(L.l("removing obsolete class `{0}'.",
548: classFile.getPath()));
549:
550: try {
551: classFile.remove();
552: } catch (IOException e) {
553: log.log(Level.WARNING, e.toString(), e);
554: }
555:
556: return null;
557: }
558: }
559:
560: if (!classFile.canRead() && !javaFile.canRead())
561: return null;
562:
563: return new CompilingClassEntry(this , getLoader(), name,
564: javaFile, classFile, getCodeSource(classFile));
565: }
566: }
567:
568: /**
569: * Returns the code source for the directory.
570: */
571: protected CodeSource getCodeSource(Path path) {
572: return _codeSource;
573: }
574:
575: /**
576: * Checks that the case is okay for the source.
577: */
578: boolean checkSource(Path sourceDir, String javaName) {
579: try {
580: while (javaName != null && !javaName.equals("")) {
581: int p = javaName.indexOf('/');
582: String head;
583:
584: if (p >= 0) {
585: head = javaName.substring(0, p);
586: javaName = javaName.substring(p + 1);
587: } else {
588: head = javaName;
589: javaName = null;
590: }
591:
592: String[] names = sourceDir.list();
593: int i;
594: for (i = 0; i < names.length; i++) {
595: if (names[i].equals(head))
596: break;
597: }
598:
599: if (i == names.length)
600: return false;
601:
602: sourceDir = sourceDir.lookup(head);
603: }
604: } catch (IOException e) {
605: log.log(Level.FINE, e.toString(), e);
606:
607: return false;
608: }
609:
610: return true;
611: }
612:
613: /**
614: * Loads the class from the class file.
615: *
616: * @param className the name of the class to load
617: * @param javaFile the path to the java source
618: * @param classFile the path to the class file
619: *
620: * @return a class entry or null on failure
621: */
622: private ClassEntry loadClass(String className, Path javaFile,
623: Path classFile) {
624: long length = classFile.getLength();
625:
626: ClassEntry entry = new CompilingClassEntry(this , getLoader(),
627: className, javaFile, classFile,
628: getCodeSource(classFile));
629:
630: Class cl = null;
631:
632: try {
633: cl = getLoader().loadClass(entry);
634: } catch (Exception e) {
635: try {
636: if (javaFile.canRead())
637: classFile.remove();
638: } catch (IOException e1) {
639: }
640: }
641:
642: if (cl != null)
643: return entry;
644: else
645: return null;
646: }
647:
648: /**
649: * Compile the Java source. Compile errors are encapsulated in a
650: * ClassNotFound wrapper.
651: *
652: * @param javaSource path to the Java source
653: */
654: void compileClass(Path javaSource, Path javaClass,
655: String sourcePath, boolean isMake)
656: throws ClassNotFoundException {
657: try {
658: JavaCompiler compiler = JavaCompiler.create(getLoader());
659: compiler.setClassDir(_classDir);
660: compiler.setSourceDir(_sourceDir);
661: if (_encoding != null)
662: compiler.setEncoding(_encoding);
663: compiler.setArgs(_args);
664: compiler.setCompileParent(!isMake);
665: compiler.setSourceExtension(_sourceExt);
666: if (_compiler != null)
667: compiler.setCompiler(_compiler);
668:
669: //LineMap lineMap = new LineMap(javaFile.getNativePath());
670: // The context path is obvious from the browser url
671: //lineMap.add(name.replace('.', '/') + _sourceExt, 1, 1);
672:
673: // Force this into a relative path so different compilers will work
674: String prefix = _sourceDir.getPath();
675: String full = javaSource.getPath();
676:
677: String source;
678: if (full.startsWith(prefix)) {
679: source = full.substring(prefix.length());
680: if (source.startsWith("/"))
681: source = source.substring(1);
682: } else
683: source = javaSource.getPath();
684:
685: /*
686: if (javaSource.canRead() && javaClass.exists())
687: javaClass.remove();
688: */
689:
690: compiler.compileIfModified(source, null);
691: } catch (Exception e) {
692: getLoader().addDependency(new Depend(javaSource));
693:
694: log.log(Level.FINEST, e.toString(), e);
695:
696: // Compile errors are wrapped in a special ClassNotFound class
697: // so the server can give a nice error message
698: throw new CompileClassNotFound(e);
699: }
700: }
701:
702: /**
703: * Compile the Java source. Compile errors are encapsulated in a
704: * ClassNotFound wrapper.
705: */
706: void compileBatch(String[] files, boolean isMake)
707: throws ClassNotFoundException {
708: try {
709: JavaCompiler compiler = JavaCompiler.create(getLoader());
710: compiler.setClassDir(_classDir);
711: compiler.setSourceDir(_sourceDir);
712: if (_encoding != null)
713: compiler.setEncoding(_encoding);
714: compiler.setArgs(_args);
715: compiler.setCompileParent(!isMake);
716: compiler.setSourceExtension(_sourceExt);
717: if (_compiler != null)
718: compiler.setCompiler(_compiler);
719:
720: //LineMap lineMap = new LineMap(javaFile.getNativePath());
721: // The context path is obvious from the browser url
722: //lineMap.add(name.replace('.', '/') + _sourceExt, 1, 1);
723:
724: compiler.compileBatch(files);
725: } catch (Exception e) {
726: getLoader().addDependency(AlwaysModified.create());
727:
728: // Compile errors are wrapped in a special ClassNotFound class
729: // so the server can give a nice error message
730: throw new CompileClassNotFound(e);
731: }
732: }
733:
734: /**
735: * Returns the path for the given name.
736: *
737: * @param name the name of the class
738: */
739: @Override
740: public Path getPath(String name) {
741: Path path = _classDir.lookup(name);
742:
743: if (path != null && path.exists())
744: return path;
745:
746: path = _sourceDir.lookup(name);
747:
748: if (path != null && path.exists())
749: return path;
750:
751: return null;
752: }
753:
754: /**
755: * Adds the classpath we're responsible for to the classpath
756: *
757: * @param head the overriding classpath
758: * @return the new classpath
759: */
760: @Override
761: protected void buildClassPath(ArrayList<String> pathList) {
762: if (!_classDir.getScheme().equals("file"))
763: return;
764:
765: try {
766: if (!_classDir.isDirectory() && _sourceDir.isDirectory()) {
767: try {
768: _classDir.mkdirs();
769: } catch (IOException e) {
770: }
771: }
772:
773: if (_classDir.isDirectory()) {
774: String path = _classDir.getNativePath();
775:
776: if (!pathList.contains(path))
777: pathList.add(path);
778: }
779:
780: if (!_classDir.equals(_sourceDir)) {
781: String path = _sourceDir.getNativePath();
782:
783: if (!pathList.contains(path))
784: pathList.add(path);
785: }
786: } catch (java.security.AccessControlException e) {
787: log.log(Level.WARNING, e.toString(), e);
788: }
789: }
790:
791: protected String prefixClassPath(String tail) {
792: CharBuffer cb = new CharBuffer();
793:
794: if (!_classDir.isDirectory() && _sourceDir.isDirectory()) {
795: try {
796: _classDir.mkdirs();
797: } catch (IOException e) {
798: }
799: }
800:
801: if (_classDir.isDirectory()) {
802: if (cb.length() > 0)
803: cb.append(CauchoSystem.getPathSeparatorChar());
804: cb.append(_classDir.getNativePath());
805: }
806:
807: if (!_classDir.equals(_sourceDir)) {
808: if (cb.length() > 0)
809: cb.append(CauchoSystem.getPathSeparatorChar());
810: cb.append(_sourceDir.getNativePath());
811: }
812:
813: if (cb.length() > 0)
814: cb.append(CauchoSystem.getPathSeparatorChar());
815: cb.append(tail);
816:
817: return cb.close();
818: }
819:
820: /*
821: @Override
822: protected void buildSourcePath(StringBuilder head)
823: {
824: buildClassPath(head);
825: }
826: */
827:
828: public String toString() {
829: if (_classDir == null)
830: return "CompilingLoader[]";
831: else if (_classDir.equals(_sourceDir))
832: return "CompilingLoader[src:" + _sourceDir + "]";
833: else
834: return ("CompilingLoader[src:" + _sourceDir + ",class:"
835: + _classDir + "]");
836: }
837: }
|