WebjetComponentParser.java

package sk.iway.iwcm.system.spring.webjet_component;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.time.StopWatch;
import org.apache.struts.util.ResponseUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

import sk.iway.iwcm.Adminlog;
import sk.iway.iwcm.Cache;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.PageLng;
import sk.iway.iwcm.PageParams;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.components.WebjetComponentInterface;
import sk.iway.iwcm.doc.DebugTimer;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.stat.BrowserDetector;
import sk.iway.iwcm.system.monitoring.ExecutionTimeMonitor;
import sk.iway.iwcm.system.monitoring.MemoryMeasurement;
import sk.iway.iwcm.system.spring.WebjetComponentParserInterface;
import sk.iway.iwcm.tags.WriteTag;
import sk.iway.iwcm.users.UserDetails;
import sk.iway.iwcm.users.UsersDB;

/**
 * trieda pre parsovanie komponent z html kodu (doc_data) a nahradzanie za vygenerovany content
 */
@Component
@RequestScope
public class WebjetComponentParser implements WebjetComponentParserInterface {
    @Autowired
    ApplicationContext context;

    // vykonava komponentu cez prisluchajucu triedu
    @Autowired
    WebjetComponentResolver componentResolver;

    private static final String INCLUDE_START = "!INCLUDE(";
	private static final String INCLUDE_END = ")!";

    /**
     * Metód parse slúži na parsovanie a nahradzovanie !INCLUDE()! v Stringu za vygenerovaný kód
     * @param request
     * @param html
     * @return
     */
    public String parse(HttpServletRequest request, HttpServletResponse response, String html) {
        Map<String, WebjetComponentInterface> components = new LinkedHashMap<>();

        if (Tools.isEmpty(html)) return html;

        if (html.contains("!INCLUDE")==false) return html;

        try {
            // parsuje komponenty a ich page params z html kodu
            parseComponentsAndPageParams(html, components);

            DebugTimer dt = new DebugTimer("WriteTag");
            // renderuje html kod z tried pre dane komponenty a nahradza v html kode include za html
            html = renderComponents(request, response, html, components, dt);
        }
        catch (Exception ex) {
            html = getErrorMessage(request, ex, html);
            sk.iway.iwcm.Logger.error(ex);
        }

        return html;
    }

    /**
     * Metóda run slúži na parsovanie a nahradzovanie !INCLUDE()! v pevne definovaných request atributoch
     * "doc_data", "doc_header","doc_footer", "doc_menu", "doc_right_menu", "template_object_a", "template_object_b", "template_object_c", "template_object_d"
     * @param request
     * @param response
     */
    public void run(HttpServletRequest request, HttpServletResponse response) {

        String[] attributes = {
                "doc_data",
                "doc_header",
                "doc_footer",
                "doc_menu",
                "doc_right_menu",
                "template_object_a",
                "template_object_b",
                "template_object_c",
                "template_object_d"
        };

        // nastavenie locale pre message source, aby sa pri validaciach pouzili spravne texty zo sablony
        LocaleContextHolder.setLocale(request.getLocale());

        for (String attribute : attributes) {

            // vybratie html kodu z requestu
            String html = getHtmlFromRequestAttribute(request, attribute);

            html = parse(request, response, html);

            // nastavenie html kodu spat do requestu
            setHtmlToRequestAttribute(request, attribute, html);
        }
    }

    /**
     * Metóda parseComponentsAndPageParams slúži na parsovanie komponent tried a pageParams z !INCLUDE()!
     * @param html
     * @throws BeansException
     */
    private void parseComponentsAndPageParams(String html, Map<String, WebjetComponentInterface> components) throws BeansException {
        // pattern od !INCLUDE do )!
        Pattern pattern = Pattern.compile("!INCLUDE\\((.*?)\\)!");
        Matcher matcher = pattern.matcher(html);

        while (matcher.find()) {
            String group = matcher.group();

            if (group.startsWith("!INCLUDE(/")) {
                //it's JSP component
                continue;
            }

            // unescape pretoze html kod moze obsahovat &quote;
            String unescapedGroup = StringEscapeUtils.unescapeHtml(group);

            if (components.containsKey(group)) {
                continue;
            }

            WebjetComponentInterface component = parseComponentClass(unescapedGroup);
            if (component != null) {
                components.put(group, component);
            }
        }
    }

    /**
     * Metóda parseComponentClass slúži na parsovanie triedy komponenty z !INCLUDE()!
     * @param include
     * @return
     * @throws BeansException
     */
    private WebjetComponentInterface parseComponentClass(String include) throws BeansException {
        WebjetComponentInterface result = null;
        String componentClass = getComponentClass(include);
        if (componentClass != null) {
            // vyhladanie beanu s originalnym nazvom
            if (context.containsBean(componentClass)) {
                result = context.getBean(componentClass, WebjetComponentInterface.class);
            }

            // vyhladanie beanu s nazvom s prvym pismenom malym
            if (result == null && context.containsBean(firstToLower(componentClass))) {
                result = context.getBean(firstToLower(componentClass), WebjetComponentInterface.class);
            }
        }

        return result;
    }

