/* * Copyright 2013 Guidewire Software, Inc. */ package gw.test.servlet; import gw.classredefiner.ClassRedefinerAgent; import gw.config.CommonServices; import gw.fs.IDirectory; import gw.fs.IFile; import gw.fs.watcher.DirectoryWatcher; import gw.fs.watcher.DirectoryWatcher.FileEvent; import gw.lang.UnstableAPI; import gw.lang.reflect.IType; import gw.lang.reflect.ITypeRef; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.gs.IGosuClass; import java.lang.instrument.ClassDefinition; import java.lang.instrument.UnmodifiableClassException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @UnstableAPI public class ChangedTypesRefresher { private DirectoryWatcher _directoryWatcher; private static ChangedTypesRefresher _instance = new ChangedTypesRefresher(); public static ChangedTypesRefresher getInstance() { return _instance; } public void initWatching() { _directoryWatcher = new DirectoryWatcher(); List<? extends IDirectory> sourceEntries = TypeSystem.getCurrentModule().getSourcePath(); for (IDirectory sourceEntry : sourceEntries) { // Ignore /classes directories and jar files if (!sourceEntry.getPath().getName().equals("classes") && !sourceEntry.getPath().getName().endsWith(".jar") && sourceEntry.isJavaFile()) { _directoryWatcher.watchDirectoryTree(sourceEntry.toJavaFile().toPath()); } } } public void reloadChangedTypes() { // Assume that the first time this is called, nothing will have changed that we care about if (_directoryWatcher == null) { initWatching(); } else { Map<Path, FileEvent> changes = _directoryWatcher.getChangesSinceLastTime(); if (changes.isEmpty()) { return; } boolean canRefreshSelectively = true; Set<String> changedGosuTypes = new HashSet<String>(); for (Map.Entry<Path, FileEvent> change : changes.entrySet()) { Path changedFilePath = change.getKey(); FileEvent changeType = change.getValue(); String fileName = changedFilePath.getFileName().toString(); // Only worry about .gs or .gsx files for now if (fileName.endsWith(".gs") || fileName.endsWith(".gsx")) { // If a file was added, it'll get picked up anyway. If it was deleted, there should be no remaining // references to it. So only worry about modifications. if (changeType == FileEvent.MODIFY) { IFile changedFile = CommonServices.getFileSystem().getIFile(changedFilePath.toFile()); for (IDirectory possibleSourceDir : TypeSystem.getCurrentModule().getSourcePath()) { if (changedFile.isDescendantOf(possibleSourceDir)) { String relativeFilePath = possibleSourceDir.getPath().relativePath(changedFile.getPath(), "/"); String typeName = relativeFilePath.replace('/', '.'); typeName = typeName.substring(0, typeName.lastIndexOf(".")); changedGosuTypes.add(typeName); System.out.println("DEBUG: Found changes to " + typeName); } } } } else if (fileName.endsWith(".pcf") || fileName.endsWith(".xsd") || fileName.endsWith(".wsdl")){ // Force a full refresh canRefreshSelectively = false; } else { // Assume it's a directory, and just ignore it: we'll have specific file changes for the files within it anyway } } // TODO - AHK - We may need some system flag here boolean shouldReloadBytecode = true; List<TypeClassPair> changedGosuClasses = new ArrayList<TypeClassPair>(); if (shouldReloadBytecode) { // For each changed Gosu type, see if it needs to be swapped. Note that we do this before any type system refresh // happens, since we need to see if the class needs to be swapped for (String typeName : changedGosuTypes) { try { IType type = TypeSystem.getByFullName(typeName); if (type instanceof IGosuClass) { List<IGosuClass> allInnerClasses = new ArrayList<IGosuClass>(); addInnerClassTreeToList((IGosuClass) type, allInnerClasses); for (IGosuClass gosuClass : allInnerClasses) { changedGosuClasses.add(new TypeClassPair(gosuClass, gosuClass.getBackingClass())); } } } catch (Exception e) { // Hmm . . . just swallow it for now and keep going System.out.println("WARN: Exception during reload of " + typeName); e.printStackTrace(); } } } if (canRefreshSelectively && changedGosuTypes.size() < 20) { refreshSpecifiedFiles(changedGosuTypes); } else { System.out.println("DEBUG: Refreshing all types"); TypeSystem.refresh(false); } if (shouldReloadBytecode) { reloadBytecodeForClasses(changedGosuClasses); } } } private class TypeClassPair { private IGosuClass _gsClass; private Class _javaClass; private TypeClassPair(IGosuClass gsClass, Class javaClass) { _gsClass = gsClass; _javaClass = javaClass; } } private void reloadBytecodeForClasses(List<TypeClassPair> changedGosuClasses) { TypeSystem.lock(); try { ClassDefinition[] classDefinitions = new ClassDefinition[changedGosuClasses.size()]; for (int i = 0; i < changedGosuClasses.size(); i++) { IGosuClass gsClass = changedGosuClasses.get(i)._gsClass; System.out.println("DEBUG: Recompiling " + gsClass.getName()); byte[] bytes = TypeSystem.getGosuClassLoader().getBytes(gsClass); classDefinitions[i] = new ClassDefinition(changedGosuClasses.get(i)._javaClass, bytes); } try { ClassRedefinerAgent.redefineClasses(classDefinitions); } catch (ClassNotFoundException e) { System.out.println("ERROR: Class redefinition failed:"); e.printStackTrace(); } catch (UnmodifiableClassException e) { System.out.println("ERROR: Class redefinition failed:"); e.printStackTrace(); } } finally { TypeSystem.unlock(); } } private void addInnerClassTreeToList(IGosuClass gosuClass, List<IGosuClass> classes) { classes.add(gosuClass); for (IGosuClass innerClass : gosuClass.getInnerClasses()) { addInnerClassTreeToList(innerClass, classes); } } private void refreshSpecifiedFiles(Set<String> types) { // TODO - AHK - Bail out and refresh everything if it doesn't work out? for (String typeName : types) { try { IType type = TypeSystem.getByFullName(typeName); TypeSystem.refresh((ITypeRef) type); } catch (Exception e) { System.out.println("Error refreshing " + typeName); e.printStackTrace(); } System.out.println("DEBUG: Refreshing type " + typeName); } } }