/* * Copyright (C) 2015 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.common.metadata; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.Charset; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import com.google.common.io.Files; import com.google.gwt.core.ext.GeneratorContext; import com.google.gwt.core.ext.typeinfo.JPackage; import com.google.gwt.dev.cfg.ModuleDef; import com.google.gwt.dev.javac.StandardGeneratorContext; /** * @author Mike Brock <cbrock@redhat.com> * @author Christian Sadilek <csadilek@redhat.com> */ public class RebindUtils { public static final String ERRAI_DEVEL_NOCACHE_PROPERTY = "errai.devel.nocache"; public static boolean NO_CACHE = Boolean.getBoolean(ERRAI_DEVEL_NOCACHE_PROPERTY); static Logger logger = LoggerFactory.getLogger(RebindUtils.class); private static String hashSeed = "errai21CR2"; private static volatile String _tempDirectory; public static String getTempDirectory() { if (_tempDirectory != null) { return _tempDirectory; } final String useramePortion = System.getProperty("user.name").replaceAll("[^0-9a-zA-Z]", "-"); final File file = new File(System.getProperty("java.io.tmpdir") + "/" + useramePortion + "/errai/" + getClasspathHash() + "/"); if (!file.exists()) { // noinspection ResultOfMethodCallIgnored file.mkdirs(); } return _tempDirectory = file.getAbsolutePath(); } private static volatile String _classpathHashCache; private static final String[] hashableExtensions = { ".java", ".class", ".properties", ".xml" }; private static boolean isValidFileType(final String fileName) { for (final String extension : hashableExtensions) { if (fileName.endsWith(extension)) return true; } return false; } public static String getClasspathHash() { if (_hasClasspathChanged != null) { return _classpathHashCache; } try { final MessageDigest md = MessageDigest.getInstance("SHA-1"); final String classPath = System.getProperty("java.class.path"); md.update(hashSeed.getBytes()); for (final String p : classPath.split(System.getProperty("path.separator"))) { _recurseDir(new File(p), new FileVisitor() { @Override public void visit(final File f) { final String fileName = f.getName(); if (isValidFileType(fileName)) { md.update(fileName.getBytes()); final long lastModified = f.lastModified(); // md.update((byte) ((lastModified >> 56 & 0xFF))); // md.update((byte) ((lastModified >> 48 & 0xFF))); // md.update((byte) ((lastModified >> 40 & 0xFF))); // md.update((byte) ((lastModified >> 32 & 0xFF))); md.update((byte) ((lastModified >> 24 & 0xFF))); md.update((byte) ((lastModified >> 16 & 0xFF))); md.update((byte) ((lastModified >> 8 & 0xFF))); md.update((byte) ((lastModified & 0xFF))); final long length = f.length(); // // md.update((byte) ((length >> 56 & 0xFF))); // md.update((byte) ((length >> 48 & 0xFF))); // md.update((byte) ((length >> 40 & 0xFF))); // md.update((byte) ((length >> 32 & 0xFF))); md.update((byte) ((length >> 24 & 0xFF))); md.update((byte) ((length >> 16 & 0xFF))); md.update((byte) ((length >> 8 & 0xFF))); md.update((byte) ((length & 0xFF))); } } }); } return _classpathHashCache = hashToHexString(md.digest()); } catch (final Exception e) { throw new RuntimeException("failed to generate hash for classpath fingerprint", e); } } public static String hashToHexString(final byte[] hash) { final StringBuilder hexString = new StringBuilder(); for (final byte b : hash) { hexString.append(Integer.toHexString(0xFF & b)); } return hexString.toString(); } public static File getErraiCacheDir() { String cacheDir = System.getProperty("errai.devel.debugCacheDir"); if (cacheDir == null) cacheDir = new File(".errai/").getAbsolutePath(); final File fileCacheDir = new File(cacheDir); // noinspection ResultOfMethodCallIgnored fileCacheDir.mkdirs(); return fileCacheDir; } public static File getCacheFile(final String name) { return new File(getErraiCacheDir(), name).getAbsoluteFile(); } public static boolean cacheFileExists(final String name) { return getCacheFile(name).exists(); } private static volatile Boolean _hasClasspathChanged; public static boolean hasClasspathChanged() { if (NO_CACHE) return true; if (_hasClasspathChanged != null) return _hasClasspathChanged; final File hashFile = new File(getErraiCacheDir().getAbsolutePath() + "/classpath.sha"); final String hashValue = RebindUtils.getClasspathHash(); if (!hashFile.exists()) { writeStringToFile(hashFile, hashValue); } else { final String fileHashValue = readFileToString(hashFile); if (!fileHashValue.equals(hashValue)) { writeStringToFile(hashFile, hashValue); return _hasClasspathChanged = true; } } return _hasClasspathChanged = false; } private static Map<Class<? extends Annotation>, Boolean> _changeMapForAnnotationScope = new HashMap<Class<? extends Annotation>, Boolean>(); public static boolean hasClasspathChangedForAnnotatedWith(final Set<Class<? extends Annotation>> annotations) { if (Boolean.getBoolean("errai.devel.forcecache")) return true; boolean result = false; for (final Class<? extends Annotation> a : annotations) { /** * We don't terminate prematurely, because we want to cache the hashes for the next run. */ if (hasClasspathChangedForAnnotatedWith(a)) result = true; } return result; } public static boolean hasClasspathChangedForAnnotatedWith(final Class<? extends Annotation> annoClass) { if (NO_CACHE) return true; Boolean changed = _changeMapForAnnotationScope.get(annoClass); if (changed == null) { final File hashFile = new File(getErraiCacheDir().getAbsolutePath() + "/" + annoClass.getName().replaceAll("\\.", "_") + ".sha"); final MetaDataScanner singleton = ScannerSingleton.getOrCreateInstance(); final String hash = singleton.getHashForTypesAnnotatedWith(hashSeed, annoClass); if (!hashFile.exists()) { writeStringToFile(hashFile, hash); changed = Boolean.TRUE; } else { final String fileHashValue = readFileToString(hashFile); if (fileHashValue.equals(hash)) { _changeMapForAnnotationScope.put(annoClass, changed = Boolean.FALSE); } else { writeStringToFile(hashFile, hash); _changeMapForAnnotationScope.put(annoClass, changed = Boolean.TRUE); } } } return changed; } /** * Writes the given Java class source to a file in the correct package subdirectory within the directory returned by * {@link #getErraiCacheDir()}. * * @param packageName * The package name of the Java class. * @param simpleClassName * The simple name of the Java class. * @param source * The source of the Java class. */ public static void writeStringToJavaSourceFileInErraiCacheDir(final String packageName, final String simpleClassName, final String source) { final File dir = new File(getErraiCacheDir() + File.separator + packageName.replace('.', File.separatorChar)); dir.mkdirs(); final File sourceFile = new File(dir, simpleClassName + ".java"); writeStringToFile(sourceFile, source); } public static void writeStringToFile(final File file, final String data) { try { final OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file, false)); outputStream.write(data.getBytes("UTF-8")); outputStream.close(); } catch (final IOException e) { throw new RuntimeException("could not write file for debug cache", e); } } public static String readFileToString(final File file) { try { return Files.toString(file, Charset.forName("UTF-8")); } catch (final IOException e) { throw new RuntimeException("could not read file for debug cache", e); } } public static String packageNameToDirName(final String pkg) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < pkg.length(); i++) { if (pkg.charAt(i) == '.') { sb.append(File.separator); } else { sb.append(pkg.charAt(i)); } } return sb.toString(); } private interface FileVisitor { public void visit(File f); } private static void _recurseDir(final File f, final FileVisitor visitor) { if (f.isDirectory()) { for (final File file : f.listFiles()) { _recurseDir(file, visitor); } } else { visitor.visit(f); } } private static final String[] moduleRootExclusions = { "target/", "out/", "build/", "src/", "war/", "exploded/" }; public static String guessWorkingDirectoryForModule(final GeneratorContext context) { if (context == null) { logger.warn("could not determine module location, using CWD (no context)"); return new File("").getAbsolutePath() + "/"; } try { final List<URL> configUrls = MetaDataScanner.getConfigUrls(); final Set<String> candidateRoots = new HashSet<String>(); final String workingDir = new File("").getAbsolutePath(); Pathcheck: for (final URL url : configUrls) { String filePath = url.getFile(); if (filePath.startsWith(workingDir) && filePath.indexOf('!') == -1) { final int start = workingDir.length() + 1; int firstSubDir = -1; for (int i = start; i < filePath.length(); i++) { if (filePath.charAt(i) == File.separatorChar) { firstSubDir = i; break; } } if (firstSubDir != -1) { filePath = filePath.substring(start, firstSubDir) + "/"; for (final String excl : moduleRootExclusions) { if (filePath.startsWith(excl)) continue Pathcheck; } candidateRoots.add(workingDir + "/" + filePath); } } } if (candidateRoots.isEmpty()) { logger.warn("could not determine module location, using CWD"); return new File("").getAbsolutePath() + "/"; } else if (candidateRoots.size() != 1) { for (final String res : candidateRoots) { logger.warn(" Multiple Possible Roots for Project -> " + res); } throw new RuntimeException("ambiguous module locations for GWT module (specify path property for module)"); } else { return candidateRoots.iterator().next(); } } catch (final Exception e) { throw new RuntimeException("could not determine module package", e); } } private static ModuleDef getModuleDef(final GeneratorContext context) { final StandardGeneratorContext standardGeneratorContext = (StandardGeneratorContext) context; try { final Field moduleField = StandardGeneratorContext.class.getDeclaredField("module"); moduleField.setAccessible(true); return (ModuleDef) moduleField.get(standardGeneratorContext); } catch (final Throwable t) { try { // for GWT versions higher than 2.5.1 we need to get the ModuleDef out of the // CompilerContext final Field compilerContextField = StandardGeneratorContext.class.getDeclaredField("compilerContext"); compilerContextField.setAccessible(true); // Using plain Object because CompilerContext doesn't exist in GWT 2.5 final Object compilerContext = compilerContextField.get(standardGeneratorContext); final Method getModuleMethod = compilerContext.getClass().getMethod("getModule"); return (ModuleDef) getModuleMethod.invoke(compilerContext); } catch (final Throwable t2) { throw new RuntimeException("could not get module definition (you may be using an incompatible GWT version)", t); } } } public static Set<File> getAllModuleXMLs(final GeneratorContext context) { final ModuleDef moduleDef = getModuleDef(context); try { final Field gwtXmlFilesField = ModuleDef.class.getDeclaredField("gwtXmlFiles"); gwtXmlFilesField.setAccessible(true); return (Set<File>) gwtXmlFilesField.get(moduleDef); } catch (final Throwable t) { throw new RuntimeException("could not access 'gwtXmlFiles' field from the module definition " + "(you may be using an incompatible GWT version)"); } } public static Set<String> getInheritedModules(final GeneratorContext context) { final ModuleDef moduleDef = getModuleDef(context); try { final Field inheritedModules = ModuleDef.class.getDeclaredField("inheritedModules"); inheritedModules.setAccessible(true); return (Set<String>) inheritedModules.get(moduleDef); } catch (final Throwable t) { throw new RuntimeException("could not access 'inheritedModules' field from the module definition " + "(you may be using an incompatible GWT version)"); } } public static boolean isModuleInherited(final GeneratorContext context, final String moduleName) { return getInheritedModules(context).contains(moduleName); } public static Set<String> getReloadablePackageNames(final GeneratorContext context) { final Set<String> result = new HashSet<String>(); final ModuleDef module = getModuleDef(context); if (module == null) { return result; } final String moduleName = module.getCanonicalName().replace(".JUnit", ""); result.add(StringUtils.substringBeforeLast(moduleName, ".")); final List<String> dottedModulePaths = new ArrayList<String>(); for (final File moduleXmlFile : getAllModuleXMLs(context)) { String fileName = moduleXmlFile.getAbsolutePath(); fileName = fileName.replace(File.separatorChar, '.'); dottedModulePaths.add(fileName); } for (final String inheritedModule : getInheritedModules(context)) { for (final String dottedModulePath : dottedModulePaths) { if (dottedModulePath.contains(inheritedModule)) { result.add(StringUtils.substringBeforeLast(inheritedModule, ".")); } } } return result; } public static Set<String> getOuterTranslatablePackages(final GeneratorContext context) { final Set<File> xmlRoots = getAllModuleXMLs(context); final Set<String> pathRoots = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); final List<String> classPathRoots = new ArrayList<String>(); try { final Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(""); while (resources.hasMoreElements()) { classPathRoots.add(resources.nextElement().getFile()); } } catch (final IOException e) { e.printStackTrace(); } final ExecutorService executorService = Executors.newCachedThreadPool(); for (final File xmlFile : xmlRoots) { if (xmlFile.exists()) { executorService.execute(new Runnable() { @Override public void run() { InputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(xmlFile)); final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); final Document document = builder.parse(inputStream); final NodeList moduleNodes = document.getElementsByTagName("module"); if (moduleNodes.getLength() > 0) { for (int i = 0; i < moduleNodes.getLength(); i++) { final Node item = moduleNodes.item(i); final String nodeName = item.getNodeName(); if (nodeName.equals("super-source") || nodeName.equals("source")) { final String path = item.getAttributes().getNamedItem("path").getNodeValue(); final String filePath = new File(xmlFile.getParentFile(), path).getAbsolutePath(); for (final String cpRoot : classPathRoots) { if (filePath.startsWith(cpRoot)) { pathRoots.add(filePath.substring(cpRoot.length()) .replace('/', '.').replace('\\', '.')); } } } } } final File clientPath = new File(xmlFile.getParentFile().getAbsoluteFile(), "client").getAbsoluteFile(); if (clientPath.exists()) { final String filePath = clientPath.getAbsolutePath(); for (final String cpRoot : classPathRoots) { if (filePath.startsWith(cpRoot)) { pathRoots.add(filePath.substring(cpRoot.length()).replace('/', '.').replace('\\', '.')); } } } } catch (final ParserConfigurationException e) { e.printStackTrace(); } catch (final SAXException e) { e.printStackTrace(); } catch (final IOException e) { logger.error("error accessing module XML file", e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException e) { logger.warn("problem closing stream", e); } } } } }); } else { logger.warn("the GWT module file '" + xmlFile.getAbsolutePath() + "' does not appear to exist."); } } try { executorService.shutdown(); executorService.awaitTermination(60, TimeUnit.MINUTES); } catch (final InterruptedException e) { e.printStackTrace(); } return pathRoots; } public static String getModuleName(final GeneratorContext context) { try { return getModuleDef(context).getCanonicalName(); } catch (final Throwable t) { return null; } } /** * Returns the list of translatable packages in the module that caused the generator to run (the * module under compilation). */ public static Set<String> findTranslatablePackagesInModule(final GeneratorContext context) { final Set<String> packages = new HashSet<String>(); try { final StandardGeneratorContext stdContext = (StandardGeneratorContext) context; final Field field = StandardGeneratorContext.class.getDeclaredField("module"); field.setAccessible(true); final Object o = field.get(stdContext); final ModuleDef moduleDef = (ModuleDef) o; if (moduleDef == null) { return Collections.emptySet(); } // moduleName looks like "com.foo.xyz.MyModule" and we just want the package part // for tests .JUnit is appended to the module name by GWT final String moduleName = moduleDef.getCanonicalName().replace(".JUnit", ""); final int endIndex = moduleName.lastIndexOf('.'); final String modulePackage = endIndex == -1 ? "" : moduleName.substring(0, endIndex); for (final String packageName : findTranslatablePackages(context)) { if (packageName != null && packageName.startsWith(modulePackage)) { packages.add(packageName); } } } catch (final NoSuchFieldException e) { logger.error("the version of GWT you are running does not appear to be compatible with this version of Errai", e); throw new RuntimeException("could not access the module field in the GeneratorContext"); } catch (final Exception e) { throw new RuntimeException("could not determine module package", e); } return packages; } private static volatile GeneratorContext _lastTranslatableContext; private static volatile Set<String> _translatablePackagesCache; /** * Returns a list of all translatable packages accessible to the module under compilation * (including inherited modules). */ public static Set<String> findTranslatablePackages(final GeneratorContext context) { if (context.equals(_lastTranslatableContext) && _translatablePackagesCache != null) { return _translatablePackagesCache; } _lastTranslatableContext = context; final JPackage[] jPackages = context.getTypeOracle().getPackages(); final Set<String> packages = new HashSet<String>(jPackages.length * 2); for (final JPackage p : jPackages) { packages.add(p.getName()); } return _translatablePackagesCache = Collections.unmodifiableSet(packages); } }