    /**
     * Parse className from !INCLUDE()! tag
     * @param include
     * @return
     */
    private String getComponentClass(String include) {
        int start = include.indexOf("(");
        int end = include.indexOf(",");
        if (end == -1) end = include.indexOf(")");

        if (end > start && end>0) {
            // nazov triedy komponenty
            String className = include.substring(start+1, end).trim();
            return className;
        }
        return null;
    }

    private String redirectComponent = null;

    /**
     * Metóda renderComponents slúži na renderovanie komponent
     * @param html
     * @return
     * @throws Exception
     */
    private String renderComponents(HttpServletRequest request, HttpServletResponse response, String html, Map<String, WebjetComponentInterface> components, DebugTimer dt) throws Exception {
        if (components.isEmpty()) {
            return html;
        }

        boolean writePerfStat = "true".equals(request.getParameter("_writePerfStat"));

        for (Map.Entry<String, WebjetComponentInterface> entry : components.entrySet()) {
            String key = entry.getKey();
            WebjetComponentInterface v = entry.getValue();

            PageParams pageParams;
            int i = key.indexOf(",");
            if (i>0) pageParams = new PageParams(key.substring(i));
            else pageParams = new PageParams("");

            /** Check if this app can be rendered in current device type (onlz if we checking device) **/
            boolean render = canRenderForDevice(pageParams, request);

            if(render) {
                /*** CACHE logic ***/
                String rendered = null;

                Cache cache = Cache.getInstance();
                //prepare cache key
                String cacheKey = getCacheKey(key, request);
                int cacheMinutes = 0;
                boolean servedFromCache = false;

                StopWatch executionTimeStopWatch = new StopWatch();
                executionTimeStopWatch.start();
                MemoryMeasurement memoryConsumed = new MemoryMeasurement();

                //Is cache permitted ?
                if (isCacheEnabled(request, key)) {
                    //Get cache time param
                    cacheMinutes = pageParams.getIntValue("cacheMinutes", -1);

                    if(cacheMinutes > 0) {
                        String cachedHtml = (String) cache.getObject(cacheKey);

                        //If html code is in cache, use it
                        if(Tools.isNotEmpty(cachedHtml)) {
                            rendered = cachedHtml;
                            servedFromCache = true;
                        }
                    }
                }

                //If cache logic wasn't executed, render html code
                if(rendered == null) {
                    // render html kodu z triedy
                    rendered = componentResolver.render(request, response, v, pageParams);

                    // ak navratova hodnota obsahuje redirect
                    if (isRedirected(response)) {
                        // nastavime nazov triedy pre vypis
                        redirectComponent = v.getClass().getSimpleName();
                        return "";
                    }

                    if (cacheMinutes > 0 && cacheKey != null) {
                        //Save new rendered html code to cache
                        cache.setObjectSeconds(cacheKey, rendered, cacheMinutes * 60, true);
                    }
                }

                if (rendered != null && html.contains(key)) {
                    if (writePerfStat && key.length()>1) {
                        long diff = dt.getDiff();
                        long lastDiff = dt.getLastDiff();
                        String logText = "\nPerfStat: " + diff + " ms (+"+lastDiff+") " + key.substring(1) +"\n";
                        rendered = rendered + logText;
                        Logger.debug(WriteTag.class, logText);
                    }
                    executionTimeStopWatch.stop();
                    if (servedFromCache) ExecutionTimeMonitor.recordComponentExecutionFromCache(cacheKey, executionTimeStopWatch.getTime());
					else ExecutionTimeMonitor.recordComponentExecution(cacheKey, executionTimeStopWatch.getTime(), memoryConsumed.diff());
                    html = Tools.replace(html, key, rendered);
                }
            } else {
                //do not render component for current device type, remove it from html
                html = Tools.replace(html, key, "");
            }
        }

        return html;
    }

