org.openqa.selenium.remote.NewSessionPayload.java Source code

Java tutorial

Introduction

Here is the source code for org.openqa.selenium.remote.NewSessionPayload.java

Source

// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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.openqa.selenium.remote;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.CapabilityType.PLATFORM;
import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.io.CharSource;
import com.google.common.io.CharStreams;
import com.google.common.io.FileBackedOutputStream;
import com.google.gson.reflect.TypeToken;

import org.openqa.selenium.Capabilities;
import org.openqa.selenium.ImmutableCapabilities;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.remote.session.CapabilitiesFilter;
import org.openqa.selenium.remote.session.CapabilityTransform;
import org.openqa.selenium.remote.session.ChromeFilter;
import org.openqa.selenium.remote.session.EdgeFilter;
import org.openqa.selenium.remote.session.FirefoxFilter;
import org.openqa.selenium.remote.session.InternetExplorerFilter;
import org.openqa.selenium.remote.session.OperaFilter;
import org.openqa.selenium.remote.session.ProxyTransform;
import org.openqa.selenium.remote.session.SafariFilter;
import org.openqa.selenium.remote.session.StripAnyPlatform;
import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class NewSessionPayload implements Closeable {

    private static final Logger LOG = Logger.getLogger(NewSessionPayload.class.getName());

    private final Set<CapabilitiesFilter> adapters;
    private final Set<CapabilityTransform> transforms;

    private static final Dialect DEFAULT_DIALECT = Dialect.OSS;
    private final static Predicate<String> ACCEPTED_W3C_PATTERNS = Stream
            .of("^[\\w-]+:.*$", "^acceptInsecureCerts$", "^browserName$", "^browserVersion$", "^platformName$",
                    "^pageLoadStrategy$", "^proxy$", "^setWindowRect$", "^timeouts$", "^unhandledPromptBehavior$")
            .map(Pattern::compile).map(Pattern::asPredicate).reduce(identity -> false, Predicate::or);

    private final Json json = new Json();
    private final FileBackedOutputStream backingStore;
    private final ImmutableSet<Dialect> dialects;

    public static NewSessionPayload create(Capabilities caps) throws IOException {
        // We need to convert the capabilities into a new session payload. At this point we're dealing
        // with references, so I'm Just Sure This Will Be Fine.
        return create(ImmutableMap.of("desiredCapabilities", caps.asMap()));
    }

    public static NewSessionPayload create(Map<String, ?> source) throws IOException {
        Objects.requireNonNull(source, "Payload must be set");

        String json = new Json().toJson(source);
        return new NewSessionPayload(new StringReader(json));
    }

    public static NewSessionPayload create(Reader source) throws IOException {
        return new NewSessionPayload(source);
    }

    private NewSessionPayload(Reader source) throws IOException {
        // Dedicate up to 10% of all RAM or 20% of available RAM (whichever is smaller) to storing this
        // payload.
        int threshold = (int) Math.min(Integer.MAX_VALUE,
                Math.min(Runtime.getRuntime().freeMemory() / 5, Runtime.getRuntime().maxMemory() / 10));

        backingStore = new FileBackedOutputStream(threshold);
        try (Writer writer = new OutputStreamWriter(backingStore, UTF_8)) {
            CharStreams.copy(source, writer);
        }

        ImmutableSet.Builder<CapabilitiesFilter> adapters = ImmutableSet.builder();
        ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add);
        adapters.add(new ChromeFilter()).add(new EdgeFilter()).add(new FirefoxFilter())
                .add(new InternetExplorerFilter()).add(new OperaFilter()).add(new SafariFilter());
        this.adapters = adapters.build();

        ImmutableSet.Builder<CapabilityTransform> transforms = ImmutableSet.builder();
        ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add);
        transforms.add(new ProxyTransform()).add(new StripAnyPlatform()).add(new W3CPlatformNameNormaliser());
        this.transforms = transforms.build();

        ImmutableSet.Builder<Dialect> dialects = ImmutableSet.builder();
        if (getOss() != null) {
            dialects.add(Dialect.OSS);
        }
        if (getAlwaysMatch() != null || getFirstMatches() != null) {
            dialects.add(Dialect.W3C);
        }
        this.dialects = dialects.build();

        validate();
    }

    private void validate() throws IOException {
        Map<String, Object> alwaysMatch = getAlwaysMatch();
        if (alwaysMatch == null) {
            alwaysMatch = ImmutableMap.of();
        }
        Map<String, Object> always = alwaysMatch;
        Collection<Map<String, Object>> firsts = getFirstMatches();
        if (firsts == null) {
            firsts = ImmutableList.of(ImmutableMap.of());
        }

        if (firsts.isEmpty()) {
            throw new IllegalArgumentException("First match w3c capabilities is zero length");
        }

        firsts.stream().peek(map -> {
            Set<String> overlap = Sets.intersection(always.keySet(), map.keySet());
            if (!overlap.isEmpty()) {
                throw new IllegalArgumentException(
                        "Overlapping keys between w3c always and first match capabilities: " + overlap);
            }
        }).map(first -> {
            Map<String, Object> toReturn = new HashMap<>();
            toReturn.putAll(always);
            toReturn.putAll(first);
            return toReturn;
        }).peek(map -> {
            ImmutableSortedSet<String> nullKeys = map.entrySet().stream().filter(entry -> entry.getValue() == null)
                    .map(Map.Entry::getKey).collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
            if (!nullKeys.isEmpty()) {
                throw new IllegalArgumentException("Null values found in w3c capabilities. Keys are: " + nullKeys);
            }
        }).peek(map -> {
            ImmutableSortedSet<String> illegalKeys = map.entrySet().stream()
                    .filter(entry -> !ACCEPTED_W3C_PATTERNS.test(entry.getKey())).map(Map.Entry::getKey)
                    .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
            if (!illegalKeys.isEmpty()) {
                throw new IllegalArgumentException("Illegal key values seen in w3c capabilities: " + illegalKeys);
            }
        }).forEach(map -> {
        });
    }

    public void writeTo(Appendable appendable) throws IOException {
        try (JsonOutput json = new Json().newOutput(appendable)) {
            json.beginObject();

            Map<String, Object> first = getOss();
            if (first == null) {
                first = stream().findFirst().orElse(new ImmutableCapabilities()).asMap();
            }

            // Write the first capability we get as the desired capability.
            json.name("desiredCapabilities");
            json.write(first, MAP_TYPE);

            // And write the first capability for gecko13
            json.name("capabilities");
            json.beginObject();

            json.name("desiredCapabilities");
            json.write(first, MAP_TYPE);

            // Then write everything into the w3c payload. Because of the way we do this, it's easiest
            // to just populate the "firstMatch" section. The spec says it's fine to omit the
            // "alwaysMatch" field, so we do this.
            json.name("firstMatch");
            json.beginArray();
            //noinspection unchecked
            getW3C().forEach(map -> json.write(map, MAP_TYPE));
            json.endArray();

            json.endObject(); // Close "capabilities" object

            writeMetaData(json);

            json.endObject();
        }
    }

    private void writeMetaData(JsonOutput out) throws IOException {
        CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8);
        try (Reader reader = charSource.openBufferedStream(); JsonInput input = json.newInput(reader)) {
            input.beginObject();
            while (input.hasNext()) {
                String name = input.nextName();
                switch (name) {
                case "capabilities":
                case "desiredCapabilities":
                case "requiredCapabilities":
                    input.skipValue();
                    break;

                default:
                    out.name(name);
                    out.write(input.<Object>read(Object.class), Object.class);
                    break;
                }
            }
        }
    }

    private void streamW3CProtocolParameters(JsonOutput out, Map<String, Object> des) {
        // Technically we should be building up a combination of "alwaysMatch" and "firstMatch" options.
        // We're going to do a little processing to figure out what we might be able to do, and assume
        // that people don't really understand the difference between required and desired (which is
        // commonly the case). Wish us luck. Looking at the current implementations, people may have
        // set options for multiple browsers, in which case a compliant W3C remote end won't start
        // a session. If we find this, then we create multiple firstMatch capabilities. Furrfu.
        // The table of options are:
        //
        // Chrome: chromeOptions
        // Firefox: moz:.*, firefox_binary, firefox_profile, marionette
        // Edge: none given
        // IEDriver: ignoreZoomSetting, initialBrowserUrl, enableElementCacheCleanup,
        //   browserAttachTimeout, enablePersistentHover, requireWindowFocus, logFile, logLevel, host,
        //   extractPath, silent, ie.*
        // Opera: operaOptions
        // SafariDriver: safari.options
        //
        // We can't use the constants defined in the classes because it would introduce circular
        // dependencies between the remote library and the implementations. Yay!
    }

    /**
     * Stream the {@link Capabilities} encoded in the payload used to create this instance. The
     * {@link Stream} will start with a {@link Capabilities} object matching the OSS capabilities, and
     * will then expand each of the "{@code firstMatch}" and "{@code alwaysMatch}" contents as defined
     * in the W3C WebDriver spec.
     * <p>
     * The OSS {@link Capabilities} are listed first because converting the OSS capabilities to the
     * equivalent W3C capabilities isn't particularly easy, so it's hoped that this approach gives us
     * the most compatible implementation.
     */
    public Stream<ImmutableCapabilities> stream() throws IOException {
        // OSS first
        Stream<Map<String, Object>> oss = Stream.of(getOss());

        // And now W3C
        Stream<Map<String, Object>> w3c = getW3C();

        return Stream.concat(oss, w3c).filter(Objects::nonNull).map(this::applyTransforms).filter(Objects::nonNull)
                .distinct().map(ImmutableCapabilities::new);
    }

    public ImmutableSet<Dialect> getDownstreamDialects() {
        return dialects.isEmpty() ? ImmutableSet.of(DEFAULT_DIALECT) : dialects;
    }

    @Override
    public void close() throws IOException {
        backingStore.reset();
    }

    private Map<String, Object> getOss() throws IOException {
        CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8);
        try (Reader reader = charSource.openBufferedStream(); JsonInput input = json.newInput(reader)) {
            input.beginObject();
            while (input.hasNext()) {
                String name = input.nextName();
                if ("desiredCapabilities".equals(name)) {
                    return input.read(MAP_TYPE);
                } else {
                    input.skipValue();
                }
            }
        }
        return null;
    }

    private Stream<Map<String, Object>> getW3C() throws IOException {
        // If there's an OSS value, generate a stream of capabilities from that using the transforms,
        // then add magic to generate each of the w3c capabilities. For the sake of simplicity, we're
        // going to make the (probably wrong) assumption we can hold all of the firstMatch values and
        // alwaysMatch value in memory at the same time.
        Map<String, Object> oss = convertOssToW3C(getOss());
        Stream<Map<String, Object>> fromOss;
        if (oss != null) {
            Set<String> usedKeys = new HashSet<>();

            // Are there any values we care want to pull out into a mapping of their own?
            List<Map<String, Object>> firsts = adapters.stream().map(adapter -> adapter.apply(oss))
                    .filter(Objects::nonNull).filter(map -> !map.isEmpty())
                    .map(map -> map.entrySet().stream().filter(entry -> entry.getKey() != null)
                            .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey()))
                            .filter(entry -> entry.getValue() != null)
                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
                    .peek(map -> usedKeys.addAll(map.keySet())).collect(ImmutableList.toImmutableList());
            if (firsts.isEmpty()) {
                firsts = ImmutableList.of(ImmutableMap.of());
            }

            // Are there any remaining unused keys?
            Map<String, Object> always = oss.entrySet().stream().filter(entry -> !usedKeys.contains(entry.getKey()))
                    .filter(entry -> entry.getValue() != null)
                    .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));

            // Firsts contains at least one entry, always contains everything else. Let's combine them
            // into the stream to form a unified set of capabilities. Woohoo!
            fromOss = firsts.stream()
                    .map(first -> ImmutableMap.<String, Object>builder().putAll(always).putAll(first).build())
                    .map(this::applyTransforms)
                    .map(map -> map.entrySet().stream().filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey()))
                            .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)))
                    .map(map -> (Map<String, Object>) map);
        } else {
            fromOss = Stream.of();
        }

        Stream<Map<String, Object>> fromW3c = null;
        Map<String, Object> alwaysMatch = getAlwaysMatch();
        Collection<Map<String, Object>> firsts = getFirstMatches();

        if (alwaysMatch == null && firsts == null) {
            fromW3c = Stream.of(); // No W3C capabilities.
        } else {
            if (alwaysMatch == null) {
                alwaysMatch = ImmutableMap.of();
            }
            Map<String, Object> always = alwaysMatch; // Keep the comoiler happy.
            if (firsts == null) {
                firsts = ImmutableList.of(ImmutableMap.of());
            }

            fromW3c = firsts.stream()
                    .map(first -> ImmutableMap.<String, Object>builder().putAll(always).putAll(first).build());
        }

        return Stream.concat(fromOss, fromW3c).distinct();
    }

    private Map<String, Object> convertOssToW3C(Map<String, Object> capabilities) {
        if (capabilities == null) {
            return null;
        }

        Map<String, Object> toReturn = new TreeMap<>();
        toReturn.putAll(capabilities);

        // Platform name
        if (capabilities.containsKey(PLATFORM) && !capabilities.containsKey(PLATFORM_NAME)) {
            toReturn.put(PLATFORM_NAME, String.valueOf(capabilities.get(PLATFORM)));
        }

        return toReturn;
    }

    private Map<String, Object> getAlwaysMatch() throws IOException {
        CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8);
        try (Reader reader = charSource.openBufferedStream(); JsonInput input = json.newInput(reader)) {
            input.beginObject();
            while (input.hasNext()) {
                String name = input.nextName();
                if ("capabilities".equals(name)) {
                    input.beginObject();
                    while (input.hasNext()) {
                        name = input.nextName();
                        if ("alwaysMatch".equals(name)) {
                            return input.read(MAP_TYPE);
                        } else {
                            input.skipValue();
                        }
                    }
                    input.endObject();
                } else {
                    input.skipValue();
                }
            }
        }
        return null;
    }

    private Collection<Map<String, Object>> getFirstMatches() throws IOException {
        CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8);
        try (Reader reader = charSource.openBufferedStream(); JsonInput input = json.newInput(reader)) {
            input.beginObject();
            while (input.hasNext()) {
                String name = input.nextName();
                if ("capabilities".equals(name)) {
                    input.beginObject();
                    while (input.hasNext()) {
                        name = input.nextName();
                        if ("firstMatch".equals(name)) {
                            return input.read(new TypeToken<List<Map<String, Object>>>() {
                            }.getType());
                        } else {
                            input.skipValue();
                        }
                    }
                    input.endObject();
                } else {
                    input.skipValue();
                }
            }
        }
        return null;
    }

    private Map<String, Object> applyTransforms(Map<String, Object> caps) {
        Queue<Map.Entry<String, Object>> toExamine = new LinkedList<>();
        toExamine.addAll(caps.entrySet());
        Set<String> seenKeys = new HashSet<>();
        Map<String, Object> toReturn = new TreeMap<>();

        // Take each entry and apply the transforms
        while (!toExamine.isEmpty()) {
            Map.Entry<String, Object> entry = toExamine.remove();
            seenKeys.add(entry.getKey());

            if (entry.getValue() == null) {
                continue;
            }

            for (CapabilityTransform transform : transforms) {
                Collection<Map.Entry<String, Object>> result = transform.apply(entry);
                if (result == null) {
                    toReturn.remove(entry.getKey());
                    break;
                }

                for (Map.Entry<String, Object> newEntry : result) {
                    if (!seenKeys.contains(newEntry.getKey())) {
                        toExamine.add(newEntry);
                    } else {
                        if (newEntry.getKey().equals(entry.getKey())) {
                            entry = newEntry;
                        }
                        toReturn.put(newEntry.getKey(), newEntry.getValue());
                    }
                }
            }
        }
        return toReturn;
    }
}