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 }