/**
* 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.openejb.server.httpd;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.openejb.AppContext;
import org.apache.openejb.assembler.classic.WebAppBuilder;
import org.apache.openejb.cdi.Proxys;
import org.apache.openejb.core.ParentClassLoaderFinder;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.AppFinder;
import org.apache.openejb.web.LightweightWebAppBuilder;
/**
* @version $Revision$ $Date$
*/
public class HttpListenerRegistry implements HttpListener {
private final Map<String, HttpListener> registry = new LinkedHashMap<>();
private final Map<String, Collection<HttpListener>> filterRegistry = new LinkedHashMap<>();
private final ThreadLocal<FilterListener> currentFilterListener = new ThreadLocal<>();
private final ThreadLocal<HttpRequest> request = new ThreadLocal<>();
private final ClassLoader defaultClassLoader;
private final File[] resourceBases;
private final Map<String, String> defaultContextTypes = new HashMap<>();
private final String welcomeFile = SystemInstance.get().getProperty("openejb.http.welcome", "index.html");
private final Map<String, byte[]> cache = new HashMap<>();
private final boolean cacheResources = "true".equals(SystemInstance.get().getProperty("openejb.http.resource.cache", "false"));
public HttpListenerRegistry() {
HttpServletRequest mock = null;
final SystemInstance systemInstance = SystemInstance.get();
if ("true".equalsIgnoreCase(systemInstance.getProperty("openejb.http.mock-request", "false"))) {
HttpRequestImpl mockRequest = null;
try {
mockRequest = new HttpRequestImpl(new URI("http://mock/"));
mockRequest.parseURI(new StringTokenizer("mock\n")); // will do http://mock/mock, we don't really care
mock = mockRequest;
} catch (final Exception e) {
// no-op
}
}
if (systemInstance.getComponent(HttpServletRequest.class) == null) {
systemInstance.setComponent(HttpServletRequest.class, Proxys.threadLocalProxy(HttpServletRequest.class, request, mock));
}
if (systemInstance.getComponent(HttpSession.class) == null) {
final javax.servlet.http.HttpSession delegate = mock != null ? mock.getSession() : null;
systemInstance.setComponent(javax.servlet.http.HttpSession.class, Proxys.threadLocalRequestSessionProxy(request, new ServletSessionAdapter(delegate) {
@Override
public void invalidate() {
final Object web = AppFinder.findAppContextOrWeb(Thread.currentThread().getContextClassLoader(), AppFinder.AppOrWebContextTransformer.INSTANCE);
if (WebContext.class.isInstance(web)) {
doInvokeSpecificListeners(WebContext.class.cast(web).getContextRoot());
} else if (AppContext.class.isInstance(web)) {
doInvokeSpecificListeners(AppContext.class.cast(web).getId());
}
super.invalidate();
}
private void doInvokeSpecificListeners(final String web) {
final WebAppBuilder wab = SystemInstance.get().getComponent(WebAppBuilder.class);
if (LightweightWebAppBuilder.class.isInstance(wab)) {
final Collection<HttpSessionListener> listeners = LightweightWebAppBuilderListenerExtractor.findByTypeForContext(web, HttpSessionListener.class);
final HttpSessionEvent event = new HttpSessionEvent(this);
for (final HttpSessionListener o : listeners) {
try {
o.sessionDestroyed(event);
} catch (final Throwable th) {
// ignore, may be undeployed
}
}
}
}
}));
}
if (systemInstance.getComponent(ServletContext.class) == null) { // a poor impl but at least we set something
systemInstance.setComponent(ServletContext.class, new EmbeddedServletContext());
}
defaultClassLoader = ParentClassLoaderFinder.Helper.get();
String resourceFolderPaths = SystemInstance.get().getProperty("openejb.embedded.http.resources");
Collection<File> resources = new LinkedList<>();
if (resourceFolderPaths != null) {
for (final String path : resourceFolderPaths.split(" , ")) {
if (!path.isEmpty()) {
resources.add(new File(path));
}
}
}
resourceBases = resources.toArray(new File[resources.size()]);
defaultContextTypes.put("html", "text/html");
defaultContextTypes.put("html", "text/html");
defaultContextTypes.put("css", "text/css");
defaultContextTypes.put("txt", "text/plain");
defaultContextTypes.put("xml", "application/xml");
defaultContextTypes.put("xsl", "application/xml");
defaultContextTypes.put("js", "application/javascript");
defaultContextTypes.put("gif", "image/gif");
defaultContextTypes.put("jpeg", "image/jpeg");
defaultContextTypes.put("jpg", "image/jpeg");
defaultContextTypes.put("png", "image/png");
defaultContextTypes.put("tiff", "image/tiff");
}
@Override
public void onMessage(final HttpRequest request, final HttpResponse response) throws Exception {
final String path;
if (!HttpRequestImpl.class.isInstance(request)) {
path = request.getRequestURI();
} else {
path = getRequestHandledPath(request);
}
final FilterListener currentFL = currentFilterListener.get();
// first look filters
Map<String, Collection<HttpListener>> filters;
synchronized (filterRegistry) {
filters = new HashMap<>(filterRegistry);
}
final HttpRequest registered = this.request.get();
final boolean reset = registered == null;
try {
if (reset) {
this.request.set(request);
}
boolean lastWasCurrent = false;
for (Map.Entry<String, Collection<HttpListener>> entry : filters.entrySet()) {
String pattern = entry.getKey();
for (HttpListener listener : entry.getValue()) {
if ((lastWasCurrent || currentFL == null) && path.matches(pattern)) {
listener.onMessage(request, response);
return;
}
lastWasCurrent = listener == currentFL;
}
}
// then others
Map<String, HttpListener> listeners;
synchronized (registry) {
listeners = new HashMap<>(registry);
}
boolean found = false;
for (final Map.Entry<String, HttpListener> entry : listeners.entrySet()) {
final String pattern = entry.getKey();
if (path.matches(pattern) || path.equals(pattern)) {
if (pattern.contains("/.*\\.") && HttpRequestImpl.class.isInstance(request)) { // TODO: enhance it, basically servlet *.xxx
HttpRequestImpl.class.cast(request).noPathInfo();
}
entry.getValue().onMessage(request, response);
found = true;
break;
}
}
if (!found) {
final String servletPath = request.getServletPath();
if (servletPath != null) {
URL url = SystemInstance.get().getComponent(ServletContext.class).getResource(servletPath);
if (url != null) {
serveResource(servletPath, response, url);
} else {
final String pathWithoutSlash = "/".equals(path) || "".equals(servletPath) || "/".equals(servletPath) ? welcomeFile :
(servletPath.startsWith("/") ? servletPath.substring(1) : servletPath);
url = defaultClassLoader.getResource("META-INF/resources/" + pathWithoutSlash);
if (url != null) {
serveResource(servletPath, response, url);
} else if (resourceBases.length > 0) {
for (final File f : resourceBases) {
final File file = new File(f, pathWithoutSlash);
if (file.isFile()) {
url = file.toURI().toURL();
serveResource(servletPath, response, url);
break;
}
}
}
}
if (url != null) {
final int dot = servletPath.lastIndexOf('.');
if (dot > 0 && dot < servletPath.length() - 1) {
final String ext = servletPath.substring(dot + 1);
final String ct = defaultContextTypes.get(ext);
if (ct != null) {
response.setContentType(ct);
} else {
final String uct = SystemInstance.get().getProperty("openejb.embedded.http.content-type." + ext);
if (uct != null) {
response.setContentType(uct);
}
}
}
}
} // TODO else 404
}
} finally {
if (currentFL == null) {
currentFilterListener.set(null);
}
if (reset) {
this.request.set(null);
}
}
}
private void serveResource(final String key, final HttpResponse response, final URL url) throws IOException {
if (cacheResources) {
byte[] value = cache.get(key);
if (value == null) {
final InputStream from = url.openStream();
try {
ByteArrayOutputStream to = new ByteArrayOutputStream();
IO.copy(from, to);
value = to.toByteArray();
cache.put(key, value);
} finally {
IO.close(from);
}
}
response.getOutputStream().write(value);
} else {
final InputStream from = url.openStream();
try {
IO.copy(from, response.getOutputStream());
} finally {
IO.close(from);
}
}
}
private String getRequestHandledPath(final HttpRequest request) {
final String servletPath = request.getServletPath();
return request.getContextPath() + (!servletPath.startsWith("/") ? "/" : "") + servletPath;
}
public void addHttpListener(HttpListener listener, String regex) {
synchronized (registry) {
registry.put(regex, listener);
}
}
public HttpListener removeHttpListener(String regex) {
HttpListener listener;
synchronized (registry) {
listener = registry.remove(regex);
}
return listener;
}
public void addHttpFilter(HttpListener listener, String regex) {
synchronized (filterRegistry) {
if (!filterRegistry.containsKey(regex)) {
filterRegistry.put(regex, new ArrayList<HttpListener>());
}
filterRegistry.get(regex).add(listener);
}
}
public Collection<HttpListener> removeHttpFilter(String regex) {
synchronized (filterRegistry) {
return filterRegistry.remove(regex);
}
}
public void setOrigin(final FilterListener origin) {
if (origin == null) {
currentFilterListener.remove();
} else {
currentFilterListener.set(origin);
}
}
}