1   /*
2    * Copyright 2003 - 2013 The eFaps Team
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Revision:        $Rev$
17   * Last Changed:    $Date$
18   * Last Changed By: $Author$
19   */
20  
21  package org.efaps.update.schema.program.esjp;
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.io.Writer;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collections;
35  import java.util.Comparator;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Set;
40  import java.util.regex.Matcher;
41  
42  import javax.tools.FileObject;
43  import javax.tools.ForwardingJavaFileManager;
44  import javax.tools.JavaCompiler;
45  import javax.tools.JavaFileManager;
46  import javax.tools.JavaFileObject;
47  import javax.tools.SimpleJavaFileObject;
48  import javax.tools.StandardJavaFileManager;
49  import javax.tools.StandardLocation;
50  import javax.tools.ToolProvider;
51  
52  import org.efaps.admin.datamodel.Type;
53  import org.efaps.ci.CIAdminProgram;
54  import org.efaps.db.Checkin;
55  import org.efaps.db.Checkout;
56  import org.efaps.db.Delete;
57  import org.efaps.db.Insert;
58  import org.efaps.db.Instance;
59  import org.efaps.db.MultiPrintQuery;
60  import org.efaps.db.QueryBuilder;
61  import org.efaps.update.util.InstallationException;
62  import org.efaps.util.EFapsException;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * The class is used to compile all checked in ESJP programs. Because the
68   * dependencies of a class are not known, all ESJP programs stored in eFaps are
69   * compiled.
70   *
71   * @author The eFaps Team
72   * @version $Id$
73   */
74  public class ESJPCompiler
75  {
76      /**
77       * Logging instance used in this class.
78       */
79      private static final Logger LOG = LoggerFactory.getLogger(ESJPCompiler.class);
80  
81      /**
82       * Type instance of Java program.
83       */
84      private final Type esjpType;
85  
86      /**
87       * Type instance of compile EJSP program.
88       */
89      private final Type classType;
90  
91      /**
92       * Mapping between ESJP name and the related ESJP source object.
93       *
94       * @see #readESJPPrograms()
95       */
96      private final Map<String, SourceObject> name2Source  = new HashMap<String, SourceObject>();
97  
98      /**
99       * Mapping between already existing compiled ESJP class name and the
100      * related eFaps id in the database.
101      *
102      * @see #readESJPClasses()
103      */
104     private final Map<String, Long> class2id = new HashMap<String, Long>();
105 
106     /**
107      * Mapping between the class name and the related ESJP class which must be
108      * stored.
109      *
110      * @see StoreObject
111      */
112     private final Map<String, ESJPCompiler.StoreObject> classFiles
113         = new HashMap<String, ESJPCompiler.StoreObject>();
114 
115     /**
116      * Stores the list of class path needed to compile (if needed).
117      */
118     private final List<String> classPathElements;
119 
120     /**
121      * The constructor initialize the two type instances {@link #esjpType} and
122      * {@link #classType}.
123      *
124      * @param _classPathElements  list of class path elements
125      * @see #esjpType
126      * @see #classType
127      */
128     public ESJPCompiler(final List<String> _classPathElements)
129     {
130         this.esjpType = CIAdminProgram.Java.getType();
131         this.classType = CIAdminProgram.JavaClass.getType();
132         this.classPathElements = _classPathElements;
133     }
134 
135   /**
136    * All stored ESJP programs in eFaps are compiled. The system Java compiler
137    * defined from the {@link ToolProvider tool provider} is used for the
138    * compiler. All old not needed compiled Java classes are automatically
139    * removed. The compiler error and warning are logged (errors are using
140    * error-level, warnings are using info-level).<br>
141    * Debug:<br>
142    * <ul>
143    * <li><code>null</code>: By default, only line number and source file information is generated.</li>
144    * <li><code>"none"</code>: Do not generate any debugging information</li>
145    * <li>Generate only some kinds of debugging information, specified by a comma separated
146    * list of keywords. Valid keywords are:
147    * <ul>
148    * <li><code>"source"</code>: Source file debugging information</li>
149    * <li><code>"lines"</code>: Line number debugging information</li>
150    * <li><code>"vars"</code>: Local variable debugging information</li>
151    * </ul>
152    * </li>
153    * </ul>
154    *
155    * @param _debug                  String for the debug option
156    * @param _addRuntimeClassPath    Must the classpath from the runtime added
157    *                                to the compiler, default: <code>false</code>
158    * @throws InstallationException if the compile failed
159    */
160     public void compile(final String _debug,
161                         final boolean _addRuntimeClassPath)
162         throws InstallationException
163     {
164         readESJPPrograms();
165         readESJPClasses();
166 
167         final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
168 
169         if (compiler == null)  {
170             ESJPCompiler.LOG.error("no compiler found for compiler !");
171         } else  {
172             // output of used compiler
173             if (ESJPCompiler.LOG.isInfoEnabled()) {
174                 ESJPCompiler.LOG.info("    Using compiler " + compiler.toString());
175             }
176 
177             // options for the compiler
178             final List<String> optionList = new ArrayList<String>();
179 
180             // set classpath!
181             // (the list of programs to compile is given to the javac as
182             // argument array, so the class path could be set in front of the
183             // programs to compile)
184             if (this.classPathElements != null)  {
185                 // different class path separators depending on the OS
186                 final String sep = System.getProperty("os.name").startsWith("Windows") ? ";" : ":";
187 
188                 final StringBuilder classPath = new StringBuilder();
189                 for (final String classPathElement : this.classPathElements)  {
190                     classPath.append(classPathElement).append(sep);
191                 }
192                 if (_addRuntimeClassPath) {
193                     classPath.append(System.getProperty("java.class.path"));
194                 }
195                 optionList.addAll(Arrays.asList("-classpath", classPath.toString()));
196             } else  {
197                 // set compiler's class path to be same as the runtime's
198                 optionList.addAll(Arrays.asList("-classpath", System.getProperty("java.class.path")));
199             }
200             //Set the source file encoding name, such as EUCJIS/SJIS. If -encoding is not specified,
201             //the platform default converter is used.
202             optionList.addAll(Arrays.asList("-encoding", "UTF-8"));
203 
204             if (_debug != null) {
205                 optionList.addAll(Arrays.asList("-g", _debug));
206             }
207 
208             // logging of compiling classes
209             if (ESJPCompiler.LOG.isInfoEnabled()) {
210                 final List<SourceObject> ls = new ArrayList<SourceObject>(this.name2Source.values());
211                 Collections.sort(ls, new Comparator<SourceObject>() {
212                     @Override
213                     public int compare(final SourceObject _arg0,
214                                        final SourceObject _arg1)
215                     {
216                         return _arg0.getJavaName().compareTo(_arg1.getJavaName());
217                     }});
218                 for (final SourceObject obj : ls) {
219                     ESJPCompiler.LOG.info("    Compiling ESJP '{}'", obj.getJavaName());
220                 }
221             }
222 
223             final FileManager fm = new FileManager(compiler.getStandardFileManager(null, null, null));
224             final boolean noErrors = compiler.getTask(new ErrorWriter(),
225                                                       fm,
226                                                       null,
227                                                       optionList,
228                                                       null,
229                                                       this.name2Source.values())
230                                              .call();
231 
232             if (!noErrors)  {
233                 throw new InstallationException("error");
234             }
235 
236             // store all compiled ESJP's
237             for (final ESJPCompiler.StoreObject obj : this.classFiles.values())  {
238                 obj.write();
239             }
240 
241             // delete not needed compiled ESJP classes
242             for (final Long id : this.class2id.values()) {
243                 try {
244                     new Delete(this.classType, id).executeWithoutAccessCheck();
245                 } catch (final EFapsException e)  {
246                     throw new InstallationException("Could not delete ESJP class with id " + id, e);
247                 }
248             }
249         }
250     }
251 
252     /**
253      * All EJSP programs in the eFaps database are read and stored in the
254      * mapping {@link #name2Source} for further using.
255      *
256      * @see #name2Source
257      * @throws InstallationException if ESJP Java programs could not be read
258      */
259     protected void readESJPPrograms()
260         throws InstallationException
261     {
262         try  {
263             final QueryBuilder queryBldr = new QueryBuilder(this.esjpType);
264             final MultiPrintQuery multi = queryBldr.getPrint();
265             multi.addAttribute("Name");
266             multi.executeWithoutAccessCheck();
267             while (multi.next()) {
268                 final String name = multi.<String>getAttribute("Name");
269                 final Long id = multi.getCurrentInstance().getId();
270                 final File file = new File(File.separator,
271                                            name.replaceAll("\\.", Matcher.quoteReplacement(File.separator))
272                                                    + JavaFileObject.Kind.SOURCE.extension);
273                 final URI uri;
274                 try {
275                     uri = new URI("efaps", null, file.getAbsolutePath(), null, null);
276                 } catch (final URISyntaxException e) {
277                     throw new InstallationException("Could not create an URI for " + file, e);
278                 }
279                 this.name2Source.put(name, new SourceObject(uri, name, id));
280             }
281         } catch (final EFapsException e) {
282             throw new InstallationException("Could not fetch the information about installed ESJP's", e);
283         }
284     }
285 
286     /**
287      * All stored compiled ESJP's classes in the eFaps database are stored in
288      * the mapping {@link #class2id}. If a ESJP's program is compiled and
289      * stored with {@link ESJPCompiler.StoreObject#write()}, the class is
290      * removed. After the compile, {@link ESJPCompiler#compile(String)} removes
291      * all stored classes which are not needed anymore.
292      *
293      * @throws InstallationException if read of the ESJP classes failed
294      * @see #class2id
295      */
296     protected void readESJPClasses()
297         throws InstallationException
298     {
299         try  {
300             final QueryBuilder queryBldr = new QueryBuilder(this.classType);
301             final MultiPrintQuery multi = queryBldr.getPrint();
302             multi.addAttribute("Name");
303             multi.executeWithoutAccessCheck();
304             while (multi.next()) {
305                 final String name = multi.<String>getAttribute("Name");
306                 final Long id = multi.getCurrentInstance().getId();
307                 this.class2id.put(name, id);
308             }
309         } catch (final EFapsException e) {
310             throw new InstallationException("Could not fetch the information about compiled ESJP's", e);
311         }
312     }
313 
314     /**
315      * Error writer to show all errors to the
316      * {@link ESJPCompiler#LOG compiler logger}.
317      */
318     private final class ErrorWriter
319         extends Writer
320     {
321         /**
322          * Stub method because only required to derive from {@link Writer}.
323          */
324         @Override
325         public void close()
326         {
327         }
328 
329         /**
330          * Stub method because only required to derive from {@link Writer}.
331          */
332         @Override
333         public void flush()
334         {
335         }
336 
337         /**
338          * Writes given message to the error log of the compiler.
339          *
340          * @param _cbuf     buffer with the message
341          * @param _off      offset within the buffer
342          * @param _len      len of the message within the buffer
343          */
344         @Override
345         public void write(final char[] _cbuf,
346                           final int _off,
347                           final int _len)
348         {
349             final String msg = new StringBuilder().append(_cbuf, _off, _len).toString().trim();
350             if (!"".equals(msg))  {
351                 for (final String line : msg.split("\n"))  {
352                     ESJPCompiler.LOG.error(line);
353                 }
354             }
355         }
356 
357     }
358 
359     /**
360      * ESJP file manager to handle the compiled ESJP classes.
361      */
362     private final class FileManager
363         extends ForwardingJavaFileManager<StandardJavaFileManager>
364     {
365         /**
366          * Defined the forwarding Java file manager.
367          *
368          * @param _sfm      original Java file manager to forward
369          */
370         public FileManager(final StandardJavaFileManager _sfm)
371         {
372             super(_sfm);
373         }
374 
375         /**
376          * The method returns always <code>null</code> to be sure the no file
377          * is written.
378          *
379          * @param _location     location for which the file output is searched
380          * @param _packageName  name of the package
381          * @param _relativeName relative name
382          * @param _fileObject   file object to be used as hint for placement
383          * @return always <code>null</code>
384          */
385         @Override
386         public FileObject getFileForOutput(final Location _location,
387                                            final String _packageName,
388                                            final String _relativeName,
389                                            final FileObject _fileObject)
390         {
391             return null;
392         }
393 
394         /**
395          * Returns the related Java file object used from the Java compiler to
396          * store the compiled ESJP.
397          *
398          * @param _location     location (not used)
399          * @param _className    name of the ESJP class
400          * @param _kind         kind of the source (not used)
401          * @param _fileObject   file object to update (used to get the URI)
402          * @return Java file object for ESJP used to store the compiled class
403          * @see ESJPCompiler
404          */
405         @Override
406         public JavaFileObject getJavaFileForOutput(final Location _location,
407                                                    final String _className,
408                                                    final JavaFileObject.Kind _kind,
409                                                    final FileObject _fileObject)
410         {
411             final ESJPCompiler.StoreObject ret = new ESJPCompiler.StoreObject(_fileObject.toUri(), _className);
412             ESJPCompiler.this.classFiles.put(_className, ret);
413             return ret;
414         }
415 
416         /**
417          * Checks if given <code>_location</code> is handled by this Java file
418          * manager.
419          *
420          * @param _location     location to prove
421          * @return <i>true</i> if the <code>_location</code> is the source path or
422          *         the forwarding standard Java file manager handles the
423          *         <code>_location</code>; otherwise <i>false</i>
424          */
425         @Override
426         public boolean hasLocation(final JavaFileManager.Location _location)
427         {
428             return StandardLocation.SOURCE_PATH.getName().equals(_location.getName()) || super.hasLocation(_location);
429         }
430 
431         /**
432          * If the <code>_location</code> is the source path a dummy binary name
433          * for the ESJP class is returned. The dummy binary name is the name of
434          * the <code>_javaFileObject</code> and the extension for
435          * {@link JavaFileObject.Kind#CLASS Java classes}. If the
436          * <code>_location</code> is not the source path, the binary name from
437          * the forwarded
438          * {@link StandardJavaFileManager standard Java file manager} is
439          * returned.
440          *
441          * @param _location         location
442          * @param _javaFileObject   java file object
443          * @return name of the binary object for the ESJP or from forwarded
444          *         {@link StandardJavaFileManager standard Java file manager}
445          */
446         @Override
447         public String inferBinaryName(final JavaFileManager.Location _location,
448                                       final JavaFileObject _javaFileObject)
449         {
450             final String ret;
451             if (StandardLocation.SOURCE_PATH.getName().equals(_location.getName()))  {
452                 ret = new StringBuilder()
453                         .append(_javaFileObject.getName())
454                         .append(JavaFileObject.Kind.CLASS.extension)
455                         .toString();
456             } else  {
457                 ret = super.inferBinaryName(_location, _javaFileObject);
458             }
459             return ret;
460         }
461 
462         /**
463          * <p>If the <code>_location</code> is the source path and the
464          * <code>_kinds</code> includes sources an investigation in the cached
465          * {@link ESJPCompiler#name2Source ESJP programs} is done and the list of
466          * ESJP's for given <code>_packageName</code> is returned.</p>
467          * <p>In all other case the list of found Java programs from the
468          * forwarded {@link StandardJavaFileManager standard Java file manager}
469          * is returned.</p>
470          *
471          * @param _location     location which must be investigated
472          * @param _packageName  name of searched package
473          * @param _kinds        kinds of file object
474          * @param _recurse      must be searched recursive including sub
475          *                      packages (ignored, because not used)
476          * @return list of found ESJP programs for given
477          *         <code>_packageName</code> or if not from source path the
478          *         list of Java classes from forwarded standard Java file
479          *         manager
480          * @throws IOException from forwarded standard Java file manager
481          */
482         @Override
483         public Iterable<JavaFileObject> list(final Location _location,
484                                              final String _packageName,
485                                              final Set<JavaFileObject.Kind> _kinds,
486                                              final boolean _recurse)
487             throws IOException
488         {
489             final Iterable<JavaFileObject> rt;
490             if (StandardLocation.SOURCE_PATH.getName().equals(_location.getName())
491                     && _kinds.contains(JavaFileObject.Kind.SOURCE))  {
492                 final List<JavaFileObject> pckObjs = new ArrayList<JavaFileObject>();
493                 final int pckLength = _packageName.length();
494                 for (final Map.Entry<String, ESJPCompiler.SourceObject> entry
495                         : ESJPCompiler.this.name2Source.entrySet())  {
496 
497                     if (entry.getKey().startsWith(_packageName)
498                             && entry.getKey().substring(pckLength + 1).indexOf('.') < 0)  {
499 
500                         pckObjs.add(entry.getValue());
501                     }
502                 }
503                 rt = pckObjs;
504             } else  {
505                 rt = super.list(_location, _packageName, _kinds, _recurse);
506             }
507             return rt;
508         }
509     }
510 
511     /**
512      * Holds the information about the ESJP source program which must be
513      * compiled (and from which the source code is fetched).
514      */
515     private final class SourceObject
516         extends SimpleJavaFileObject
517     {
518         /**
519          * Name of the ESJP program.
520          */
521         private final String javaName;
522 
523         /**
524          * Used internal id in eFaps.
525          */
526         private final long id;
527 
528         /**
529          * Initializes the source object.
530          *
531          * @param _uri          URI of the ESJP
532          * @param _javaName     Java name of the ESJP
533          * @param _id           id used from eFaps within database
534          */
535         private SourceObject(final URI _uri,
536                              final String _javaName,
537                              final long _id)
538         {
539             super(_uri, JavaFileObject.Kind.SOURCE);
540             this.javaName = _javaName;
541             this.id = _id;
542         }
543 
544         /**
545          * Returns the char sequence of the ESJP source code.
546          *
547          * @param _ignoreEncodingErrors     ignore encoding error (not used)
548          * @return source code from the ESJP
549          * @throws IOException if source could not be read from the eFaps
550          *                     database
551          */
552         @Override
553         public CharSequence getCharContent(final boolean _ignoreEncodingErrors)
554             throws IOException
555         {
556             final StringBuilder ret = new StringBuilder();
557             try {
558                 final Checkout checkout = new Checkout(Instance.get(ESJPCompiler.this.esjpType, this.id));
559                 final InputStream is = checkout.executeWithoutAccessCheck();
560                 final byte[] bytes = new byte[is.available()];
561                 is.read(bytes);
562                 is.close();
563                 ret.append(new String(bytes, "UTF-8"));
564             } catch (final EFapsException e) {
565                 throw new IOException("could not checkout class '" + this.javaName + "'", e);
566             }
567             return ret;
568         }
569 
570         /**
571          * Getter method for the instance variable {@link #javaName}.
572          *
573          * @return value of instance variable {@link #javaName}
574          */
575         public String getJavaName()
576         {
577             return this.javaName;
578         }
579     }
580 
581     /**
582      * The class is used to store the result of a Java compilation.
583      */
584     private final class StoreObject
585         extends SimpleJavaFileObject
586     {
587         /**
588          * Name of the class to compile.
589          */
590         private final String className;
591 
592         /**
593          * Byte array output stream to store the result of the compilation.
594          *
595          * @see #openOutputStream()
596          */
597         private final ByteArrayOutputStream out = new ByteArrayOutputStream();
598 
599         /**
600          * Initializes this store object.
601          *
602          * @param _uri          URI of the class to store
603          * @param _className    name of the class to store
604          */
605         private StoreObject(final URI _uri,
606                             final String _className)
607         {
608             super(_uri, JavaFileObject.Kind.CLASS);
609             this.className = _className;
610         }
611 
612         /**
613          * Returns this {@link #out} which is used as buffer for the compiled
614          * ESJP {@link #className}.
615          *
616          * @return {@link #out} as output stream
617          * @see #out
618          */
619         @Override
620         public OutputStream openOutputStream()
621         {
622             return this.out;
623         }
624 
625         /**
626          * The compiled class in <i>_resourceData</i> is stored with the name
627          * <i>_resourceName</i> in the eFaps database (checked in). If the class
628          * instance already exists in eFaps, the class data is updated. Otherwise, the
629          * compiled class is new inserted in eFaps (related to the original Java
630          * program).
631          */
632         public void write()
633         {
634             if (ESJPCompiler.LOG.isDebugEnabled()) {
635                 ESJPCompiler.LOG.debug("write '" + this.className + "'");
636             }
637             try {
638                 final Long id = ESJPCompiler.this.class2id.get(this.className);
639                 Instance instance;
640                 if (id == null) {
641                     final String parent = this.className.replaceAll(".class$", "").replaceAll("\\$.*", "");
642 
643                     final ESJPCompiler.SourceObject parentId = ESJPCompiler.this.name2Source.get(parent);
644 
645                     final Insert insert = new Insert(ESJPCompiler.this.classType);
646                     insert.add("Name", this.className);
647                     insert.add("ProgramLink", "" + parentId.id);
648                     insert.executeWithoutAccessCheck();
649                     instance = insert.getInstance();
650                     insert.close();
651                 } else {
652                     instance = Instance.get(ESJPCompiler.this.classType, id);
653                     ESJPCompiler.this.class2id.remove(this.className);
654                 }
655 
656                 final Checkin checkin = new Checkin(instance);
657                 checkin.executeWithoutAccessCheck(this.className,
658                                                   new ByteArrayInputStream(this.out.toByteArray()),
659                                                   this.out.toByteArray().length);
660                 //CHECKSTYLE:OFF
661             } catch (final Exception e) {
662               //CHECKSTYLE:ON
663                 ESJPCompiler.LOG.error("unable to write to eFaps ESJP class '" + this.className + "'", e);
664             }
665         }
666     }
667 }