/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.asakusafw.compiler.flow.packager;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.asakusafw.compiler.batch.ResourceRepository;
import com.asakusafw.compiler.batch.ResourceRepository.Cursor;
import com.asakusafw.compiler.common.FileRepository;
import com.asakusafw.compiler.common.Precondition;
import com.asakusafw.compiler.flow.FlowCompilerOptions.GenericOptionValue;
import com.asakusafw.compiler.flow.FlowCompilingEnvironment;
import com.asakusafw.compiler.flow.Location;
import com.asakusafw.compiler.flow.Packager;
import com.asakusafw.utils.java.model.syntax.CompilationUnit;
import com.asakusafw.utils.java.model.syntax.Name;
import com.asakusafw.utils.java.model.util.Filer;
/**
* An implementation of {@link Packager} that generates packages into the local file system.
* @since 0.1.0
* @version 0.7.3
*/
public class FilePackager extends FlowCompilingEnvironment.Initialized implements Packager {
static final Logger LOG = LoggerFactory.getLogger(FilePackager.class);
private static final Charset CHARSET = StandardCharsets.UTF_8;
/**
* The option name of whether or not packaging is enabled.
* @since 0.5.0
*/
public static final String KEY_OPTION_PACKAGING = "packaging"; //$NON-NLS-1$
/**
* The option name of Java source/target version.
* @since 0.7.3
*/
public static final String KEY_JAVA_VERSION = "javaVersion"; //$NON-NLS-1$
/**
* The default value of {@link #KEY_JAVA_VERSION}.
* @since 0.7.3
*/
public static final String DEFAULT_JAVA_VERSION = "1.8"; //$NON-NLS-1$
private static final String SOURCE_DIRECTORY = "src"; //$NON-NLS-1$
private static final String CLASS_DIRECTORY = "bin"; //$NON-NLS-1$
private final File sourceDirectory;
private final File classDirectory;
private final Filer sourceFiler;
private final Filer resourceFiler;
private final List<? extends ResourceRepository> fragmentRepositories;
/**
* Creates a new instance.
* @param workingDirectory the working directory
* @param fragmentRepositories the resources for embedding to the generating packages
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public FilePackager(File workingDirectory, List<? extends ResourceRepository> fragmentRepositories) {
Precondition.checkMustNotBeNull(workingDirectory, "workingDirectory"); //$NON-NLS-1$
Precondition.checkMustNotBeNull(fragmentRepositories, "resourceRepositories"); //$NON-NLS-1$
this.fragmentRepositories = fragmentRepositories;
this.sourceDirectory = new File(workingDirectory, SOURCE_DIRECTORY);
this.classDirectory = new File(workingDirectory, CLASS_DIRECTORY);
this.sourceFiler = new Filer(sourceDirectory, CHARSET);
this.resourceFiler = new Filer(classDirectory, CHARSET);
}
@Override
public PrintWriter openWriter(CompilationUnit source) throws IOException {
Precondition.checkMustNotBeNull(source, "source"); //$NON-NLS-1$
return sourceFiler.openFor(source);
}
@Override
public OutputStream openStream(Name packageNameOrNull, String relativePath) throws IOException {
Precondition.checkMustNotBeNull(relativePath, "relativePath"); //$NON-NLS-1$
File directory = resourceFiler.getFolderFor(packageNameOrNull);
File file = new File(directory, relativePath);
mkdir(file.getParentFile());
return buffering(new FileOutputStream(file));
}
private void mkdir(File file) throws IOException {
assert file != null;
if (file.isDirectory() == false) {
if (file.mkdirs() == false) {
throw new IOException(MessageFormat.format(
Messages.getString("FilePackager.errorFailedToCreateDirectory"), //$NON-NLS-1$
file));
}
}
}
@Override
public void build(OutputStream output) throws IOException {
if (skipCompile()) {
return;
}
compile();
try (JarOutputStream jar = new JarOutputStream(buffering(output))) {
LOG.debug("creating a package of compilation results"); //$NON-NLS-1$
List<ResourceRepository> repos = new ArrayList<>();
if (classDirectory.exists()) {
repos.add(new FileRepository(classDirectory));
}
boolean exists = drain(
jar,
repos,
fragmentRepositories);
if (exists == false) {
LOG.warn(Messages.getString("FilePackager.warnEmptyBuild")); //$NON-NLS-1$
addDummyEntry(jar);
}
}
}
@Override
public void packageSources(OutputStream output) throws IOException {
if (skipCompile()) {
return;
}
LOG.debug("creating a package of generated source files"); //$NON-NLS-1$
try (JarOutputStream jar = new JarOutputStream(buffering(output))) {
boolean exists = drain(
jar,
Collections.singletonList(new FileRepository(sourceDirectory)),
Collections.emptyList());
if (exists == false) {
LOG.warn(Messages.getString("FilePackager.warnEmptySource")); //$NON-NLS-1$
addDummyEntry(jar);
}
}
}
private boolean drain(
JarOutputStream jar,
Iterable<? extends ResourceRepository> main,
Iterable<? extends ResourceRepository> fragments) throws IOException {
assert jar != null;
assert fragments != null;
Set<Location> saw = new HashSet<>();
for (ResourceRepository repo : main) {
drainRepo(repo, jar, saw, true);
}
for (ResourceRepository repo : fragments) {
drainRepo(repo, jar, saw, false);
}
return saw.isEmpty() == false;
}
private void drainRepo(
ResourceRepository repo, JarOutputStream jar, Set<Location> saw,
boolean allowFrameworkInfo) throws IOException {
assert repo != null;
assert jar != null;
assert saw != null;
try (Cursor cursor = repo.createCursor()) {
while (cursor.next()) {
Location location = cursor.getLocation();
if (allowFrameworkInfo == false
&& (FRAMEWORK_INFO.isPrefixOf(location) || MANIFEST_FILE.isPrefixOf(location))) {
LOG.debug("Skipped adding a framework info: {}", location); //$NON-NLS-1$
continue;
}
if (saw.contains(location)) {
LOG.warn(MessageFormat.format(
Messages.getString("FilePackager.warnConflictResource"), //$NON-NLS-1$
location));
continue;
}
saw.add(location);
addEntry(jar, buffering(cursor.openResource()), location);
}
}
}
private void addDummyEntry(JarOutputStream jar) throws IOException {
ZipEntry entry = new ZipEntry(".EMPTY"); //$NON-NLS-1$
entry.setComment("This archive file is empty."); //$NON-NLS-1$
jar.putNextEntry(entry);
}
private void addEntry(
JarOutputStream jar,
InputStream source,
Location location) throws IOException {
assert jar != null;
assert source != null;
assert location != null;
LOG.trace("Adding to jar entry: {}", location); //$NON-NLS-1$
JarEntry entry = new JarEntry(location.toPath('/'));
jar.putNextEntry(entry);
try {
byte[] buffer = new byte[1024];
while (true) {
int read = source.read(buffer);
if (read < 0) {
break;
}
jar.write(buffer, 0, read);
}
jar.closeEntry();
} finally {
source.close();
}
}
private void compile() throws IOException {
LOG.debug("compiling generated Java source files"); //$NON-NLS-1$
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException(
Messages.getString("FilePackager.errorMissingJavaCompiler")); //$NON-NLS-1$
}
if (sourceDirectory.isDirectory() == false) {
return;
}
List<File> sources = collect(sourceDirectory, new ArrayList<File>());
if (sources.isEmpty()) {
return;
}
compile(compiler, sources);
}
private void compile(JavaCompiler compiler, List<File> sources) throws IOException {
assert compiler != null;
assert sources != null;
LOG.debug("compiling Java:{} files -> {}", sources.size(), classDirectory); //$NON-NLS-1$
mkdir(sourceDirectory);
mkdir(classDirectory);
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(
diagnostics,
Locale.getDefault(),
CHARSET)) {
List<String> arguments = new ArrayList<>();
String javaVersion = getJavaVersion();
Collections.addAll(arguments, "-source", javaVersion); //$NON-NLS-1$
Collections.addAll(arguments, "-target", javaVersion); //$NON-NLS-1$
Collections.addAll(arguments, "-encoding", CHARSET.name()); //$NON-NLS-1$
Collections.addAll(arguments,
"-sourcepath", //$NON-NLS-1$
sourceDirectory.getCanonicalFile().toString());
Collections.addAll(arguments,
"-d", //$NON-NLS-1$
classDirectory.getCanonicalFile().toString());
Collections.addAll(arguments, "-proc:none"); //$NON-NLS-1$
Collections.addAll(arguments, "-Xlint:all"); //$NON-NLS-1$
Collections.addAll(arguments, "-Xlint:-options"); //$NON-NLS-1$
Boolean succeeded;
StringWriter errors = new StringWriter();
try (PrintWriter pw = new PrintWriter(errors)) {
LOG.debug("Java compile options: {}", arguments); //$NON-NLS-1$
CompilationTask task = compiler.getTask(
pw,
fileManager,
diagnostics,
arguments,
Collections.emptyList(),
fileManager.getJavaFileObjectsFromFiles(sources));
succeeded = task.call();
}
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
switch (diagnostic.getKind()) {
case ERROR:
case MANDATORY_WARNING:
getEnvironment().error(toMessage(diagnostic));
break;
case WARNING:
LOG.warn(toMessage(diagnostic));
break;
default:
LOG.info(toMessage(diagnostic));
break;
}
}
if (Boolean.TRUE.equals(succeeded) == false) {
throw new IOException(MessageFormat.format(
Messages.getString("FilePackager.errorFailedToCompile"), //$NON-NLS-1$
getEnvironment().getTargetId(),
errors.toString()));
}
}
}
private static String toMessage(Diagnostic<? extends JavaFileObject> diagnostic) {
return String.format("%s (%s:%,d)", //$NON-NLS-1$
diagnostic.getMessage(null),
Optional.ofNullable(diagnostic.getSource())
.map(FileObject::getName)
.orElse("<unknown-file>"), //$NON-NLS-1$
diagnostic.getLineNumber());
}
private String getJavaVersion() {
return getEnvironment().getOptions().getExtraAttribute(KEY_JAVA_VERSION, DEFAULT_JAVA_VERSION);
}
private List<File> collect(File file, List<File> sourceFiles) {
if (file.isFile()) {
LOG.trace("found compilation target: {}", file); //$NON-NLS-1$
sourceFiles.add(file);
} else {
for (File child : list(file)) {
collect(child, sourceFiles);
}
}
return sourceFiles;
}
private static List<File> list(File file) {
return Optional.ofNullable(file.listFiles())
.map(Arrays::asList)
.orElse(Collections.emptyList());
}
private InputStream buffering(InputStream input) {
if (input instanceof BufferedInputStream) {
return input;
}
return new BufferedInputStream(input);
}
private OutputStream buffering(OutputStream output) {
if (output instanceof BufferedOutputStream) {
return output;
}
return new BufferedOutputStream(output);
}
private boolean skipCompile() {
GenericOptionValue option = getEnvironment().getOptions()
.getGenericExtraAttribute(KEY_OPTION_PACKAGING, GenericOptionValue.ENABLED);
if (option == GenericOptionValue.INVALID) {
getEnvironment().error(
Messages.getString("FilePackager.errorInvalidCompilerOption"), //$NON-NLS-1$
getEnvironment().getOptions().getExtraAttributeKeyName(KEY_OPTION_PACKAGING),
getEnvironment().getOptions().getExtraAttribute(KEY_OPTION_PACKAGING),
GenericOptionValue.ENABLED.getSymbol() + '|' + GenericOptionValue.DISABLED.getSymbol());
option = GenericOptionValue.AUTO;
}
return option == GenericOptionValue.DISABLED;
}
}