001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bjalon
016 */
017package org.nuxeo.ecm.mobile;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import javax.servlet.http.HttpServletRequest;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.common.utils.Path;
031import org.nuxeo.ecm.mobile.handler.RequestHandler;
032import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
033import org.nuxeo.runtime.api.Framework;
034import org.nuxeo.runtime.model.ComponentInstance;
035import org.nuxeo.runtime.model.DefaultComponent;
036
037/**
038 * @author <a href="mailto:bjalon@nuxeo.com">Benjamin JALON</a>
039 * @since 5.5
040 */
041public class ApplicationRedirectServiceImpl extends DefaultComponent implements ApplicationDefinitionService {
042
043    private static final Log log = LogFactory.getLog(ApplicationRedirectServiceImpl.class);
044
045    private final Map<String, ApplicationDefinitionDescriptor> applications = new HashMap<String, ApplicationDefinitionDescriptor>();
046
047    private final Map<String, RequestHandlerDescriptor> requestHandlers = new HashMap<String, RequestHandlerDescriptor>();
048
049    private final List<ApplicationDefinitionDescriptor> applicationsOrdered = new ArrayList<ApplicationDefinitionDescriptor>();
050
051    private List<String> unAuthenticatedURLPrefix;
052
053    private Path nuxeoRelativeContextPath;
054
055    public enum ExtensionPoint {
056        applicationDefinition, requestHandlers
057    }
058
059    protected String buildRedirectUrl(HttpServletRequest request, String... uris) {
060        Path path = new Path("");
061        for (String uri : uris) {
062            path = path.append(uri);
063        }
064
065        return VirtualHostHelper.getBaseURL(request) + path.toString();
066    }
067
068    protected Path getNuxeoRelativeContextPath() {
069        if (nuxeoRelativeContextPath == null) {
070            nuxeoRelativeContextPath = new Path(Framework.getProperty("org.nuxeo.ecm.contextPath"));
071        }
072        return nuxeoRelativeContextPath;
073    }
074
075    @Override
076    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
077        ExtensionPoint ep = Enum.valueOf(ExtensionPoint.class, extensionPoint);
078        switch (ep) {
079        case applicationDefinition:
080            registerApplication((ApplicationDefinitionDescriptor) contribution, contributor.getName().getName());
081            break;
082        case requestHandlers:
083            registerRequestHandler((RequestHandlerDescriptor) contribution, contributor.getName().getName());
084            break;
085        default:
086            throw new RuntimeException("error in exception handling configuration");
087        }
088
089    }
090
091    protected void registerRequestHandler(RequestHandlerDescriptor rhd, String componentName) {
092        RequestHandlerDescriptor finalRH = null;
093
094        String requestHandlerName = rhd.getRequestHandlerName();
095
096        if (requestHandlers.containsKey(requestHandlerName)) {
097            if (!rhd.disabled) {
098                String messageTemplate = "Request Handler definition %s will be"
099                        + " overriden by on declared into %s component";
100                String message = String.format(messageTemplate, requestHandlerName, componentName);
101                log.info(message);
102            } else {
103                String messageTemplate = "Request Handler definition '%s' will be removed as defined into %s";
104                String message = String.format(messageTemplate, requestHandlerName, componentName);
105                log.info(message);
106                for (ApplicationDefinitionDescriptor app : applicationsOrdered) {
107                    if (app.getRequestHandlerName().equals(requestHandlerName)) {
108                        messageTemplate = "Request Handler definition '%s' used by %s Application Definition";
109                        message = String.format(messageTemplate, requestHandlerName, app.getName());
110                        log.warn(message);
111                    }
112                }
113            }
114            finalRH = mergeRequestHandlerDescriptor(requestHandlers.get(requestHandlerName), rhd);
115        } else {
116            finalRH = rhd;
117        }
118        requestHandlers.put(requestHandlerName, finalRH);
119
120    }
121
122    private RequestHandlerDescriptor mergeRequestHandlerDescriptor(RequestHandlerDescriptor initial,
123            RequestHandlerDescriptor toMerge) {
124        if (toMerge.klass == null) {
125            toMerge.klass = initial.klass;
126        }
127        if (toMerge.properties == null) {
128            toMerge.properties = initial.properties;
129        }
130        return toMerge;
131    }
132
133    protected void registerApplication(ApplicationDefinitionDescriptor appDescriptor, String componentName) {
134        String name = appDescriptor.getName();
135
136        validateApplicationDescriptor(appDescriptor, componentName);
137
138        if (applications.containsKey(name)) {
139            if (!appDescriptor.isDisable()) {
140                String messageTemplate = "Application definition '%s' will be overridden, "
141                        + "replaced by ones declared into %s component";
142                String message = String.format(messageTemplate, name, componentName);
143                log.info(message);
144                applicationsOrdered.remove(name);
145            } else {
146                String messageTemplate = "Application definition '%s' will be removed as defined into %s";
147                String message = String.format(messageTemplate, name, componentName);
148                log.info(message);
149                disableApplication(name);
150                return;
151            }
152        }
153        if (appDescriptor.isDisable()) {
154            String messageTemplate = "Application definition '%s' already removed, definition into %s component ignored";
155            String message = String.format(messageTemplate, name, componentName);
156            log.info(message);
157            disableApplication(name);
158            return;
159        }
160
161        String messageTemplate = "New Application definition detected '%s' into %s component";
162        String message = String.format(messageTemplate, name, componentName);
163        log.info(message);
164        applications.put(name, appDescriptor);
165        applicationsOrdered.add(appDescriptor);
166        Collections.sort(applicationsOrdered, new MobileApplicationComparator());
167        unAuthenticatedURLPrefix = null;
168    }
169
170    private void disableApplication(String applicationName) {
171        applications.remove(applicationName);
172        for (int i = 0; i < applicationsOrdered.size(); i++) {
173            ApplicationDefinitionDescriptor application = applicationsOrdered.get(i);
174            if (application.getName().equals(applicationName)) {
175                applicationsOrdered.remove(i);
176            }
177        }
178    }
179
180    protected String getBaseURL(HttpServletRequest request) {
181        return VirtualHostHelper.getWebAppName(request);
182    }
183
184    protected RequestHandlerDescriptor getRequestHandlerByName(String name) {
185        return requestHandlers.get(name);
186    }
187
188    private ApplicationDefinitionDescriptor getTargetApplication(HttpServletRequest request) {
189
190        for (ApplicationDefinitionDescriptor application : applicationsOrdered) {
191            RequestHandlerDescriptor rhd = getRequestHandlerByName(application.getRequestHandlerName());
192            if (rhd == null) {
193                String message = "Can't find request handler %s for app definition %s, please check your configuration, skipping check";
194                log.error(String.format(message, application.getRequestHandlerName(), application.getName()));
195                continue;
196            }
197            RequestHandler handler = rhd.getRequestHandlerInstance();
198
199            if (handler.isRequestRedirectedToApplication(request)) {
200                String messageTemplate = "Request '%s' match the application '%s' request handler";
201                String message = String.format(messageTemplate, request.getRequestURI(), application.getName());
202                log.debug(message);
203                return application;
204
205            }
206        }
207
208        log.debug("Request match no application request handler");
209        return null;
210    }
211
212    @Override
213    public String getApplicationBaseURL(HttpServletRequest request) {
214        ApplicationDefinitionDescriptor app = getTargetApplication(request);
215        if (app == null) {
216            log.debug(String.format("No application matched for this request," + " no Application base url found"));
217            return null;
218        }
219        return buildRedirectUrl(request, app.getApplicationRelativePath());
220    }
221
222    @Override
223    public String getApplicationBaseURI(HttpServletRequest request) {
224        ApplicationDefinitionDescriptor app = getTargetApplication(request);
225        if (app == null) {
226            log.debug(String.format("No application matched for this request," + " no Application base uri found"));
227            return null;
228        }
229
230        return getNuxeoRelativeContextPath().append(app.getApplicationRelativePath()).toString();
231    }
232
233    @Override
234    public String getLoginURL(HttpServletRequest request) {
235        ApplicationDefinitionDescriptor app = getTargetApplication(request);
236        if (app == null) {
237            log.debug(String.format("No application matched for this request," + " no Login page found"));
238            return null;
239        }
240
241        return buildRedirectUrl(request, app.getApplicationRelativePath(), app.getLoginPage());
242    }
243
244    @Override
245    public String getLogoutURL(HttpServletRequest request) {
246        ApplicationDefinitionDescriptor app = getTargetApplication(request);
247        if (app == null) {
248            log.debug(String.format("No application matched for this request," + ", no Logout page found"));
249            return null;
250        }
251        return buildRedirectUrl(request, app.getApplicationRelativePath(), app.getLogoutPage());
252    }
253
254    /**
255     * Check that application descriptor is valide and can be registred. Also modify path to if not well formed and log
256     * warn.
257     */
258    private void validateApplicationDescriptor(ApplicationDefinitionDescriptor app, String componentName) {
259        if (app.getName() == null) {
260            String messageTemplate = "Application given in '%s' component is null, " + "can't register it";
261            String message = String.format(messageTemplate, componentName);
262            throw new RuntimeException(message);
263        }
264        if (app.getApplicationRelativePath() == null) {
265            String messageTemplate = "Application name %s given in '%s' component as "
266                    + "an empty base URL, can't register it";
267            String message = String.format(messageTemplate, app.getName(), componentName);
268            throw new RuntimeException(message);
269        }
270        if (app.getApplicationRelativePath().startsWith("/")) {
271            log.warn("Application relative path must not start by a slash, please think"
272                    + " to change your contribution");
273            app.applicationRelativePath = app.getApplicationRelativePath().substring(1);
274        }
275        if (app.getApplicationRelativePath().endsWith("/")) {
276            log.warn("Application relative path must not end with a slash, please think"
277                    + " to change your contribution");
278            app.applicationRelativePath = app.getApplicationRelativePath().substring(0,
279                    app.getApplicationRelativePath().length() - 1);
280        }
281        List<String> resourcesUriChanged = new ArrayList<String>();
282
283        for (String resourceUri : app.getResourcesBaseUrl()) {
284            if (resourceUri.startsWith("/")) {
285                log.warn("Resource Uri relative path must not start by a slash, please"
286                        + " think to change your contribution");
287                resourceUri = resourceUri.substring(1);
288            }
289            resourcesUriChanged.add(resourceUri);
290            app.resourcesBaseUrl = resourcesUriChanged;
291        }
292
293        if (app.getLoginPage() == null) {
294            String messageTemplate = "Application name %s given in '%s' component as "
295                    + "an empty login URL, can't register it";
296            String message = String.format(messageTemplate, app.getName(), componentName);
297            throw new RuntimeException(message);
298        }
299        if (app.getLogoutPage() == null) {
300            String messageTemplate = "Application name %s given in '%s' component as "
301                    + "an empty logout URL, can't register it";
302            String message = String.format(messageTemplate, app.getName(), componentName);
303            throw new RuntimeException(message);
304        }
305    }
306
307    @Override
308    public List<String> getUnAuthenticatedURLPrefix(HttpServletRequest request) {
309        ApplicationDefinitionDescriptor app = getTargetApplication(request);
310
311        return getUnAuthenticatedURLPrefix(app);
312    }
313
314    private List<String> getUnAuthenticatedURLPrefix(ApplicationDefinitionDescriptor app) {
315
316        List<String> result = new ArrayList<String>();
317
318        if (app == null) {
319            return null;
320        }
321
322        String loginPage = new Path(app.getApplicationRelativePath()).append(app.getLoginPage()).toString();
323        log.debug("Add login page as Unauthenticated resources" + loginPage);
324        result.add(loginPage);
325        if (app.getResourcesBaseUrl() != null) {
326            result.addAll(app.getResourcesBaseUrl());
327        } else {
328            log.error("No base URL found can't add unauthenticated URL for application: " + app.getName());
329        }
330
331        return result;
332    }
333
334    @Override
335    public List<String> getUnAuthenticatedURLPrefix() {
336        if (unAuthenticatedURLPrefix == null) {
337            unAuthenticatedURLPrefix = new ArrayList<String>();
338
339            for (ApplicationDefinitionDescriptor app : applicationsOrdered) {
340                unAuthenticatedURLPrefix.addAll(getUnAuthenticatedURLPrefix(app));
341            }
342        }
343        return unAuthenticatedURLPrefix;
344    }
345
346    @Override
347    public boolean isResourceURL(HttpServletRequest request) {
348        ApplicationDefinitionDescriptor app = getTargetApplication(request);
349        if (app == null) {
350            return false;
351        }
352        List<String> resourcesBaseURL = app.getResourcesBaseUrl();
353
354        if (resourcesBaseURL == null || resourcesBaseURL.size() == 0) {
355            return false;
356        }
357        String uri = request.getRequestURI();
358        for (String resourceBaseURL : resourcesBaseURL) {
359            log.debug("Check if this is this Resources application : "
360                    + new Path("/").append(getNuxeoRelativeContextPath()).append(resourceBaseURL) + " for uri : " + uri);
361            if (uri.startsWith(new Path("/").append(getNuxeoRelativeContextPath()).append(resourceBaseURL).toString())) {
362                return true;
363            }
364        }
365        return false;
366    }
367
368}
369
370class MobileApplicationComparator implements Comparator<ApplicationDefinitionDescriptor> {
371
372    private static final Log log = LogFactory.getLog(MobileApplicationComparator.class);
373
374    @Override
375    public int compare(ApplicationDefinitionDescriptor app1, ApplicationDefinitionDescriptor app2) {
376        if (app1.getOrder() == null) {
377            return 1;
378        }
379        if (app2.getOrder() == null) {
380            return -1;
381        }
382        if (app1.getOrder().equals(app2.getOrder())) {
383            log.warn("The two following applications have the same order," + " please change to have different order: "
384                    + app1.getName() + " / " + app2.getName());
385        }
386        return app1.getOrder().compareTo(app2.getOrder());
387
388    }
389}