blob: 48f35e86f9440fdf77ccc5ac5c053985428bfd88 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.myfaces.extensions.scripting.api;
import org.apache.commons.io.FilenameUtils;
import org.apache.myfaces.extensions.scripting.core.reloading.GlobalReloadingStrategy;
import org.apache.myfaces.extensions.scripting.core.util.ClassUtils;
import org.apache.myfaces.extensions.scripting.core.util.FileUtils;
import org.apache.myfaces.extensions.scripting.core.util.StringUtils;
import org.apache.myfaces.extensions.scripting.core.util.WeavingContext;
import org.apache.myfaces.extensions.scripting.monitor.ClassResource;
import org.apache.myfaces.extensions.scripting.monitor.RefreshContext;
import org.apache.myfaces.extensions.scripting.api.extensionevents.FullRecompileRecommended;
import org.apache.myfaces.extensions.scripting.api.extensionevents.FullScanRecommended;
import javax.faces.context.FacesContext;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Refactored the common weaver code into a base class
* <p/>
* Note we added a bean dropping code, the bean dropping works that way
* if we are the first request after a compile issued
* we drop all beans
* <p/>
* every other request has to drop only the session
* and custom scoped beans
* <p/>
* we set small mutexes to avoid at least in our code synchronisation issues
* the mutexes are as atomic as possible to avoid speed problems.
* <p/>
* Unfortunately if someone alters the bean map from outside while we reload
* we for now cannot do anything until we have covered that in the myfaces core!
* <p/>
* Since all weavers are application scoped we can handle the mutexes properly *
*
* @author Werner Punz
* <p/>
* <p/>
* TODO once we have moved over to asynchronous compilation
* we can drop a load of the local code
*/
public abstract class BaseWeaver implements ScriptingWeaver {
/**
* only be set from the
* initialisation code so no thread safety needed
*/
protected ReloadingStrategy _reloadingStrategy = null;
protected DynamicCompiler _compiler = null;
protected ClassScanner _annotationScanner = null;
protected ClassScanner _dependencyScanner = null;
private BeanHandler _beanHandler;
protected String _classPath = "";
Logger _log = Logger.getLogger(this.getClass().getName());
private String _fileEnding = null;
private int _scriptingEngine = ScriptingConst.ENGINE_TYPE_JSF_NO_ENGINE;
public BaseWeaver() {
_reloadingStrategy = new GlobalReloadingStrategy(this);
_beanHandler = new MyFacesBeanHandler(getScriptingEngine());
}
public BaseWeaver(String fileEnding, int scriptingEngine) {
this._fileEnding = fileEnding;
this._scriptingEngine = scriptingEngine;
_reloadingStrategy = new GlobalReloadingStrategy(this);
_beanHandler = new MyFacesBeanHandler(getScriptingEngine());
}
/**
* add custom source lookup paths
*
* @param scriptPath the new path which has to be added
*/
public void appendCustomScriptPath(String scriptPath) {
String normalizedScriptPath = FilenameUtils.normalize(scriptPath);
if (normalizedScriptPath.endsWith(File.separator)) {
normalizedScriptPath = normalizedScriptPath.substring(0, normalizedScriptPath.length() - File.separator.length());
}
WeavingContext.getConfiguration().addSourceDir(getScriptingEngine(), normalizedScriptPath);
if (_annotationScanner != null) {
_annotationScanner.addScanPath(normalizedScriptPath);
}
_dependencyScanner.addScanPath(normalizedScriptPath);
}
/**
* condition which marks a metadata as reload candidate
*
* @param reloadMeta the metadata to be investigated for reload candidacy
* @return true if it is a reload candidate
*/
private boolean isReloadCandidate(ClassResource reloadMeta) {
return reloadMeta != null && assertScriptingEngine(reloadMeta) && reloadMeta.getRefreshAttribute().getRequestedRefreshDate() != 0l;
}
/**
* helper for accessing the reloading metadata map
*
* @return a map with the class name as key and the reloading meta data as value
*/
protected Map<String, ClassResource> getClassMap() {
return WeavingContext.getFileChangedDaemon().getClassMap();
}
/**
* reloads a scripting instance object
*
* @param scriptingInstance the object which has to be reloaded
* @param artifactType integer value indication which type of JSF artifact we have to deal with
* @return the reloaded object with all properties transferred or the original object if no reloading was needed
*/
public Object reloadScriptingInstance(Object scriptingInstance, int artifactType) {
Map<String, ClassResource> classMap = getClassMap();
if (classMap.size() == 0) {
return scriptingInstance;
}
//TODO reload candidate does not necessarily need to
//parse the meta data we also can work
//over the class information
//the main problem is we need the meta data
//for the graph refreshing, so we probably
//have to keep it that way
ClassResource reloadMeta = classMap.get(scriptingInstance.getClass().getName());
try {
//This gives a minor speedup because we jump out as soon as possible
//files never changed do not even have to be considered
//not tainted even once == not even considered to be reloaded
if (isReloadCandidate(reloadMeta)) {
Object reloaded = _reloadingStrategy.reload(scriptingInstance, artifactType);
if (reloaded != null) {
return reloaded;
}
}
return scriptingInstance;
} finally {
//just in case the executed refresh is not triggered by
//the classloader we issue another timestamp here
reloadMeta.getRefreshAttribute().executedRefresh();
}
}
/**
* reweaving of an existing woven class
* by reloading its file contents and then reweaving it
*/
public Class reloadScriptingClass(Class aclass) {
ClassResource metadata = getClassMap().get(aclass.getName());
if (metadata == null)
return aclass;
if (!assertScriptingEngine(metadata)) {
return null;
}
if (!metadata.getRefreshAttribute().requiresRefresh()) {
//if not tainted then we can recycle the last class loaded
return metadata.getAClass();
}
synchronized (RefreshContext.COMPILE_SYNC_MONITOR) {
//another chance just in case someone has reloaded between
//the last if and synchronized, that way we can reduce the number of waiting threads
if (!metadata.getRefreshAttribute().requiresRefresh()) {
//if not tainted then we can recycle the last class loaded
return metadata.getAClass();
}
return loadScriptingClassFromFile(metadata.getSourceDir(), metadata.getSourceFile());
}
}
/**
* recompiles and loads a scripting class from a given class name
*
* @param className the class name including the package
* @return a valid class if the sources could be found null if nothing could be found
*/
public Class loadScriptingClassFromName(String className) {
Map<String, ClassResource> classMap = getClassMap();
ClassResource metadata = classMap.get(className);
if (metadata == null) {
String separator = FileUtils.getFileSeparatorForRegex();
String fileName = className.replaceAll("\\.", separator) + getFileEnding();
for (String pathEntry : WeavingContext.getConfiguration().getSourceDirs(getScriptingEngine())) {
/**
* the reload has to be performed synchronized
* hence there is no chance to do it unsynchronized
*/
synchronized (RefreshContext.COMPILE_SYNC_MONITOR) {
Class retVal = loadScriptingClassFromFile(pathEntry, fileName);
if (retVal != null) {
return retVal;
}
}
}
} else {
return reloadScriptingClass(metadata.getAClass());
}
return null;
}
protected boolean assertScriptingEngine(ClassResource reloadMeta) {
return reloadMeta.getScriptingEngine() == getScriptingEngine();
}
public String getFileEnding() {
return _fileEnding;
}
@SuppressWarnings("unused")
public void setFileEnding(String fileEnding) {
this._fileEnding = fileEnding;
}
public final int getScriptingEngine() {
return _scriptingEngine;
}
@SuppressWarnings("unused")
public void setScriptingEngine(int scriptingEngine) {
this._scriptingEngine = scriptingEngine;
}
public abstract boolean isDynamic(Class clazz);
public ScriptingWeaver getWeaverInstance(Class weaverClass) {
if (getClass().equals(weaverClass)) return this;
return null;
}
/**
* full scan, scans for all artifacts in all files
*/
public void fullClassScan() {
WeavingContext.getExtensionEventRegistry().sendEvent(new FullScanRecommended());
//now we scan the classes which are under our domain
_dependencyScanner.scanPaths();
}
public void postStartupActions() {
if (WeavingContext.getRefreshContext().isRecompileRecommended(getScriptingEngine())) {
// we set a lock over the compile and bean refresh
//and an inner check again to avoid unneeded compile triggers
synchronized (RefreshContext.BEAN_SYNC_MONITOR) {
if (WeavingContext.getConfiguration().isInitialCompile() && WeavingContext.getRefreshContext().isRecompileRecommended(getScriptingEngine())) {
recompileRefresh();
return;
}
}
}
_beanHandler.personalScopeRefresh();
}
public void requestRefresh() {
if (WeavingContext.getRefreshContext().isRecompileRecommended(getScriptingEngine())) {
// we set a lock over the compile and bean refresh
//and an inner check again to avoid unneeded compile triggers
synchronized (RefreshContext.BEAN_SYNC_MONITOR) {
if (WeavingContext.getRefreshContext().isRecompileRecommended(getScriptingEngine())) {
//TODO move this over to application events once they are in place
WeavingContext.getRequestMap().put("REFRESH_JSF_PHASE", Boolean.TRUE);
//
recompileRefresh();
return;
}
}
}
}
public void jsfRequestRefresh() {
if (WeavingContext.getRequestMap().get("REFRESH_JSF_PHASE") != null) {
clearExtvalCache();
/*
* we scan all intra bean dependencies
* which are not covered by our
* class dependency scan
*/
_beanHandler.scanDependencies();
/*
* Now it is time to refresh the tainted managed beans
* by now we should have a good grasp about which beans
* need to to be refreshed (note we cannot cover all corner cases
* but our extended dependency scan should be able to cover
* most refreshing cases.
*/
_beanHandler.refreshAllManagedBeans();
}
if(_annotationScanner != null)
_annotationScanner.scanPaths();
_beanHandler.personalScopeRefresh();
}
/**
* this clears the attached EXT-VAL cache in case of a refresh,
* note this is a temporarily hack once our application
* event system is in place this will be moved over to a specialized event handler
* <p/>
* TODO move this call into a phase listener
*/
private void clearExtvalCache() {
Map fcRequestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestMap();
//TODO do this for the faces context in a phase listener
if (fcRequestMap.containsKey(ScriptingConst.EXT_VAL_REQ_KEY)) {
return;
}
//we have to remove the Validator Factory to clear its cache
//ext-val does basically the same with a replacement one
//but in case of a normal bean validation impl this has to be done
//this is somewhat brute force, but it will be tested it it works out
fcRequestMap.put(ScriptingConst.EXT_VAL_REQ_KEY, Boolean.TRUE);
Set<String> keySet = WeavingContext.getApplicationMap().keySet();
boolean extValPresent = false;
for (String key : keySet) {
if (key.startsWith(ScriptingConst.EXT_VAL_MARKER)) {
WeavingContext.getApplicationMap().remove(key);
extValPresent = true;
}
}
if (!extValPresent) {
fcRequestMap.remove("javax.faces.validator.beanValidator.ValidatorFactory");
}
}
/**
* Loads a list of possible dynamic classNames
* for this scripting engine
*
* @return a list of classNames which are dynamic classes
* for the current compile state on the filesystem
*/
public Collection<String> loadPossibleDynamicClasses() {
Collection<String> scriptPaths = WeavingContext.getConfiguration().getSourceDirs(getScriptingEngine());
List<String> retVal = new LinkedList<String>();
for (String scriptPath : scriptPaths) {
List<File> tmpList = FileUtils.fetchSourceFiles(new File(scriptPath), "*" + getFileEnding());
int lenRoot = scriptPath.length();
//ok O(n2) but we are lazy for now if this imposes a problem we can flatten the inner loop out
for (File sourceFile : tmpList) {
String relativeFile = sourceFile.getAbsolutePath().substring(lenRoot + 1);
String className = ClassUtils.relativeFileToClassName(relativeFile);
retVal.add(className);
}
}
return retVal;
}
public void fullRecompile() {
if (isFullyRecompiled() || !isRecompileRecommended()) {
return;
}
//we now issue the full recompile event here:
WeavingContext.getExtensionEventRegistry().sendEvent(new FullRecompileRecommended(getScriptingEngine()));
//we now issue the recompile for the resources under our domain, TODO it might be wise to move that to an event listener as well
if (_compiler == null) {
_compiler = instantiateCompiler();//new ReflectCompilerFacade();
}
for (String scriptPath : WeavingContext.getConfiguration().getSourceDirs(getScriptingEngine())) {
//compile via javac dynamically, also after this block dynamic compilation
//for the entire length of the request,
try {
if (!StringUtils.isBlank(scriptPath))
_compiler.compileAllFiles(scriptPath, _classPath);
} catch (ClassNotFoundException e) {
_log.logp(Level.SEVERE, "BaseWeaver", "fullyRecompile", e.getMessage(), e);
}
}
markAsFullyRecompiled();
}
protected boolean isRecompileRecommended() {
return WeavingContext.getRefreshContext().isRecompileRecommended(getScriptingEngine());
}
protected boolean isFullyRecompiled() {
try {
return WeavingContext.getRequestMap() != null && WeavingContext.getRequestMap().containsKey(this.getClass().getName() + "_recompiled");
} catch (UnsupportedOperationException ex) {
//still in startup
return false;
}
}
public void markAsFullyRecompiled() {
try {
FacesContext context = FacesContext.getCurrentInstance();
if (context != null) {
//mark the request as tainted with recompile
Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
requestMap.put(this.getClass().getName() + "_recompiled", Boolean.TRUE);
}
} catch (UnsupportedOperationException ex) {
}
touchTaintedClasses();
WeavingContext.getRefreshContext().setRecompileRecommended(getScriptingEngine(), Boolean.FALSE);
}
/**
* helper which returns all tainted classes
*
* @return the tainted classes
*/
private void touchTaintedClasses() {
for (Map.Entry<String, ClassResource> it : WeavingContext.getFileChangedDaemon().getClassMap().entrySet()) {
if (it.getValue().getScriptingEngine() == getScriptingEngine() && it.getValue().getRefreshAttribute().requiresRefresh()) {
FileUtils.touch(ClassUtils.classNameToFile(WeavingContext.getConfiguration().getCompileTarget().getAbsolutePath(), it.getValue().getAClass().getName()));
}
}
}
/**
* loads a class from a given sourceroot and filename
* note this method does not have to be thread safe
* it is called in a thread safe manner by the base class
* <p/>
*
* @param sourceRoot the source search lookup path
* @param file the filename to be compiled and loaded
* @return a valid class if it could be found, null if none was found
*/
protected Class loadScriptingClassFromFile(String sourceRoot, String file) {
//we load the scripting class from the given className
File currentSourceFile = new File(sourceRoot + File.separator + file);
if (!currentSourceFile.exists()) {
return null;
}
if (_log.isLoggable(Level.INFO)) {
_log.info(getLoadingInfo(file));
}
Class retVal = null;
try {
//we initialize the compiler lazy
//because the facade itself is lazy
if (_compiler == null) {
_compiler = instantiateCompiler();//new ReflectCompilerFacade();
}
retVal = _compiler.loadClass(sourceRoot, _classPath, file);
if (retVal == null) {
return retVal;
}
} catch (ClassNotFoundException e) {
//can be safely ignored
if (_log.isLoggable(Level.FINEST)) {
_log.log(Level.FINEST, "loadScriptingClassFromFile(), can be ignored", e);
}
}
//no refresh needed because this is done in the case of java already by
//the classloader
// if (retVal != null) {
// refreshReloadingMetaData(sourceRoot, file, currentClassFile, retVal, ScriptingConst.ENGINE_TYPE_JSF_JAVA);
// }
/**
* we now scan the return value and update its configuration parameters if needed
* this can help to deal with method level changes of class files like managed properties
* or scope changes from shorter running scopes to longer running ones
* if the annotation has been moved the class will be de-registered but still delivered for now
*
* at the next refresh the second step of the registration cycle should pick the new class up
*
*/
try {
if (!scanAnnotation.containsKey(retVal.getName()) && _annotationScanner != null && FacesContext.getCurrentInstance() != null && retVal != null) {
scanAnnotation.put(retVal.getName(), "");
_annotationScanner.scanClass(retVal);
}
} finally {
scanAnnotation.remove(retVal.getName());
}
return retVal;
}
//blocker to prevent recursive calls to the annotation scan which can be triggered by subsequent calls of scanAnnotation and loadClass
//a simple boolean check does not suffice here because scanClass might trigger subsequent calls to other classes
Map<String, String> scanAnnotation = new ConcurrentHashMap<String, String>();
private void recompileRefresh() {
synchronized (RefreshContext.COMPILE_SYNC_MONITOR) {
fullRecompile();
//we update our dependencies and annotation info prior to going
//into the refresh cycle
fullClassScan();
}
}
protected abstract DynamicCompiler instantiateCompiler();
protected abstract String getLoadingInfo(String file);
}