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