minium.web.internal.drivers.DefaultJavascriptInvoker.java Source code

Java tutorial

Introduction

Here is the source code for minium.web.internal.drivers.DefaultJavascriptInvoker.java

Source

/*
 * Copyright (C) 2015 The Minium Authors
 *
 * Licensed 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 minium.web.internal.drivers;

import static java.lang.String.format;

import java.io.IOException;
import java.util.Collection;
import java.util.List;

import minium.web.internal.ResourceException;
import minium.web.internal.compressor.Compressor;
import minium.web.internal.compressor.ResourceFunctions;

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriverException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;

/**
 * This class is responsible for injecting the necessary javascript code in the
 * current page so that jQuery expressions can be evaluated.
 *
 * @author Rui
 */
public class DefaultJavascriptInvoker implements JavascriptInvoker {

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

    private static final String CLOSURE_COMPRESSOR_CLASSNAME = "minium.web.internal.compressor.ClosureCompressor";
    private static final String CLOSURE_COMPILER_CLASSNAME = "com.google.javascript.jscomp.Compiler";

    private static final String INTERNAL_TEMPLATES_PATH = "minium/web/internal/templates/";

    private static final String FULL_TEMPLATE_PATH = INTERNAL_TEMPLATES_PATH + "jquery-invoker-full.template";
    private static final String LIGHT_TEMPLATE_PATH = INTERNAL_TEMPLATES_PATH + "jquery-invoker-light.template";

    private static Compressor compressor;

    static {
        try {
            Class.forName(CLOSURE_COMPILER_CLASSNAME, false, DefaultJavascriptInvoker.class.getClassLoader());
            compressor = (Compressor) Class.forName(CLOSURE_COMPRESSOR_CLASSNAME).newInstance();
        } catch (Exception e) {
            LOGGER.info("Google Closure Compiler not found in classpath, javascript will not be compressed");
            compressor = Compressor.NULL;
        }
    }

    enum ResponseType {
        MINIUM_UNDEFINED("minium-undefined"), NULL("null"), JSON("json"), ARRAY("array"), NUMBER("number"), STRING(
                "string"), BOOLEAN("boolean");

        private String typeStr;

        private ResponseType(String typeStr) {
            this.typeStr = typeStr;
        }

        public static ResponseType of(String typeStr) {
            for (ResponseType type : ResponseType.values()) {
                if (Objects.equal(type.typeStr, typeStr))
                    return type;
            }
            throw new IllegalArgumentException(String.format("Type %s is not valid", typeStr));
        }

        @Override
        public String toString() {
            return typeStr;
        }
    }

    // resource paths
    private final Collection<String> jsResources;
    private final Collection<String> cssResources;

    // concatenated styles resources content
    private final String styles;

    private final ClassLoader classLoader;
    private final String lightInvokerScriptTemplate;
    private final String fullInvokerScriptTemplate;

    public DefaultJavascriptInvoker(ClassLoader classLoader, Collection<String> jsResources,
            Collection<String> cssResources) {
        this.classLoader = classLoader;
        this.jsResources = jsResources;
        this.cssResources = cssResources;

        try {
            LOGGER.debug("DefaultJavascriptInvoker initialized with:");
            LOGGER.debug("  jsResources  : {}", jsResources);
            LOGGER.debug("  cssResources : {}", cssResources);

            String jsScripts = compressor.compress(DefaultJavascriptInvoker.class.getClassLoader(), jsResources);

            styles = cssResources != null ? combineResources(cssResources) : null;

            lightInvokerScriptTemplate = getFileContent(LIGHT_TEMPLATE_PATH);
            fullInvokerScriptTemplate = getFileContent(FULL_TEMPLATE_PATH).replace("{{jsScript}}", jsScripts);
        } catch (IOException e) {
            throw new ResourceException(e);
        }
    }

    /* (non-Javadoc)
     * @see minium.web.internal.drivers.Foo#invoke(org.openqa.selenium.JavascriptExecutor, java.lang.String, java.lang.Object)
     */
    @Override
    public <T> T invoke(JavascriptExecutor wd, String expression, Object... args) {
        return this.<T>doInvoke(wd, false, expression, args);
    }

