/**
* Copyright (C) 2016 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.marshalling.rebind.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.util.ClassChangeUtil;
import org.jboss.errai.common.metadata.RebindUtils;
import org.jboss.errai.common.rebind.ClassListReader;
import org.jboss.errai.marshalling.server.MappingContextSingleton;
import org.jboss.errai.marshalling.server.ServerMappingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.typeinfo.JClassType;
/**
* Discovers directories where generated class files can be written.
*/
public class OutputDirectoryUtil {
private static final class ModuleStrategy implements DiscoveryStrategy {
@Override
public Set<String> getCandidate(final GeneratorContext context,
final DiscoveryContext veto) {
final String moduleWorkingDir = RebindUtils.guessWorkingDirectoryForModule(context);
return Arrays
.stream(candidateOutputDirectories)
.map(relPath -> moduleWorkingDir + File.separator + relPath)
.collect(Collectors.toSet());
}
}
private static final class CurrentWorkingDirectoryStrategy implements DiscoveryStrategy {
@Override
public Set<String> getCandidate(final GeneratorContext context,
final DiscoveryContext veto) {
final String cwd = new File("").getAbsolutePath();
return Arrays
.stream(candidateOutputDirectories)
.map(relPath -> cwd + File.separator + relPath)
.collect(Collectors.toSet());
}
}
private static final class MarshallerModelStrategy implements DiscoveryStrategy {
@Override
public Set<String> getCandidate(final GeneratorContext context,
final DiscoveryContext discoveryContext) {
final ServerMappingContext ctx = MappingContextSingleton.get();
final Map<String, String> matchNames = new HashMap<String, String>();
for (final MetaClass cls : ctx.getDefinitionsFactory().getExposedClasses()) {
matchNames.put(cls.getName(), cls.getName());
}
final File cwd = new File("").getAbsoluteFile();
final Set<File> roots = findMatchingOutputDirectoryByModel(matchNames, cwd);
if (roots.isEmpty()) {
discoveryContext.veto();
}
final Set<String> rootsPaths = new HashSet<String>();
for (final File f : roots) {
rootsPaths.add(f.getAbsolutePath());
}
return rootsPaths;
}
}
private static final class ClassListManifestStrategy implements DiscoveryStrategy {
private static class Candidate {
int score;
File root;
}
@Override
public Set<String> getCandidate(final GeneratorContext context, final DiscoveryContext veto) {
final File cwd = new File("").getAbsoluteFile();
final Set<File> matching = findAllMatching("classlist.mf", cwd);
final Set<String> candidateDirectories = new HashSet<String>();
if (!matching.isEmpty()) {
Candidate bestCandidate = null;
String gwtModuleName = RebindUtils.getModuleName(context);
if (gwtModuleName != null) {
if (gwtModuleName.endsWith(".JUnit")) {
gwtModuleName = gwtModuleName.substring(0, gwtModuleName.length() - 6);
}
final int endIndex = gwtModuleName.lastIndexOf('.');
if (endIndex != -1) {
gwtModuleName = gwtModuleName.substring(0, endIndex);
}
else {
gwtModuleName = "";
}
for (final File f : matching) {
final Candidate candidate = new Candidate();
candidate.root = f.getParentFile();
try {
final Set<String> clazzes = ClassListReader.getClassSetFromFile(f);
for (final String fqcn : clazzes) {
final JClassType type = context.getTypeOracle().findType(fqcn);
if (type != null && fqcn.startsWith(gwtModuleName)) {
candidate.score++;
}
}
} catch (final Throwable ignored) {}
if (candidate.score > 0 && (bestCandidate == null || candidate.score > bestCandidate.score)) {
bestCandidate = candidate;
}
}
if (bestCandidate != null) {
candidateDirectories.add(bestCandidate.root.getAbsolutePath());
}
}
}
return candidateDirectories;
}
}
private static class ReverseMatchResult {
private final boolean match;
private final File matchRoot;
private ReverseMatchResult(final boolean match, final File matchRoot) {
this.match = match;
this.matchRoot = matchRoot;
}
public boolean isMatch() {
return match;
}
public File getMatchRoot() {
return matchRoot;
}
}
private static Logger log = LoggerFactory.getLogger(OutputDirectoryUtil.class);
public static final String OUTPUT_DIR_PROP = "errai.server.classOutput";
public static final Optional<String> OUTPUT_DIR = Optional.ofNullable(System.getProperty(OUTPUT_DIR_PROP, null));
private static final String[] candidateOutputDirectories = {
"war/WEB-INF/classes/",
"web/WEB-INF/classes/",
"target/war/WEB-INF/classes/",
"WEB-INF/classes/",
"src/main/webapp/WEB-INF/classes/"
};
private static final DiscoveryStrategy[] rootDiscoveryStrategies = new DiscoveryStrategy[] {
new CurrentWorkingDirectoryStrategy(),
new ClassListManifestStrategy(),
new MarshallerModelStrategy(),
new ModuleStrategy(),
};
public static Set<File> findMatchingOutputDirectoryByModel(final Map<String, String> toMatch, final File from) {
final HashSet<File> matching = new HashSet<File>();
_findMatchingOutputDirectoryByModel(matching, toMatch, from);
return matching;
}
private static void _findMatchingOutputDirectoryByModel(final Set<File> matching,
final Map<String, String> toMatch,
final File from) {
if (from.getName().startsWith(".")) return;
if (from.isDirectory()) {
final File[] files = from.listFiles();
if (files != null) {
for (final File file : files) {
_findMatchingOutputDirectoryByModel(matching, toMatch, file);
}
}
}
else {
String name = from.getName();
if (name.endsWith(".class") && toMatch.containsKey(name = name.substring(0, name.length() - 6))
&& Arrays.stream(candidateOutputDirectories).anyMatch(s -> from.getAbsolutePath().contains(s))) {
final String full = toMatch.get(name);
final ReverseMatchResult res = reversePathMatch(full, from);
if (res.isMatch()) {
matching.add(res.getMatchRoot());
}
}
}
}
public static Set<File> findAllMatching(final String fileName, final File from) {
final HashSet<File> matching = new HashSet<File>();
_findAllMatching(matching, fileName, from);
return matching;
}
public static void _findAllMatching(final HashSet<File> matching, final String fileName, final File from) {
if (from.isDirectory()) {
final File[] files = from.listFiles();
if (files != null) {
for (final File file : files) {
_findAllMatching(matching, fileName, file);
}
}
else {
log.debug("Failed to read: " + from.getAbsolutePath());
}
}
else {
if (fileName.equals(from.getName())) {
matching.add(from);
}
}
}
private static ReverseMatchResult reversePathMatch(final String fqcn, final File location) {
final List<String> stk = new ArrayList<String>(Arrays.asList(fqcn.split("\\.")));
File curr = location;
if (!stk.isEmpty()) {
// remove the last element -- as that would be the file name.
stk.remove(stk.size() - 1);
}
while (!stk.isEmpty()) {
final String el = stk.remove(stk.size() - 1);
curr = curr.getParentFile();
if (curr == null || !curr.getName().equals(el)) {
break;
}
}
if (curr != null) {
curr = curr.getParentFile();
}
if (stk.isEmpty()) {
return new ReverseMatchResult(true, curr);
}
else {
return new ReverseMatchResult(false, curr);
}
}
public static void forEachDiscoveredOutputDir(final GeneratorContext context, final Consumer<File> consumer) {
log.info("Searching candidate output directories...");
File outputDirCdt;
int deposits = 0;
Strategies:
for (final DiscoveryStrategy strategy : rootDiscoveryStrategies) {
final DiscoveryContext discoveryContext = DiscoveryContext.create();
for (final String path : strategy.getCandidate(context, discoveryContext)) {
log.info("Considering '" + path + "' as an output path (" + strategy.getClass().getSimpleName() + ")");
if (discoveryContext.isVetoed()) {
continue Strategies;
}
outputDirCdt = new File(path).getAbsoluteFile();
if (outputDirCdt.exists()) {
log.info(" Accepting '" + outputDirCdt + "' output directory.");
consumer.accept(outputDirCdt);
deposits++;
}
else {
log.info(" Rejecting " + outputDirCdt + " because it does not exist");
}
}
if (deposits > 0) {
break;
}
}
if (deposits == 0) {
log.warn(" A target output could not be resolved through configuration or auto-detection!\n"
+ " Your deployment may be missing required class files.");
}
}
public static void generateClassFileInDiscoveredDirs(final GeneratorContext context, final String packageName, final String simpleClassName,
final String sourceOutputTemp, final String source) {
forEachDiscoveredOutputDir(context, outputDirCdt -> {
try {
final String classFilePath = ClassChangeUtil.generateClassFile(packageName, simpleClassName, sourceOutputTemp,
source, outputDirCdt.getAbsolutePath());
log.info("** Wrote {}.{} class to {}", packageName, simpleClassName, classFilePath);
}
catch (final Throwable t) {
log.warn("Encountered error while trying to generate {}.{} class in {}", packageName, simpleClassName, outputDirCdt.getAbsolutePath());
}
});
}
public static void generateClassFileInTmpDir(final String packageName, final String simpleClassName, final String source, final String tmpDirPath) {
final String classFilePath = ClassChangeUtil.generateClassFile(packageName, simpleClassName, tmpDirPath, source, tmpDirPath);
try {
ClassChangeUtil.loadClassDefinition(classFilePath, packageName, simpleClassName);
} catch (final IOException e) {
throw new RuntimeException("Could not load " + packageName + "." + simpleClassName, e);
}
}
}