From f456c3428deb92dbca5fb68d944cb82b80ec7205 Mon Sep 17 00:00:00 2001 From: Philippe Marschall Date: Tue, 27 Jul 2021 19:39:04 +0200 Subject: [PATCH] Add a JSR-199 based compiler --- .../engine/design/JRJavaToolCompiler.java | 642 ++++++++++++++++++ 1 file changed, 642 insertions(+) create mode 100644 jasperreports/src/net/sf/jasperreports/engine/design/JRJavaToolCompiler.java diff --git a/jasperreports/src/net/sf/jasperreports/engine/design/JRJavaToolCompiler.java b/jasperreports/src/net/sf/jasperreports/engine/design/JRJavaToolCompiler.java new file mode 100644 index 0000000000..d2ad8aa617 --- /dev/null +++ b/jasperreports/src/net/sf/jasperreports/engine/design/JRJavaToolCompiler.java @@ -0,0 +1,642 @@ +/* + * JasperReports - Free Java Reporting Library. + * Copyright (C) 2021 TIBCO Software Inc. All rights reserved. + * http://www.jaspersoft.com + * + * Unless you have purchased a commercial license agreement from Jaspersoft, + * the following license terms apply: + * + * This program is part of JasperReports. + * + * JasperReports is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * JasperReports is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with JasperReports. If not, see . + */ +package net.sf.jasperreports.engine.design; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRReport; +import net.sf.jasperreports.engine.JasperReportsContext; + +/** + * A compiler for that uses the built in {@link JavaCompiler} to compile + * reports. + * + * @author Philippe Marschall (philippe.marschall@gmail.com) + */ +public final class JRJavaToolCompiler extends JRAbstractJavaCompiler { + + private static final Log LOG = LogFactory.getLog(JRJavaToolCompiler.class); + + private final JavaCompiler compiler; + + /** + * + */ + public JRJavaToolCompiler(JasperReportsContext jasperReportsContext) { + super(jasperReportsContext, false); + this.compiler = ToolProvider.getSystemJavaCompiler(); + } + + @Override + protected void checkLanguage(String language) throws JRException + { + if (!JRReport.LANGUAGE_JAVA.equals(language)) + { + throw + new JRException( + EXCEPTION_MESSAGE_KEY_EXPECTED_JAVA_LANGUAGE, + new Object[] { language, JRReport.LANGUAGE_JAVA}); + } + } + + @Override + protected JRCompilationSourceCode generateSourceCode(JRSourceCompileTask sourceTask) throws JRException + { + return JRClassGenerator.generateClass(sourceTask); + } + + @Override + protected String compileUnits(JRCompilationUnit[] units, String classpath, File tempDirFile) throws JRException + { + + StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager(null, null, null); + Map unitsByName = Arrays.stream(units) + .collect(toMap(JRCompilationUnit::getName, identity())); + JavaFileManager jrFileManager = new JRJavaFileManager(standardFileManager, unitsByName); + + Writer out = new LoggingWriter(LOG); + ReportingDiagnosticListener diagnosticListener = new ReportingDiagnosticListener(); + Iterable options = classpath != null ? Arrays.asList("-classpath", classpath) : null; + Iterable classesToBeProcessed = null; + Iterable compilationUnits = unitsByName.values().stream() + .map(JavaFileObjectInputAdapter::new) + .collect(Collectors.toList()); + CompilationTask task = this.compiler.getTask(out, jrFileManager, diagnosticListener, options, classesToBeProcessed, compilationUnits); + task.call(); + + String errors = diagnosticListener.getErrors(); + if (errors.isEmpty()) + { + return null; + } + else + { + return errors; + } + } + + @Override + protected String getSourceFileName(String unitName) + { + return unitName + ".java"; + } + + /** + * A {@link DiagnosticListener} that collects all errors to a {@link String}. + */ + static final class ReportingDiagnosticListener implements DiagnosticListener { + + private final StringBuilder errors; + + ReportingDiagnosticListener() { + this.errors = new StringBuilder(); + } + + @Override + public void report(Diagnostic diagnostic) + { + if (diagnostic.getKind() == javax.tools.Diagnostic.Kind.ERROR) + { + this.errors.append(diagnostic.getMessage(null)); + } + + } + + String getErrors() + { + return this.errors.toString(); + } + + } + + /** + * A {@link Writer} that delegates to a {@link Log}. + */ + static final class LoggingWriter extends Writer { + + private final Log log; + + LoggingWriter(Log log) { + this.log = log; + } + + @Override + public void write(char[] cbuf) + { + this.log.error(new String(cbuf)); + } + + @Override + public void write(char[] cbuf, int off, int len) + { + this.log.error(new String(cbuf, off, len)); + } + + @Override + public void write(String str) + { + this.log.error(str); + } + + @Override + public void write(String str, int off, int len) + { + this.log.error(str.substring(off, off + len)); + } + + @Override + public Writer append(CharSequence csq) + { + this.log.error(csq); + return this; + } + + @Override + public Writer append(CharSequence csq, int start, int end) + { + this.log.error(csq.subSequence(start, end)); + return this; + } + + @Override + public Writer append(char c) + { + this.log.error(c); + return this; + } + + @Override + public void flush() + { + // ignore + } + + @Override + public void close() + { + // ignore + } + + } + + /** + * A file manager that delegate to a {@link JRCompilationUnit} for reading + * sources and writing classes. All other operations a delegate the a + * default {@link JavaFileManager}. + */ + static final class JRJavaFileManager extends ForwardingJavaFileManager { + + private final Map unitsByName; + + JRJavaFileManager(JavaFileManager delegate, Map unitsByName) { + super(delegate); + this.unitsByName = unitsByName; + } + + @Override + public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException + { + List ownFiles; + if (isSourcePath(location) && kinds.contains(Kind.SOURCE)) + { + ownFiles = this.unitsByName.values().stream() + .map(JavaFileObjectInputAdapter::new) + .collect(toList()); + } + else if (isClassOutput(location) && kinds.contains(Kind.CLASS)) + { + ownFiles = this.unitsByName.values().stream() + .map(JavaFileObjectOutputAdapter::new) + .collect(toList()); + } + else + { + ownFiles = Collections.emptyList(); + } + Iterable delegateFiles = super.list(location, packageName, kinds, recurse); + if (ownFiles.isEmpty()) + { + return delegateFiles; + } + else + { + List merged = new ArrayList<>(ownFiles); + for (JavaFileObject toAdd : delegateFiles) + { + merged.add(toAdd); + } + return merged; + } + } + + @Override + public String inferBinaryName(Location location, JavaFileObject file) + { + if (this.isSourcePathOrClassOutput(location)) + { + if (file instanceof AbstractJRJavaFileObject) + { + AbstractJRJavaFileObject jrFileObject = (AbstractJRJavaFileObject) file; + return jrFileObject.getCompilationUnitName(); + } + } + return super.inferBinaryName(location, file); + } + + @Override + public boolean isSameFile(FileObject a, FileObject b) + { + if (a instanceof AbstractJRJavaFileObject) + { + if (!(b instanceof AbstractJRJavaFileObject)) + { + return false; + } + AbstractJRJavaFileObject first = (AbstractJRJavaFileObject) a; + AbstractJRJavaFileObject second = (AbstractJRJavaFileObject) b; + return (first.getKind() == second.getKind()) && first.getCompilationUnitName().equals(second.getCompilationUnitName()); + } + else if (b instanceof AbstractJRJavaFileObject) + { + return false; + } + return super.isSameFile(a, b); + } + + @Override + public boolean hasLocation(Location location) + { + if (this.isSourcePathOrClassOutput(location)) + { + return true; + } + return super.hasLocation(location); + } + + @Override + public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException + { + if (isSourcePath(location)) + { + if (kind == Kind.SOURCE) + { + JRCompilationUnit compilationUnit = this.unitsByName.get(className); + if (compilationUnit != null) + { + return new JavaFileObjectInputAdapter(compilationUnit, kind); + } + } + } + return super.getJavaFileForInput(location, className, kind); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException + { + if (isClassOutput(location)) + { + if (kind == Kind.CLASS) + { + JRCompilationUnit compilationUnit = this.unitsByName.get(className); + if (compilationUnit != null) + { + return new JavaFileObjectOutputAdapter(compilationUnit, kind); + } + } + } + return super.getJavaFileForOutput(location, className, kind, sibling); + } + + @Override + public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException + { + if (isSourcePath(location) && packageName.equals("")) + { + JRCompilationUnit compilationUnit = this.unitsByName.get(relativeName); + if (compilationUnit != null) + { + return new JavaFileObjectInputAdapter(compilationUnit); + } + } + return super.getFileForInput(location, packageName, relativeName); + } + + @Override + public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException + { + if (isClassOutput(location) && packageName.equals("")) + { + JRCompilationUnit compilationUnit = this.unitsByName.get(relativeName); + if (compilationUnit != null) + { + return new JavaFileObjectOutputAdapter(compilationUnit); + } + } + return super.getFileForOutput(location, packageName, relativeName, sibling); + } + + private boolean isSourcePathOrClassOutput(Location location) + { + return isSourcePath(location) || isClassOutput(location); + } + + private static boolean isClassOutput(Location location) + { + return location == StandardLocation.CLASS_OUTPUT; + } + + private static boolean isSourcePath(Location location) + { + return location == StandardLocation.SOURCE_PATH; + } + + } + + /** + * Abstract base class for a {@link JavaFileObject} over a + * {@link JRCompilationUnit}. + */ + static abstract class AbstractJRJavaFileObject implements JavaFileObject { + + protected final JRCompilationUnit jasperCompilationUnit; + private final Kind kind; + + AbstractJRJavaFileObject(JRCompilationUnit jasperCompilationUnit, Kind kind) { + this.jasperCompilationUnit = jasperCompilationUnit; + this.kind = kind; + } + + @Override + public URI toUri() + { + return URI.create("jasper://" + this.getCompilationUnitName()); + } + + String getCompilationUnitName() + { + return this.jasperCompilationUnit.getName(); + } + + @Override + public long getLastModified() + { + return 0; + } + + @Override + public boolean delete() + { + return false; + } + + @Override + public Kind getKind() + { + return this.kind; + } + + @Override + public boolean isNameCompatible(String simpleName, Kind kind) + { + return (this.kind == kind) && this.getCompilationUnitName().equals(simpleName); + } + + @Override + public NestingKind getNestingKind() + { + return NestingKind.TOP_LEVEL; + } + + @Override + public Modifier getAccessLevel() + { + return Modifier.PUBLIC; + } + + } + + /** + * An input {@link JavaFileObject} over a {@link JRCompilationUnit}. + */ + static final class JavaFileObjectInputAdapter extends AbstractJRJavaFileObject { + + JavaFileObjectInputAdapter(JRCompilationUnit jasperCompilationUnit, Kind kind) { + super(jasperCompilationUnit, kind); + } + + JavaFileObjectInputAdapter(JRCompilationUnit jasperCompilationUnit) { + this(jasperCompilationUnit, Kind.SOURCE); + } + + @Override + public String getName() + { + return this.getCompilationUnitName() + ".java"; + } + + @Override + public InputStream openInputStream() + { + // not optimized but never clalled + return new ByteArrayInputStream(this.jasperCompilationUnit.getSourceCode().getBytes()); + } + + @Override + public OutputStream openOutputStream() + { + throw new IllegalStateException("not for writing"); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) + { + return new StringReader(this.jasperCompilationUnit.getSourceCode()); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) + { + return this.jasperCompilationUnit.getSourceCode(); + } + + @Override + public Writer openWriter() + { + throw new IllegalStateException("not for writing"); + } + + @Override + public String toString() + { + return "input for: " + this.getCompilationUnitName() + ".java"; + } + + } + + /** + * An output {@link JavaFileObject} over a {@link JRCompilationUnit}, saves + * the output using {@link JRCompilationUnit#setCompileData(java.io.Serializable)}. + */ + static final class JavaFileObjectOutputAdapter extends AbstractJRJavaFileObject { + + JavaFileObjectOutputAdapter(JRCompilationUnit jasperCompilationUnit, Kind kind) { + super(jasperCompilationUnit, kind); + } + + JavaFileObjectOutputAdapter(JRCompilationUnit jasperCompilationUnit) { + this(jasperCompilationUnit, Kind.CLASS); + } + + @Override + public String getName() + { + return this.getCompilationUnitName() + "class"; + } + + @Override + public InputStream openInputStream() + { + throw new IllegalStateException("not for reading"); + } + + @Override + public OutputStream openOutputStream() + { + return new CompilationUnitOutputStream(this.jasperCompilationUnit); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) + { + throw new IllegalStateException("not for reading"); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) + { + throw new IllegalStateException("not for reading"); + } + + @Override + public Writer openWriter() + { + throw new IllegalStateException("only binary writing supported"); + } + + @Override + public String toString() + { + return "output for: " + this.getCompilationUnitName() + ".class"; + } + + } + + /** + * An {@link OutputStream} that calls + * {@link JRCompilationUnit#setCompileData(java.io.Serializable)} when closed. + */ + static final class CompilationUnitOutputStream extends OutputStream { + + private final JRCompilationUnit compilationUnit; + + private final ByteArrayOutputStream delegate; + + CompilationUnitOutputStream(JRCompilationUnit compilationUnit) { + this.compilationUnit = compilationUnit; + this.delegate = new ByteArrayOutputStream(); + } + + @Override + public void write(int b) + { + this.delegate.write(b); + } + + @Override + public void write(byte[] b) throws IOException + { + this.delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) + { + this.delegate.write(b, off, len); + } + + @Override + public void flush() throws IOException + { + this.delegate.flush(); + } + + @Override + public void close() throws IOException + { + this.delegate.close(); + this.compilationUnit.setCompileData(this.delegate.toByteArray()); + } + + } + +}