/** * Copyright (C) 2009 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.web; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.exoplatform.commons.utils.Safe; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.container.component.RequestLifeCycle; import org.exoplatform.container.xml.InitParams; import org.exoplatform.container.xml.ValueParam; import org.exoplatform.management.annotations.Impact; import org.exoplatform.management.annotations.ImpactType; import org.exoplatform.management.annotations.Managed; import org.exoplatform.management.annotations.ManagedDescription; import org.exoplatform.management.annotations.ManagedName; import org.exoplatform.management.jmx.annotations.NameTemplate; import org.exoplatform.management.jmx.annotations.Property; import org.exoplatform.management.rest.annotations.RESTEndpoint; import org.exoplatform.web.application.Application; import org.exoplatform.web.controller.QualifiedName; import org.exoplatform.web.controller.metadata.ControllerDescriptor; import org.exoplatform.web.controller.metadata.DescriptorBuilder; import org.exoplatform.web.controller.router.Router; import org.exoplatform.web.controller.router.RouterConfigException; import org.gatein.common.http.QueryStringParser; import org.gatein.common.logging.Logger; import org.gatein.common.logging.LoggerFactory; import org.picocontainer.Startable; /** * The WebAppController is the entry point of the GateIn service. */ @Managed @ManagedDescription("The portal controller") @NameTemplate({ @Property(key = "view", value = "portal"), @Property(key = "service", value = "controller") }) @RESTEndpoint(path = "portalcontroller") public class WebAppController implements Startable { /** . */ public static final QualifiedName HANDLER_PARAM = QualifiedName.create("gtn", "handler"); /** . */ protected static Logger log = LoggerFactory.getLogger(WebAppController.class); /** . */ private final HashMap<String, Object> attributes_; /** . */ private volatile HashMap<String, Application> applications_; /** . */ private final HashMap<String, WebRequestHandler> handlers; /** . */ private final AtomicReference<Router> routerRef; /** . */ private final AtomicReference<String> configurationPathRef; /** * Must have 'controller.config' as init parameter configuration (in xml) * that point to the controller.xml used to initialize navigation controller * * This service component is a container for applications: PortalApplication, PortletApplication, StandaloneApplication.... * It use the navigation controller to route the request to corresponding handlers (config as service plugin) * * @param params the init params * @throws Exception any exception */ public WebAppController(InitParams params) throws Exception { // Get router config ValueParam routerConfig = params.getValueParam("controller.config"); if (routerConfig == null) { throw new IllegalArgumentException("No router param defined"); } String configurationPath = routerConfig.getValue(); // this.applications_ = new HashMap<String, Application>(); this.attributes_ = new HashMap<String, Object>(); this.handlers = new HashMap<String, WebRequestHandler>(); this.routerRef = new AtomicReference<Router>(); this.configurationPathRef = new AtomicReference<String>(configurationPath); // reloadConfiguration(); } public Object getAttribute(String name, Object value) { return attributes_.get(name); } @SuppressWarnings("unchecked") public <T extends Application> T getApplication(String appId) { return (T) applications_.get(appId); } public List<Application> getApplicationByType(String type) { List<Application> applications = new ArrayList<Application>(); for (Application app : applications_.values()) { if (app.getApplicationType().equals(type)) applications.add(app); } return applications; } public synchronized void removeApplication(String appId) { applications_.remove(appId); } @Managed @ManagedDescription("The configuration path") public String getConfigurationPath() { return String.valueOf(configurationPathRef.get()); } @Managed @ManagedDescription("Load the controller configuration") @Impact(ImpactType.WRITE) public void loadConfiguration(@ManagedDescription("The configuration path") @ManagedName("path") String path) throws IOException, RouterConfigException { File f = new File(path); if (!f.exists()) { throw new MalformedURLException("Could not resolve path " + path); } if (!f.isFile()) { throw new MalformedURLException("Could not resolve path " + path + " to a valid file"); } loadConfiguration(f.toURI().toURL()); configurationPathRef.set(path); } private void loadConfiguration(URL url) throws RouterConfigException, IOException { log.info("Loading router configuration " + url); InputStream in = url.openStream(); try { ControllerDescriptor routerDesc = new DescriptorBuilder().build(in); Router router = new Router(routerDesc); routerRef.set(router); } finally { Safe.close(in); } } @Managed @ManagedDescription("Reload the controller configuration") @Impact(ImpactType.WRITE) public void reloadConfiguration() throws RouterConfigException, IOException { log.info("Loading router configuration " + configurationPathRef.get()); loadConfiguration(configurationPathRef.get()); } @Managed @ManagedDescription("Enumerates the routes found for the specified request") @Impact(ImpactType.READ) public String findRoutes( @ManagedDescription("The request uri relative to the web application") @ManagedName("uri") String uri) { Router router = routerRef.get(); if (router != null) { Map<String, String[]> parameters; String path; int pos = uri.indexOf('?'); if (pos != -1) { parameters = QueryStringParser.getInstance().parseQueryString(uri.substring(pos + 1)); path = uri.substring(0, pos); } else { parameters = Collections.emptyMap(); path = uri; } // List<Map<QualifiedName, String>> results = new ArrayList<Map<QualifiedName, String>>(); Iterator<Map<QualifiedName, String>> matcher = router.matcher(path, parameters); while (matcher.hasNext()) { Map<QualifiedName, String> match = matcher.next(); results.add(match); } // return results.toString(); } else { throw new IllegalStateException("No route currently configured"); } } /** * Add application (portlet, gadget) to the global application map if and only if it has not been registered yet. * * @param <T> * @param app * @return */ public <T extends Application> T addApplication(T app) { Application result = getApplication(app.getApplicationId()); // Double-check block if (result == null) { synchronized (this) { result = getApplication(app.getApplicationId()); if (result == null) { HashMap<String, Application> temporalApplicationsMap = new HashMap<String, Application>(applications_); temporalApplicationsMap.put(app.getApplicationId(), app); this.applications_ = temporalApplicationsMap; result = app; } } } return (T) result; } /** * Register an handler as a component plugin, this method is invoked by the kernel with reflection. * * @param handler the handler * @throws Exception any exception */ public void register(WebRequestHandler handler) { handlers.put(handler.getHandlerName(), handler); } public void unregister(String[] paths) { for (String path : paths) { WebRequestHandler handler = handlers.remove(path); handler.onDestroy(this); } } /** * Delegate init action to handler, for example: PortalRequestHandler have * a change to create and register PortalApplication with WebAppController * @param config * @throws Exception */ public void onHandlersInit(ServletConfig config) throws Exception { Collection<WebRequestHandler> hls = handlers.values(); for (WebRequestHandler handler : hls) { handler.onInit(this, config); } } public Router getRouter() { return routerRef.get(); } /** * <p> * This is the first method - in the GateIn portal - reached by incoming HTTP request, it acts like a servlet service() * method. According to the servlet path used the correct handler is selected and then executed. * </p> * * <p> * During a request the request life cycle is demarcated by calls to {@link RequestLifeCycle#begin(ExoContainer);} and * {@link RequestLifeCycle#end()}. * </p> * * @param req the http request * @param res the http response * @throws Exception any exception */ public void service(HttpServletRequest req, HttpServletResponse res) throws Exception { boolean debug = log.isDebugEnabled(); try { // We set the character encoding now to UTF-8 before obtaining parameters req.setCharacterEncoding("UTF-8"); } catch (UnsupportedEncodingException e) { log.error("Encoding not supported", e); } String portalPath = req.getRequestURI().substring(req.getContextPath().length()); Router router = routerRef.get(); // if (router != null) { Iterator<Map<QualifiedName, String>> matcher = router.matcher(portalPath, req.getParameterMap()); // boolean started = false; boolean processed = false; // try { while (matcher.hasNext() && !processed) { // Map<QualifiedName, String> parameters = matcher.next(); String handlerKey = parameters.get(HANDLER_PARAM); if (handlerKey != null) { WebRequestHandler handler = handlers.get(handlerKey); if (handler != null) { if (debug) { log.debug("Serving request path=" + portalPath + ", parameters=" + parameters + " with handler " + handler); } if (!started && handler.getRequiresLifeCycle()) { if (debug) { log.debug("Starting RequestLifeCycle for handler " + handler); } RequestLifeCycle.begin(ExoContainerContext.getCurrentContainer()); started = true; } // processed = handler.execute(new ControllerContext(this, router, req, res, parameters)); } else { if (debug) { log.debug("No handler " + handlerKey + " for request path=" + portalPath + ", parameters=" + parameters); } } } } } finally { if (started) { if (debug) { log.debug("Finishing RequestLifeCycle for current request"); } RequestLifeCycle.end(); } } // if (!processed) { log.error("Could not associate the request path=" + portalPath + " with an handler"); res.sendError(HttpServletResponse.SC_NOT_FOUND); } } else { log.error("Missing valid router configuration " + configurationPathRef.get()); res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } @Override public void start() { } @Override public void stop() { for (WebRequestHandler handler : handlers.values()) { handler.onDestroy(this); } } }