    /* (non-Javadoc)
     * @see minium.web.internal.drivers.Foo#invokeAsync(org.openqa.selenium.JavascriptExecutor, java.lang.String, java.lang.Object)
     */
    @Override
    public <T> T invokeAsync(JavascriptExecutor wd, String expression, Object... args) {
        return this.<T>doInvoke(wd, true, expression, args);
    }

    @Override
    public <T> T invokeExpression(JavascriptExecutor executor, String expression, Object... args) {
        return this.<T>invoke(executor, format("return %s;", expression), args);
    }

    @Override
    public <T> T invokeExpressionAsync(JavascriptExecutor executor, String expression, Object... args) {
        return this.<T>invokeAsync(executor, format("return %s;", expression), args);
    }

    @SuppressWarnings("unchecked")
    protected <T> T doInvoke(JavascriptExecutor wd, boolean async, String expression, Object... args) {
        try {
            Object[] fullArgs = createLightInvokerScriptArgs(async, args);

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("About to invoke light invoker:");
                LOGGER.trace("async: {}", async);
                LOGGER.trace("expression: {}", expression);
                LOGGER.trace("fullArgs: {}", fullArgs);
            }

            Object result = async ? wd.executeAsyncScript(lightInvokerScript(expression), fullArgs)
                    : wd.executeScript(lightInvokerScript(expression), fullArgs);

            LOGGER.trace("result: {}", result);

            List<?> response = getValidResponse(result);
            ResponseType type = ResponseType.of((String) response.get(0));

            if (type == ResponseType.MINIUM_UNDEFINED) {
                // minium is not defined yet, we need to send all the necessary javascript
                fullArgs = createFullInvokerScriptArgs(async, args);

                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("About to invoke full invoker:");
                    LOGGER.trace("fullArgs: {}", fullArgs);
                }

                result = async ? wd.executeAsyncScript(fullInvokerScript(expression), fullArgs)
                        : wd.executeScript(fullInvokerScript(expression), fullArgs);

                LOGGER.trace("result: {}", result);

                response = getValidResponse(result);
                type = ResponseType.of((String) response.get(0));
            }

            return (T) extractValue(type, response);

        } catch (WebDriverException e) {
            throw new JavascriptInvocationFailedException(format("Failed invoking expression:\n", expression), e);
        }
    }

    private Object extractValue(ResponseType type, List<?> response) {
        switch (type) {
        case MINIUM_UNDEFINED:
            throw new IllegalStateException("Should not be here...");
        case NULL:
            return null;
        case ARRAY:
            return response.subList(1, response.size());
        default:
            return response.get(1);
        }
    }

    protected List<?> getValidResponse(Object result) {
        Preconditions.checkState(result instanceof List, "Expected a list as response but got %s", result);
        List<?> response = (List<?>) result;
        Preconditions.checkState(!response.isEmpty(), "Expected response list not to be empty");
        Object type = response.get(0);
        Preconditions.checkState(type instanceof String,
                "Expected a string value in the first position of the list but got %s", result);

        return response;
    }

    protected Object[] createLightInvokerScriptArgs(boolean async, Object... args) {
        Object[] fullArgs = args == null ? new Object[1] : new Object[args.length + 1];
        fullArgs[0] = async;
        if (args != null)
            System.arraycopy(args, 0, fullArgs, 1, args.length);
        return fullArgs;
    }

    protected Object[] createFullInvokerScriptArgs(boolean async, Object... args) {
        Object[] fullArgs;
        fullArgs = args == null ? new Object[2] : new Object[args.length + 2];
        fullArgs[0] = async;
        fullArgs[1] = styles;
        if (args != null)
            System.arraycopy(args, 0, fullArgs, 2, args.length);
        return fullArgs;
    }

    protected String lightInvokerScript(String expression) {
        return lightInvokerScriptTemplate.replace("{{expression}}", expression);
    }

    protected String fullInvokerScript(String expression) {
        return fullInvokerScriptTemplate.replace("{{expression}}", expression);
    }

    protected Collection<String> getJsResources() {
        return jsResources;
    }

    protected Collection<String> getCssResources() {
        return cssResources;
    }

    protected String getFileContent(String filePath) {
        return ResourceFunctions.classpathFileToStringFunction(classLoader).apply(filePath);
    }

    protected String combineResources(Collection<String> resources) {
        return Joiner.on("\n\n").join(
                Collections2.transform(resources, ResourceFunctions.classpathFileToStringFunction(classLoader)));
    }
}