package act.app;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* 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.
* #L%
*/
import act.Act;
import act.controller.meta.ControllerClassMetaInfo;
import act.metric.Timer;
import act.util.Files;
import act.util.FsChangeDetector;
import act.util.FsEvent;
import act.util.FsEventListener;
import org.osgl.$;
import org.osgl.exception.NotAppliedException;
import org.osgl.logging.L;
import org.osgl.logging.Logger;
import org.osgl.util.C;
import org.osgl.util.S;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static act.app.App.F.*;
/**
* Dev mode application class loader, which is able to
* load classes directly from app src folder
*/
public class DevModeClassLoader extends AppClassLoader {
private final static Logger logger = L.get(DevModeClassLoader.class);
private Map<String, Source> sources = C.newMap();
private final AppCompiler compiler;
private List<FsChangeDetector> detectors = new ArrayList<>();
public DevModeClassLoader(App app) {
super(app);
compiler = new AppCompiler(this);
}
@Override
protected void releaseResources() {
sources.clear();
compiler.destroy();
super.releaseResources();
}
public boolean isSourceClass(String className) {
return sources.containsKey(className);
}
public ControllerClassMetaInfo controllerClassMetaInfo(String controllerClassName) {
return super.controllerClassMetaInfo(controllerClassName);
}
@Override
protected void preload() {
preloadSources();
super.preload();
setupFsChangeDetectors();
}
@Override
protected void preloadClasses() {
// do not preload classes in dev mode
}
@Override
protected void scan() {
super.scan();
compileSources();
scanSources();
}
@Override
protected byte[] loadAppClassFromDisk(String name) {
App app = app();
List<File> srcRoots = app.sourceDirs();
preloadSource(srcRoots, name);
return bytecodeFromSource(name, true);
}
private void addSourceRoot(List<File> sourceRoots, File base, ProjectLayout layout) {
if (null != base && base.isDirectory()) {
sourceRoots.add(layout.source(base));
}
}
@Override
protected byte[] appBytecode(String name, boolean compileSource) {
byte[] bytecode = super.appBytecode(name, compileSource);
return null == bytecode && compileSource ? bytecodeFromSource(name, compileSource) : bytecode;
}
public Source source(String className) {
if (className.contains("$")) {
String name0 = S.before(className, "$");
return sources.get(name0);
}
return sources.get(className);
}
private void preloadSources() {
List<File> sourceRoots = app().allSourceDirs(true);
for (final File sourceRoot : sourceRoots) {
Files.filter(sourceRoot, JAVA_SOURCE, new $.Visitor<File>() {
@Override
public void visit(File file) throws $.Break {
Source source = Source.ofFile(sourceRoot, file);
if (null != source) {
if (null == sources) {
sources = C.newMap();
}
sources.put(source.className(), source);
}
}
});
}
}
private void preloadSource(List<File> sourceRoot, String className) {
if (null != sources) {
Source source = sources.get(className);
if (null != source) {
return;
}
}
Source source = Source.ofClass(sourceRoot, className);
if (null != source) {
if (null == sources) {
sources = C.newMap();
}
sources.put(source.className(), source);
}
}
private void compileSources() {
logger.debug("start to compile sources ...");
compiler.compile(sources.values());
}
private void scanSources() {
Timer timer = metric.startTimer("act:classload:scan:scanSources");
try {
logger.debug("start to scan sources...");
List<AppSourceCodeScanner> scanners = app().scannerManager().sourceCodeScanners();
Set<String> classesNeedByteCodeScan = C.newSet();
if (scanners.isEmpty()) {
//LOGGER.warn("No source code scanner found");
for (String className : sources.keySet()) {
classesNeedByteCodeScan.add(className);
}
} else {
for (String className : sources.keySet()) {
classesNeedByteCodeScan.add(className);
logger.debug("scanning %s ...", className);
List<AppSourceCodeScanner> l = C.newList();
for (AppSourceCodeScanner scanner : scanners) {
if (scanner.start(className)) {
//LOGGER.trace("scanner %s added to the list", scanner.getClass().getName());
l.add(scanner);
}
}
Source source = source(className);
String[] lines = source.code().split("[\\n\\r]+");
for (int i = 0, j = lines.length; i < j; ++i) {
String line = lines[i];
for (AppSourceCodeScanner scanner : l) {
scanner.visit(i, line, className);
}
}
}
}
if (classesNeedByteCodeScan.isEmpty()) {
return;
}
final Set<String> embeddedClassNames = C.newSet();
scanByteCode(classesNeedByteCodeScan, new $.F1<String, byte[]>() {
@Override
public byte[] apply(String s) throws NotAppliedException, $.Break {
return bytecodeFromSource(s, embeddedClassNames);
}
});
while (!embeddedClassNames.isEmpty()) {
Set<String> embeddedClassNameCopy = C.newSet(embeddedClassNames);
scanByteCode(embeddedClassNameCopy, new $.F1<String, byte[]>() {
@Override
public byte[] apply(String s) throws NotAppliedException, $.Break {
return bytecodeFromSource(s, embeddedClassNames);
}
});
embeddedClassNames.removeAll(embeddedClassNameCopy);
}
} finally {
timer.stop();
}
}
private byte[] bytecodeFromSource(String name, boolean compile) {
Source source = source(name);
if (null == source) {
return null;
}
byte[] bytes = source.bytes();
if (null == bytes && compile) {
compiler.compile(name);
bytes = source.bytes();
}
if (name.contains("$")) {
String innerClassName = S.afterFirst(name, "$");
return source.bytes(innerClassName);
}
return bytes;
}
private byte[] bytecodeFromSource(String name, Set<String> embeddedClassNames) {
Source source = source(name);
if (null == source) {
return null;
}
byte[] bytes = source.bytes();
if (null == bytes) {
compiler.compile(name);
bytes = source.bytes();
}
if (!name.contains("$")) {
embeddedClassNames.addAll(C.list(source.innerClassNames()).map(S.F.prepend(name + "$")));
} else {
String innerClassName = S.afterFirst(name, "$");
return source.bytes(innerClassName);
}
return bytes;
}
@Override
public void detectChanges() {
for (FsChangeDetector detector : detectors) {
detectChanges(detector);
}
super.detectChanges();
}
private void detectChanges(FsChangeDetector detector) {
if (null != detector) {
detector.detectChanges();
}
}
private void setupFsChangeDetectors() {
ProjectLayout layout = app().layout();
File appBase = app().base();
List<File> bases = C.newList(appBase);
bases.addAll(app().config().moduleBases());
boolean isTest = "test".equals(Act.profile());
for (File base : bases) {
addDetector(layout.source(base), JAVA_SOURCE, sourceChangeListener);
addDetector(layout.lib(base), JAR_FILE, libChangeListener);
File rsrc = layout.resource(base);
addDetector(rsrc, CONF_FILE.or(ROUTES_FILE), confChangeListener);
addDetector(rsrc, null, resourceChangeListener);
if (isTest) {
addDetector(layout.testSource(base), JAVA_SOURCE, sourceChangeListener);
addDetector(layout.testLib(base), JAR_FILE, libChangeListener);
File testRsrc = layout.testResource(base);
addDetector(testRsrc, CONF_FILE.or(ROUTES_FILE), confChangeListener);
addDetector(testRsrc, null, resourceChangeListener);
}
}
}
private void addDetector(File base, $.Predicate<String> predicate, FsEventListener listener) {
if (null != base && base.isDirectory()) {
detectors.add(new FsChangeDetector(base, predicate, listener));
}
}
private final FsEventListener sourceChangeListener = new FsEventListener() {
@Override
public void on(FsEvent... events) {
throw Act.requestRefreshClassLoader();
}
};
private final FsEventListener libChangeListener = new FsEventListener() {
@Override
public void on(FsEvent... events) {
int len = events.length;
if (len < 0) return;
throw Act.requestRefreshClassLoader();
}
};
private final FsEventListener confChangeListener = new ResourceChangeListener() {
@Override
public void on(FsEvent... events) {
super.on(events);
throw Act.requestRestart();
}
};
private final FsEventListener resourceChangeListener = new ResourceChangeListener();
private class ResourceChangeListener implements FsEventListener {
@Override
public void on(FsEvent... events) {
int len = events.length;
for (int i = 0; i < len; ++i) {
FsEvent e = events[i];
List<String> paths = e.paths();
File[] files = new File[paths.size()];
int idx = 0;
for (String path : paths) {
files[idx++] = new File(path);
}
switch (e.kind()) {
case CREATE:
case MODIFY:
app().builder().copyResources(files);
break;
case DELETE:
app().builder().removeResources(files);
break;
default:
assert false;
}
}
}
}
}