org.wso2.carbon.uuf.core.App.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.carbon.uuf.core.App.java

Source

/*
 *  Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. 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.wso2.carbon.uuf.core;

import com.google.gson.JsonObject;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.uuf.api.Placeholder;
import org.wso2.carbon.uuf.api.auth.Session;
import org.wso2.carbon.uuf.api.config.Bindings;
import org.wso2.carbon.uuf.api.config.Configuration;
import org.wso2.carbon.uuf.api.config.I18nResources;
import org.wso2.carbon.uuf.api.exception.RenderingException;
import org.wso2.carbon.uuf.api.model.MapModel;
import org.wso2.carbon.uuf.internal.exception.FragmentNotFoundException;
import org.wso2.carbon.uuf.internal.exception.HttpErrorException;
import org.wso2.carbon.uuf.internal.exception.PageNotFoundException;
import org.wso2.carbon.uuf.internal.exception.PageRedirectException;
import org.wso2.carbon.uuf.internal.exception.PluginExecutionException;
import org.wso2.carbon.uuf.internal.exception.SessionNotFoundException;
import org.wso2.carbon.uuf.internal.util.NameUtils;
import org.wso2.carbon.uuf.internal.util.UriUtils;
import org.wso2.carbon.uuf.spi.HttpRequest;
import org.wso2.carbon.uuf.spi.HttpResponse;
import org.wso2.carbon.uuf.spi.auth.Authorizer;
import org.wso2.carbon.uuf.spi.auth.SessionManager;
import org.wso2.carbon.uuf.spi.model.Model;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static org.wso2.carbon.uuf.spi.HttpResponse.STATUS_INTERNAL_SERVER_ERROR;

public class App {

    private static final Logger LOGGER = LoggerFactory.getLogger(App.class);

    private final String name;
    private final String contextPath;
    private final Lookup lookup;
    private final Map<String, Component> components;
    private final Component rootComponent;
    private final Map<String, Theme> themes;
    private final Theme defaultTheme;
    private final SessionManager sessionManager;
    private final Authorizer authorizer;
    private final Configuration configuration;

    public App(String name, String contextPath, Set<Component> components, Set<Theme> themes,
            Configuration configuration, Bindings bindings, I18nResources i18nResources,
            SessionManager sessionManager, Authorizer authorizer) {
        this.name = name;
        this.contextPath = contextPath;

        this.components = components.stream().collect(Collectors.toMap(Component::getContextPath, cmp -> cmp));
        this.rootComponent = this.components.get(Component.ROOT_COMPONENT_CONTEXT_PATH);

        this.themes = themes.stream().collect(Collectors.toMap(Theme::getName, theme -> theme));
        this.defaultTheme = configuration.getThemeName().map(configuredThemeName -> {
            Theme configuredTheme = App.this.themes.get(configuredThemeName);
            if (configuredTheme == null) {
                throw new IllegalArgumentException(
                        "Theme '" + configuredThemeName + "' which is configured for app '" + name
                                + "' does not exists. Available themes: " + themes);
            }
            return configuredTheme;
        }).orElse(null);

        this.lookup = new Lookup(components, configuration, bindings, i18nResources);
        this.configuration = configuration;
        this.sessionManager = sessionManager;
        this.authorizer = authorizer;
    }

    public String getName() {
        return name;
    }

    public String getContextPath() {
        return contextPath;
    }

    public Map<String, Component> getComponents() {
        return components;
    }

    public Map<String, Fragment> getFragments() {
        return lookup.getAllFragments();
    }

    public Map<String, Layout> getLayouts() {
        return lookup.getAllLayouts();
    }

    public Map<String, Theme> getThemes() {
        return themes;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * Renders the relevant page for the given request.
     *
     * @param request  HTTP request
     * @param response HTTP response
     * @return HTML from page rendering
     * @throws PageRedirectException if a redirection for another page/URL is needed
     * @throws HttpErrorException    if some other HTTP error occurred
     */
    public String renderPage(HttpRequest request, HttpResponse response) {
        RequestLookup requestLookup = createRequestLookup(request, response);
        API api = new API(sessionManager, authorizer, requestLookup);
        Theme theme = getRenderingTheme(api);
        try {
            return renderPageUri(request.getUriWithoutContextPath(), null, requestLookup, api, theme);
        } catch (SessionNotFoundException e) {
            String loginPageUri = configuration.getLoginPageUri().orElseThrow(() -> e);
            // Redirect to the login page.
            throw new PageRedirectException(requestLookup.getContextPath() + loginPageUri, e);
        } catch (PageRedirectException e) {
            throw e;
        } catch (PageNotFoundException e) {
            // See https://googlewebmastercentral.blogspot.com/2010/04/to-slash-or-not-to-slash.html
            // If the tailing '/' is extra or a it is missing, then send 301 with corrected URL.
            String uriWithoutContextPath = request.getUriWithoutContextPath();
            String correctedUriWithoutContextPath = uriWithoutContextPath.endsWith("/")
                    ? uriWithoutContextPath.substring(0, uriWithoutContextPath.length() - 1)
                    : (uriWithoutContextPath + "/");
            if (hasPage(correctedUriWithoutContextPath)) {
                if (request.isGetRequest()) {
                    // Redirecting to the correct page.
                    String correctedUri = request.getContextPath() + correctedUriWithoutContextPath;
                    if (request.getQueryString() != null) {
                        correctedUri = correctedUri + '?' + request.getQueryString();
                    }
                    throw new PageRedirectException(correctedUri, e);
                } else {
                    // If GET, we correct, since this can be an end-user error. But if POST it's the responsibility of
                    // the dev to use correct URL. Because HTTP POST redirect is not well supported.
                    // See : https://softwareengineering.stackexchange.com/q/99894
                    String message = e.getMessage() + " Retry with correct URI ending "
                            + correctedUriWithoutContextPath;
                    return renderErrorPage(new PageNotFoundException(message, e), request, response, theme);
                }
            } else {
                return renderErrorPage(e, request, response, theme);
            }
        } catch (HttpErrorException e) {
            return renderErrorPage(e, request, response, theme);
        } catch (PluginExecutionException e) {
            LOGGER.error("An error occurred while executing a plugin.", e);
            return renderErrorPage(new HttpErrorException(STATUS_INTERNAL_SERVER_ERROR, e.getMessage(), e), request,
                    response, theme);
        } catch (RenderingException e) {
            LOGGER.error("An error occurred while rendering page for request '{}'.", request, e);
            String message = (e.getCause() != null) ? ExceptionUtils.getRootCause(e).getMessage() : e.getMessage();
            return renderErrorPage(new HttpErrorException(STATUS_INTERNAL_SERVER_ERROR, message, e), request,
                    response, theme);
        }
    }

    private String renderErrorPage(HttpErrorException e, HttpRequest request, HttpResponse response, Theme theme) {
        String errorPageUri = configuration.getErrorPageUri(e.getHttpStatusCode())
                .orElse(configuration.getDefaultErrorPageUri().orElseThrow(() -> e));

        // Create Model with HTTP status code and error message.
        Map<String, Object> modelMap = new HashMap<>(2);
        modelMap.put("status", e.getHttpStatusCode());
        modelMap.put("message", e.getMessage());
        MapModel model = new MapModel(modelMap);

        RequestLookup requestLookup = createRequestLookup(request, response);
        API api = new API(sessionManager, authorizer, requestLookup);

        return renderPageUri(errorPageUri, model, requestLookup, api, theme);
    }

    private String renderPageUri(String pageUri, Model model, RequestLookup requestLookup, API api, Theme theme) {
        // If theme exists, add theme values to the requestLookup
        if (theme != null) {
            theme.addPlaceHolderValues(requestLookup);
        }
        // First try to addPlaceHolderValues the page with 'root' component.
        Optional<String> output = rootComponent.renderPage(pageUri, model, lookup, requestLookup, api);
        if (output.isPresent()) {
            return output.get();
        }

        // Since 'root' component doesn't have the page, try with other components.
        int secondSlashIndex = pageUri.indexOf('/', 1);
        if (secondSlashIndex == -1) {
            // No component context found in the 'pageUri' URI.
            throw new PageNotFoundException("Requested page '" + pageUri + "' does not exists.");
        }
        String componentContext = pageUri.substring(0, secondSlashIndex);
        Component component = components.get(componentContext);
        if (component == null) {
            // No component found for the 'componentContext' key.
            throw new PageNotFoundException("Requested page '" + pageUri + "' does not exists.");
        }
        String uriWithoutComponentContext = pageUri.substring(secondSlashIndex);
        output = component.renderPage(uriWithoutComponentContext, model, lookup, requestLookup, api);
        if (output.isPresent()) {
            return output.get();
        }
        // No page found for 'uriWithoutComponentContext' in the 'component'.
        throw new PageNotFoundException("Requested page '" + pageUri + "' does not exists.");
    }

    /**
     * @param request  HTTP request
     * @param response HTTP response
     * @return rendered HTML,CSS and JS outputs as JSON
     */
    public JsonObject renderFragment(HttpRequest request, HttpResponse response) {
        String uriWithoutContextPath = request.getUriWithoutContextPath();
        String uriPart = uriWithoutContextPath.substring(UriUtils.FRAGMENTS_URI_PREFIX.length());
        String fragmentName = NameUtils.getFullyQualifiedName(rootComponent.getName(), uriPart);
        // When building the dependency tree, all fragments are accumulated into the rootComponent.
        Fragment fragment = lookup.getAllFragments().get(fragmentName);
        if (fragment == null) {
            throw new FragmentNotFoundException("Requested fragment '" + fragmentName + "' does not exists.");
        }

        Model model = new MapModel(request.getFormParams());
        RequestLookup requestLookup = createRequestLookup(request, response);
        API api = new API(sessionManager, authorizer, requestLookup);

        JsonObject output = new JsonObject();
        output.addProperty("html", fragment.render(model, lookup, requestLookup, api));
        output.addProperty(Placeholder.headJs.name(),
                requestLookup.getPlaceholderContent(Placeholder.headJs).orElse(null));
        output.addProperty(Placeholder.js.name(), requestLookup.getPlaceholderContent(Placeholder.js).orElse(null));
        output.addProperty(Placeholder.css.name(),
                requestLookup.getPlaceholderContent(Placeholder.css).orElse(null));
        return output;
    }

    private boolean hasPage(String uriWithoutContextPath) {
        // First check 'root' component has the page.
        if (rootComponent.hasPage(uriWithoutContextPath)) {
            return true;
        }

        // Since 'root' components doesn't have the page, search in other components.
        int secondSlashIndex = uriWithoutContextPath.indexOf('/', 1);
        if (secondSlashIndex == -1) {
            // No component context found in the 'uriWithoutContextPath' URI.
            return false;
        }
        String componentContext = uriWithoutContextPath.substring(0, secondSlashIndex);
        Component component = components.get(componentContext);
        if (component == null) {
            // No component found for the 'componentContext' key.
            return false;
        }
        String pageUri = uriWithoutContextPath.substring(secondSlashIndex);
        return component.hasPage(pageUri);
    }

    private Theme getRenderingTheme(API api) {
        Optional<String> sessionThemeName = api.getSession().map(Session::getThemeName);
        if (!sessionThemeName.isPresent()) {
            return defaultTheme;
        }
        return sessionThemeName.map(themes::get)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Theme '" + sessionThemeName.get() + "' which is set as for the current session of app '"
                                + name + "' does not exists. Available themes are " + themes.keySet() + "."));
    }

    private RequestLookup createRequestLookup(HttpRequest request, HttpResponse response) {
        return new RequestLookup((configuration.getContextPath().orElse(null)), request, response);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, contextPath);
    }

    @Override
    public boolean equals(Object obj) {
        return (obj != null) && (obj instanceof App) && (this.name.equals(((App) obj).name));
    }

    @Override
    public String toString() {
        return "{\"name\": \"" + name + "\", \"context\": \"" + contextPath + "\"}";
    }
}