    /**
     * Chech if this app can be rendered in current device type
     * @param pageParams
     * @param request
     * @return
     */
    private boolean canRenderForDevice(PageParams pageParams, HttpServletRequest request) {

        //We are checking device type only if we are not in preview mode (preview mode showing apps for all devices types)
        if (request.getAttribute("inPreviewMode") != null) return true;

        String devices = pageParams.getValue("device", "");
        if (Tools.isEmpty(devices)) return true;

        String[] devicesArr = Tools.getTokens(devices, "+", true);

        BrowserDetector browser = BrowserDetector.getInstance(request);
        for (String device : devicesArr) {
            if("pc".equalsIgnoreCase(device) && browser.isDesktop()) {
                return true;
            }
            else if("tablet".equalsIgnoreCase(device) && browser.isTablet()) {
                return true;
            }
            else if("phone".equalsIgnoreCase(device) && browser.isPhone()) {
                return true;
            } else if (device.equalsIgnoreCase(browser.getBrowserDeviceType())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Test if result of component can be cached:
     * - if user is admin, cache is disabled
     * - if parameter _disableCache=true then cache is disabled
     * - if component has parameter page other than 1 then cache is disabled
     * @param request
     * @param includeText
     * @return
     */
    private boolean isCacheEnabled(HttpServletRequest request, String includeText) {
        if ("true".equals(request.getParameter("_disableCache"))) return false;

        Identity user = getUser(request);
        if (user!=null && user.isAdmin() && Constants.getBoolean("cacheStaticContentForAdmin")==false) return false;

        //news komponenta nemoze cachovat ak ma parameter page
        if (request.getParameter("page")!=null && "1".equals(request.getParameter("page"))==false)
        {
            return false;
        }

        return true;
    }

    /**
     * Prepare chache key for html code (rendered code)
     * @param key
     * @param request
     * @return
     */
    private String getCacheKey(String key, HttpServletRequest request) {
        //Prepare cache key
        StringBuilder cacheKeySB = new StringBuilder( key );
        String cacheKey = "";
        int startIndex = cacheKeySB.indexOf(INCLUDE_START);
        int includeEndIndex;
        int failsafe = 0;

        while (startIndex != -1 && failsafe < 100) {
            failsafe++;
            includeEndIndex = cacheKeySB.indexOf(INCLUDE_END, startIndex);

            if (includeEndIndex < 0) {
                //nenasiel sa koniec
                cacheKeySB.delete(0,startIndex+INCLUDE_START.length());
                startIndex = cacheKeySB.indexOf(INCLUDE_START);
                continue;
            }

            cacheKey = "writeTag_" + cacheKeySB.substring(startIndex, includeEndIndex);
			cacheKey = Tools.replace(cacheKey, "!DOC_ID!", request.getParameter("docid")) + " ;" + PageLng.getUserLng(request);
        }

        return cacheKey;
    }

    /**
     * Helper pre upravu stringu, prve pismenu na lowercase
     * @param str
     * @return
     */
    private String firstToLower(String str) {
        return Character.toLowerCase(str.charAt(0)) + str.substring(1);
    }

    private String getHtmlFromRequestAttribute(HttpServletRequest request, String attribute) {
        if (attribute == null || request == null) return null;
        return (String) request.getAttribute(attribute);
    }

    private void setHtmlToRequestAttribute(HttpServletRequest request, String attribute, String html) {
        request.setAttribute(attribute, html);
    }

    /**
     * @return boolean či response obsahuje presmerovanie
     */
    public boolean isRedirected(HttpServletResponse response) {
        return getRedirectLocation(response) != null;
    }

    /**
     * @return String hodnotu presmerovania alebo null
     */
    public String getRedirectLocation(HttpServletResponse response) {
        return response != null ? response.getHeader("Location") : null;
    }

    /**
     * @return komponenta, ktorej hodnota vracia presmerovanie
     */
    public String getRedirectComponent() {
        return redirectComponent;
    }

    private String getErrorMessage(HttpServletRequest request, Exception ex, String include) {
        Logger.debug(WebjetComponentParser.class,"INCLUDE ERROR: " + ex.getMessage());

        StringBuilder content = new StringBuilder();
        Prop prop = Prop.getInstance();
        content.append(WriteTag.getErrorMessage(prop, "writetag.error", getComponentClass(include)));

        UserDetails user = getUser(request);
        if (user != null && user.isAdmin() && request.getAttribute("writeTagDontShowError") == null)
        {
            StringWriter sw = new StringWriter();
            ex.printStackTrace(new PrintWriter(sw));
            String stack = sw.toString();

            content.append("<div class=\"component-error\" style='border:2px solid red; background-color: white; color: black; margin: 5px; white-space: pre;'>" + ResponseUtils.filter(ex.getMessage()) + "<br>");
            String stackTrace = ResponseUtils.filter(stack);
            content.append(stackTrace + "</div>");
        }

        Adminlog.add(Adminlog.TYPE_JSPERROR, "ERROR: " + include + "\n\n" + ex.getMessage(), -1, -1);

        return content.toString();
    }

    /**
     * @return Optional prihlásený používateľ
     */
    private Identity getUser(HttpServletRequest request) {
        return UsersDB.getCurrentUser(request);
    }
}