/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.js;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import io.undertow.js.templates.TemplateProvider;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.RoutingHandler;
import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceChangeListener;
import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.util.AttachmentKey;
import io.undertow.util.FileUtils;
import io.undertow.util.Methods;
import io.undertow.util.StatusCodes;
import io.undertow.websockets.WebSocketConnectionCallback;
import io.undertow.websockets.WebSocketProtocolHandshakeHandler;
/**
* Builder class for Undertow Javascipt deployments
*
* @author Stuart Douglas
*/
public class UndertowJS {
private static final AttachmentKey<HttpHandler> NEXT = AttachmentKey.create(HttpHandler.class);
public static final int HOT_DEPLOYMENT_INTERVAL = 500;
private final List<ResourceSet> resources;
private final boolean hotDeployment;
private final Map<ResourceSet, ResourceChangeListener> listeners = new IdentityHashMap<>();
private final ClassLoader classLoader;
private final Map<String, InjectionProvider> injectionProviders;
private final JavabeanIntrospector javabeanIntrospector = new JavabeanIntrospector();
private final List<HandlerWrapper> handlerWrappers;
private final ResourceManager resourceManager;
private final Map<String, TemplateProvider> templateProviders;
private ScriptEngine engine;
private HttpHandler routingHandler;
private Map<Resource, Date> lastModified;
private volatile long lastHotDeploymentCheck = -1;
private volatile Set<String> rejectPaths = Collections.emptySet();
/**
*
* @param resources
* @param hotDeployment
* @param classLoader
* @param injectionProviders
* @param handlerWrappers
* @param resourceManager
* @param templateProviders
*/
public UndertowJS(List<ResourceSet> resources, boolean hotDeployment, ClassLoader classLoader, Map<String, InjectionProvider> injectionProviders, List<HandlerWrapper> handlerWrappers, ResourceManager resourceManager, Map<String, TemplateProvider> templateProviders) {
this.classLoader = classLoader;
this.injectionProviders = injectionProviders;
this.handlerWrappers = handlerWrappers;
this.resources = new ArrayList<>(resources);
this.hotDeployment = hotDeployment;
this.resourceManager = resourceManager;
this.templateProviders = templateProviders;
}
public UndertowJS start() throws ScriptException, IOException {
buildEngine();
return this;
}
public Object evaluate(String code) throws ScriptException {
return engine.eval(code);
}
private synchronized void buildEngine() throws ScriptException, IOException {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
RoutingHandler routingHandler = new RoutingHandler(true);
routingHandler.setFallbackHandler(new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.getAttachment(NEXT).handleRequest(exchange);
}
});
RoutingHandler wsRoutingHandler = new RoutingHandler(false);
wsRoutingHandler.setFallbackHandler(routingHandler);
for (TemplateProvider templateProvider : templateProviders.values()) {
// TODO properties should be configurable
templateProvider.init(Collections.emptyMap(), resourceManager);
}
UndertowSupport support = new UndertowSupport(routingHandler, classLoader, injectionProviders, javabeanIntrospector, handlerWrappers, resourceManager, wsRoutingHandler, templateProviders);
engine.put("$undertow_support", support);
engine.put(ScriptEngine.FILENAME, "undertow-core-scripts.js");
engine.eval(FileUtils.readFile(UndertowJS.class, "undertow-core-scripts.js"));
Map<Resource, Date> lm = new HashMap<>();
final Set<String> rejectPaths = new HashSet<>();
for (ResourceSet set : resources) {
for (String resource : set.getResources()) {
if(resource.startsWith("/")) {
rejectPaths.add(resource);
} else {
rejectPaths.add("/" + resource);
}
Resource res = set.getResourceManager().getResource(resource);
if (res == null) {
UndertowScriptLogger.ROOT_LOGGER.couldNotReadResource(resource);
} else {
try (InputStream stream = res.getUrl().openStream()) {
engine.put(ScriptEngine.FILENAME, res.getUrl().toString());
engine.eval(new InputStreamReader(new BufferedInputStream(stream)));
}
if (hotDeployment) {
lm.put(res, res.getLastModified());
}
}
}
}
this.engine = engine;
this.routingHandler = wsRoutingHandler;
this.lastModified = lm;
this.rejectPaths = Collections.unmodifiableSet(rejectPaths);
}
public UndertowJS stop() {
for (Map.Entry<ResourceSet, ResourceChangeListener> entry : listeners.entrySet()) {
entry.getKey().getResourceManager().removeResourceChangeListener(entry.getValue());
}
listeners.clear();
for (TemplateProvider templateProvider : templateProviders.values()) {
templateProvider.cleanup();
}
engine = null;
return this;
}
public HttpHandler getHandler(final HttpHandler next) {
return new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (hotDeployment) {
long lastHotDeploymentCheck = UndertowJS.this.lastHotDeploymentCheck;
if (System.currentTimeMillis() > lastHotDeploymentCheck + HOT_DEPLOYMENT_INTERVAL) {
synchronized (UndertowJS.this) {
if (UndertowJS.this.lastHotDeploymentCheck == lastHotDeploymentCheck) {
for (Map.Entry<Resource, Date> entry : lastModified.entrySet()) {
if (!entry.getValue().equals(entry.getKey().getLastModified())) {
UndertowScriptLogger.ROOT_LOGGER.rebuildingDueToFileChange(entry.getKey().getPath());
buildEngine();
break;
}
}
UndertowJS.this.lastHotDeploymentCheck = System.currentTimeMillis();
}
}
}
}
if(rejectPaths.contains(exchange.getRelativePath())) {
exchange.setResponseCode(StatusCodes.NOT_FOUND);
exchange.endExchange();
return;
}
exchange.putAttachment(NEXT, next);
routingHandler.handleRequest(exchange);
}
};
}
public HandlerWrapper getHandlerWrapper() {
return new HandlerWrapper() {
@Override
public HttpHandler wrap(HttpHandler handler) {
return getHandler(handler);
}
};
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
Builder() {
}
private final List<ResourceSet> resources = new ArrayList<>();
private boolean hotDeployment = true;
private ClassLoader classLoader = UndertowJS.class.getClassLoader();
private final Map<String, InjectionProvider> injectionProviders = new HashMap<>();
private final List<HandlerWrapper> handlerWrappers = new ArrayList<>();
private ResourceManager resourceManager;
private final Map<String, TemplateProvider> templateProviders = new HashMap<>();
public ResourceSet addResourceSet(ResourceManager manager) {
ResourceSet resourceSet = new ResourceSet(manager);
resources.add(resourceSet);
return resourceSet;
}
public Builder addResources(ResourceManager manager, String... resources) {
ResourceSet resourceSet = new ResourceSet(manager);
resourceSet.addResources(resources);
this.resources.add(resourceSet);
return this;
}
public Builder addResources(ResourceManager manager, Collection<String> resources) {
ResourceSet resourceSet = new ResourceSet(manager);
resourceSet.addResources(resources);
this.resources.add(resourceSet);
return this;
}
public boolean isHotDeployment() {
return hotDeployment;
}
public Builder setHotDeployment(boolean hotDeployment) {
this.hotDeployment = hotDeployment;
return this;
}
public ClassLoader getClassLoader() {
return classLoader;
}
public Builder setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
return this;
}
public Builder addInjectionProvider(InjectionProvider provider) {
this.injectionProviders.put(provider.getPrefix(), provider);
return this;
}
public Builder addHandlerWrapper(HandlerWrapper handlerWrapper) {
this.handlerWrappers.add(handlerWrapper);
return this;
}
public Builder setResourceManager(ResourceManager resourceManager) {
this.resourceManager = resourceManager;
return this;
}
public Builder addTemplateProvider(TemplateProvider provider) {
this.templateProviders.put(provider.name(), provider);
return this;
}
public UndertowJS build() {
return new UndertowJS(resources, hotDeployment, classLoader, injectionProviders, handlerWrappers, resourceManager, templateProviders);
}
}
public static class ResourceSet {
private final ResourceManager resourceManager;
private final List<String> resources = new ArrayList<>();
ResourceSet(ResourceManager resourceManager) {
this.resourceManager = resourceManager;
}
public ResourceManager getResourceManager() {
return resourceManager;
}
public ResourceSet addResource(String resource) {
this.resources.add(resource);
return this;
}
public ResourceSet addResources(String... resource) {
this.resources.addAll(Arrays.asList(resource));
return this;
}
public ResourceSet addResources(Collection<String> resource) {
this.resources.addAll(resource);
return this;
}
public List<String> getResources() {
return Collections.unmodifiableList(resources);
}
}
/**
* class that is used to inspect java objects from scripts
*/
public static final class JavabeanIntrospector {
private JavabeanIntrospector() {
}
private Map<Class<?>, Map<String, Method>> cache = new ConcurrentHashMap<>();
public Map<String, Method> inspect(Class<?> clazz) {
Map<String, Method> existing = cache.get(clazz);
if (existing != null) {
return existing;
}
existing = new HashMap<>();
for (Method method : clazz.getMethods()) {
if (method.isBridge() || Modifier.isStatic(method.getModifiers())) {
continue;
}
if (method.getName().equals("getClass")) {
continue;
}
if (method.getParameterCount() == 0 &&
method.getName().startsWith("get") &&
method.getName().length() > 3 &&
method.getReturnType() != void.class) {
existing.put(Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4), method);
}
}
cache.put(clazz, existing);
return existing;
}
}
/**
* Holder class for objects that undertow needs to access from the script environment
*/
public static class UndertowSupport {
private final RoutingHandler routingHandler;
private final ClassLoader classLoader;
private final Map<String, InjectionProvider> injectionProviders;
private final JavabeanIntrospector javabeanIntrospector;
private final List<HandlerWrapper> handlerWrappers;
private final ResourceManager resourceManager;
private final RoutingHandler wsRoutingHandler;
private final Map<String, TemplateProvider> templateProviders;
public UndertowSupport(RoutingHandler routingHandler, ClassLoader classLoader, Map<String, InjectionProvider> injectionProviders, JavabeanIntrospector javabeanIntrospector, List<HandlerWrapper> handlerWrappers, ResourceManager resourceManager, RoutingHandler wsRoutingHandler, Map<String, TemplateProvider> templateProviders) {
this.routingHandler = routingHandler;
this.classLoader = classLoader;
this.injectionProviders = injectionProviders;
this.javabeanIntrospector = javabeanIntrospector;
this.handlerWrappers = handlerWrappers;
this.resourceManager = resourceManager;
this.wsRoutingHandler = wsRoutingHandler;
this.templateProviders = templateProviders;
}
public ClassLoader getClassLoader() {
return classLoader;
}
public Map<String, InjectionProvider> getInjectionProviders() {
return injectionProviders;
}
public JavabeanIntrospector getJavabeanIntrospector() {
return javabeanIntrospector;
}
public List<HandlerWrapper> getHandlerWrappers() {
return handlerWrappers;
}
public RoutingHandler getRoutingHandler() {
return routingHandler;
}
public ResourceManager getResourceManager() {
return resourceManager;
}
public Map<String, TemplateProvider> getTemplateProviders() {
return templateProviders;
}
public void addWebsocket(String path, WebSocketConnectionCallback callback) {
wsRoutingHandler.add(Methods.GET, path, new WebSocketProtocolHandshakeHandler(callback, routingHandler));
}
public InjectionContext getInjectionContext(String name) {
return new DefaultInjectionContext(name);
}
}
private static final class DefaultInjectionContext implements InjectionContext {
private final String name;
private Runnable requestHandledCallback;
DefaultInjectionContext(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void setRequestHandledCallback(Runnable callback) {
this.requestHandledCallback = callback;
}
@Override
public Runnable getRequestHandledCallback() {
return requestHandledCallback;
}
}
}