org.jsweet.transpiler.candy.CandyProcessor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsweet-transpiler Show documentation
Show all versions of jsweet-transpiler Show documentation
A Java to TypeScript/JavaScript Open Transpiler
The newest version!
/*
* JSweet transpiler - http://www.jsweet.org
* Copyright (C) 2015 CINCHEO SAS
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jsweet.transpiler.candy;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.jsweet.JSweetConfig;
import org.jsweet.transpiler.JSweetProblem;
import org.jsweet.transpiler.TranspilationHandler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The candy processor extracts and processes what is required from the candy
* jars. It can include:
*
*
* - embedded TypeScript definition files (*.d.ts)
* - embedded JavaScript
*
*
* @author Louis Grignon
*/
public class CandyProcessor {
private static final Logger logger = Logger.getLogger(CandyProcessor.class);
private final static Gson gson = new GsonBuilder().setPrettyPrinting().create();
private String classPath;
/**
* The name of the directory that will contain the candies.
*/
public static final String CANDIES_DIR_NAME = "candies";
/**
* This directory will contain the sources.
*/
public static final String CANDIES_SOURCES_DIR_NAME = CANDIES_DIR_NAME + File.separator + "src";
/**
* The name of the file that stores processed candies info.
*/
public static final String CANDIES_STORE_FILE_NAME = CANDIES_DIR_NAME + File.separator
+ CandyStore.class.getSimpleName() + ".json";
/**
* The name of the directory that contains the TypeScript source files.
*/
public static final String CANDIES_TSDEFS_DIR_NAME = CANDIES_DIR_NAME + File.separator
+ JSweetConfig.TS_LIBS_DIR_NAME;
/**
* Default directory for extracted candies' javascript.
*/
private static final String CANDIES_DEFAULT_JS_DIR_NAME = CANDIES_DIR_NAME + File.separator + "js";
private File candiesSourceDir;
private File candyStoreFile;
private File candiesTsdefsDir;
private File candiesJavascriptOutDir;
private File workingDir;
private Map> extractedJsFilesByCandy = new HashMap<>();
private File tsOutputDir;
/**
* Create a candies processor.
*
* @param workingDir the directory where the processor will
* save all cache and temporary data for
* processing
* @param tsOutputDir directory where TS files will be
* generated by transpiler. It is used for
* ES6 modules support (a node_module dir
* will be generated in this dir)
* @param classPath the classpath where the processor will
* seek for JSweet candies
* @param extractedCandiesJavascriptDir see
* JSweetTranspiler.extractedCandyJavascriptDir
*/
public CandyProcessor(File workingDir, File tsOutputDir, String classPath, File extractedCandiesJavascriptDir) {
this.workingDir = workingDir;
this.tsOutputDir = tsOutputDir;
this.classPath = (classPath == null ? System.getProperty("java.class.path") : classPath);
String[] cp = this.classPath.split(File.pathSeparator);
int[] indices = new int[0];
for (int i = 0; i < cp.length; i++) {
if (cp[i].replace('\\', '/').matches(".*org/jsweet/lib/.*-testbundle/.*/.*-testbundle-.*\\.jar")) {
logger.warn("candies processor ignores classpath entry: " + cp[i]);
indices = ArrayUtils.add(indices, i);
}
}
cp = ArrayUtils.removeAll(cp, indices);
this.classPath = StringUtils.join(cp, File.pathSeparator);
logger.info("candies processor classpath: " + this.classPath);
candiesSourceDir = new File(workingDir, CANDIES_SOURCES_DIR_NAME);
candyStoreFile = new File(workingDir, CANDIES_STORE_FILE_NAME);
candiesTsdefsDir = new File(workingDir, CANDIES_TSDEFS_DIR_NAME);
setCandiesJavascriptOutDir(extractedCandiesJavascriptDir);
}
private List getCandies() {
return getCandiesStore().getCandies();
}
private void setCandiesJavascriptOutDir(File extractedCandiesJavascriptDir) {
this.candiesJavascriptOutDir = extractedCandiesJavascriptDir;
if (this.candiesJavascriptOutDir == null) {
logger.info("extracted candies directory is set to default");
this.candiesJavascriptOutDir = new File(workingDir, CANDIES_DEFAULT_JS_DIR_NAME);
}
logger.info("extracted candies directory: " + extractedCandiesJavascriptDir);
this.candiesJavascriptOutDir.mkdirs();
}
/**
* Returns the directory that contains the orginal TypeScript source code of the
* processed (merged) candies.
*/
public File getCandiesTsdefsDir() {
return candiesTsdefsDir;
}
/**
* Does the processing for the candies jars found in the classpath.
*/
public void processCandies(TranspilationHandler transpilationHandler) throws IOException {
CandyStore candiesStore = getCandiesStore();
LinkedHashMap newCandiesDescriptors = getCandiesDescriptorsFromClassPath(
transpilationHandler);
CandyStore newStore = new CandyStore(new ArrayList<>(newCandiesDescriptors.values()));
if (newStore.equals(candiesStore)) {
logger.info("candies are up to date");
return;
}
this.candyStore = newStore;
logger.info("candies changed, processing candies: " + this.candyStore);
try {
extractCandies(newCandiesDescriptors);
writeCandyStore();
} catch (Throwable t) {
logger.error("cannot generate candies bundle", t);
// exit with fatal if no jar ?
}
}
/**
* Returns true if the candy store contains the J4TS candy.
*/
public boolean isUsingJavaRuntime() {
if (candyStore == null) {
return false;
} else {
for (CandyDescriptor c : candyStore.getCandies()) {
if (c.name != null && c.name.equals("j4ts")) {
logger.info("found j4ts Java runtime in classpath");
return true;
}
}
return false;
}
}
private LinkedHashMap getCandiesDescriptorsFromClassPath(
TranspilationHandler transpilationHandler) throws IOException {
LinkedHashMap jarFilesCollector = new LinkedHashMap<>();
for (String classPathEntry : classPath.split("[" + System.getProperty("path.separator") + "]")) {
if (classPathEntry.endsWith(".jar")) {
File jarFile = new File(classPathEntry);
if (!jarFile.exists()) {
// critical warning, candy not found
logger.warn("candy jar file not found: " + jarFile, new Exception());
continue;
}
try (JarFile jarFileHandle = new JarFile(jarFile)) {
JarEntry candySpecificEntry = jarFileHandle
.getJarEntry("META-INF/maven/" + JSweetConfig.MAVEN_CANDIES_GROUP);
JarEntry candySpecificEntry2 = jarFileHandle.getJarEntry("META-INF/candy-metadata.json");
boolean isCandy = candySpecificEntry != null || candySpecificEntry2 != null;
if (isCandy) {
CandyDescriptor descriptor = CandyDescriptor.fromCandyJar(jarFileHandle,
candiesJavascriptOutDir.getAbsolutePath());
checkCandyVersion(descriptor, transpilationHandler);
jarFilesCollector.put(jarFile, descriptor);
}
}
}
}
logger.info(jarFilesCollector.keySet().size() + " candies found in classpath");
return jarFilesCollector;
}
private String normalizeVersion(String version) {
if (version == null) {
return null;
}
String[] v = JSweetConfig.getVersionNumber().split("\\.");
if (v.length == 2) {
return version;
} else if (v.length == 1) {
return v[0] + ".0";
} else {
return v[0] + "." + v[1];
}
}
private void checkCandyVersion(CandyDescriptor candy, TranspilationHandler transpilationHandler) {
String actualTranspilerVersion = normalizeVersion(JSweetConfig.getVersionNumber().split("-")[0]);
String candyTranspilerVersion = normalizeVersion(
candy.transpilerVersion == null ? null : candy.transpilerVersion.split("-")[0]);
if (candyTranspilerVersion == null || !candyTranspilerVersion.equals(actualTranspilerVersion)) {
transpilationHandler.report(JSweetProblem.CANDY_VERSION_DISCREPANCY, null,
JSweetProblem.CANDY_VERSION_DISCREPANCY.getMessage(candy.name, candy.version,
actualTranspilerVersion, candyTranspilerVersion));
}
}
private void extractCandies(Map candies) throws IOException {
File extractedSourcesDir = candiesSourceDir;
File extractedTsDefsDir = candiesTsdefsDir;
FileUtils.deleteQuietly(extractedSourcesDir);
FileUtils.deleteQuietly(extractedTsDefsDir);
extractedSourcesDir.mkdirs();
extractedTsDefsDir.mkdirs();
for (Map.Entry candy : candies.entrySet()) {
CandyDescriptor candyDescriptor = candy.getValue();
File jarFile = candy.getKey();
String candyName = candyDescriptor.name;
boolean isCore = "jsweet-core".equals(candyName);
try (JarFile jarFileHandle = new JarFile(jarFile)) {
String candyJarName = FilenameUtils.getBaseName(jarFile.getName());
File candyExtractedSourcesDir = new File(extractedSourcesDir, candyJarName);
File candyExtractedJsDir = new File(candiesJavascriptOutDir, candyJarName);
extractCandy( //
candyDescriptor, //
jarFileHandle, //
candyExtractedSourcesDir, //
extractedTsDefsDir, //
candyExtractedJsDir, //
isCore ? tsDefName -> false : null);
}
}
}
private void extractCandy( //
CandyDescriptor descriptor, //
JarFile jarFile, //
File javaOutputDirectory, //
File tsDefOutputDirectory, //
File jsOutputDirectory, //
Predicate isTsDefToBeExtracted) {
logger.info("extract candy: " + jarFile.getName() + " javaOutputDirectory=" + javaOutputDirectory
+ " tsDefOutputDirectory=" + tsDefOutputDirectory + " jsOutputDir=" + jsOutputDirectory);
jarFile.stream()
.filter(entry -> entry.getName().endsWith(".d.ts")
&& (entry.getName().startsWith("src/") || entry.getName().startsWith("META-INF/resources/"))) //
.forEach(entry -> {
File out;
if (entry.getName().endsWith(".java")) {
// RP: this looks like dead code...
out = new File(javaOutputDirectory + "/" + entry.getName().substring(4));
} else if (entry.getName().endsWith(".d.ts")) {
if (isTsDefToBeExtracted != null && !isTsDefToBeExtracted.test(entry.getName())) {
return;
}
out = new File(tsDefOutputDirectory + "/" + entry.getName());
} else {
out = null;
}
extractEntry(jarFile, entry, out);
});
List extractJsFileForCandy = new LinkedList<>();
extractedJsFilesByCandy.put(descriptor.name, extractJsFileForCandy);
for (String jsFilePath : descriptor.jsFilesPaths) {
JarEntry entry = jarFile.getJarEntry(jsFilePath);
String relativeJsPath = jsFilePath.substring(descriptor.jsDirPath.length());
File out = new File(jsOutputDirectory, relativeJsPath);
extractEntry(jarFile, entry, out);
extractJsFileForCandy.add(out);
}
}
private void extractEntry(JarFile jarFile, JarEntry entry, File out) {
if (out == null) {
return;
}
out.getParentFile().mkdirs();
try {
FileUtils.copyInputStreamToFile(jarFile.getInputStream(entry), out);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private CandyStore candyStore;
/**
* Cleans the candies store so that it will be read from file next time.
*/
public void touch() {
candyStore = null;
}
private CandyStore getCandiesStore() {
if (candyStore == null) {
if (candyStoreFile.exists()) {
try {
candyStore = gson.fromJson(FileUtils.readFileToString(candyStoreFile), CandyStore.class);
} catch (Exception e) {
logger.error("cannot read candies index", e);
}
}
if (candyStore == null) {
candyStore = new CandyStore();
}
}
return candyStore;
}
private void writeCandyStore() {
if (candyStore != null) {
try {
FileUtils.write(candyStoreFile, gson.toJson(candyStore));
} catch (Exception e) {
logger.error("cannot read candies index", e);
}
}
}
/**
* Checks if the candy store contains a deprecated candy.
*/
public boolean hasDeprecatedCandy() {
for (CandyDescriptor candy : getCandies()) {
if (candy.transpilerVersion.startsWith("1")) {
return true;
}
}
return false;
}
public List getExtractedJsFiles() {
List extractedJsFiles = new LinkedList<>();
for (List extractedFilesForCandy : getExtractedJsFilesByCandy().values()) {
extractedJsFiles.addAll(extractedFilesForCandy);
}
return extractedJsFiles;
}
public Map> getExtractedJsFilesByCandy() {
return extractedJsFilesByCandy;
}
public void setTsOutputDir(File tsOutputDir) {
this.tsOutputDir = tsOutputDir;
}
}