com.ponysdk.driver.PonySDKWebDriver.java Source code

Java tutorial

Introduction

Here is the source code for com.ponysdk.driver.PonySDKWebDriver.java

Source

/*
 * Copyright (c) 2017 PonySDK
 *  Owners:
 *  Luciano Broussal  <luciano.broussal AT gmail.com>
 *  Mathieu Barbier   <mathieu.barbier AT gmail.com>
 *  Nicolas Ciaravola <nicolas.ciaravola.pro AT gmail.com>
 *
 *  WebSite:
 *  http://code.google.com/p/pony-sdk/
 *
 * 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 com.ponysdk.driver;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.ToIntFunction;

import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.websocket.CloseReason;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ponysdk.core.model.ArrayValueModel;
import com.ponysdk.core.model.BooleanModel;
import com.ponysdk.core.model.ClientToServerModel;
import com.ponysdk.core.model.ServerToClientModel;
import com.ponysdk.core.model.ValueTypeModel;
import com.ponysdk.core.model.WidgetType;

public class PonySDKWebDriver implements WebDriver {

    private final static Logger log = LoggerFactory.getLogger(PonySDKWebDriver.class);
    private final static ThreadLocal<byte[]> byteArrays = ThreadLocal.withInitial(() -> new byte[32]);
    private final ConcurrentHashMap<Integer, PonyWebElement> elements = new ConcurrentHashMap<>();
    private final PonySearchContext globalContext = new PonySearchContext(
            Collections.unmodifiableCollection(elements.values()), false);
    private final MessageHandler.Whole<ByteBuffer> messageHandler = this::onMessage;
    private final Map<String, String> cookies = new ConcurrentHashMap<>();
    private final WebsocketClient client;
    private final List<PonyFrame> messageInConstruction = new ArrayList<>();
    private final PonyMessageListener messageListener;
    private final PonyBandwithListener bandwithListener;
    private final boolean handleImplicitCommunication;

    /*
     * Use EnumMap instead of normal switch since, with ecj compiler, ServerToClientModel::values is called on every
     * switch invocation causing enormous garbage
     */
    private final EnumMap<ServerToClientModel, BiConsumer<List<PonyFrame>, PonyFrame>> onMessageSwitch = new EnumMap<>(
            ServerToClientModel.class);

    private volatile String typeHistory;
    private volatile String url;
    private volatile int contextId;

    private ByteBuffer buffer = ByteBuffer.allocate(4096);
    private int length = 0;

    public PonySDKWebDriver() {
        this(null, null, null, true);
    }

    public PonySDKWebDriver(final PonyMessageListener messageListener, final PonyBandwithListener bandwithListener,
            PonySessionListener sessionListener, final boolean handleImplicitCommunication) {
        super();
        this.handleImplicitCommunication = handleImplicitCommunication;
        this.messageListener = messageListener == null ? INDIFFERENT_MSG_LISTENER : messageListener;
        this.bandwithListener = bandwithListener == null ? INDIFFERENT_BANDWITH_LISTENER : bandwithListener;
        sessionListener = sessionListener == null ? INDIFFERENT_SESSION_LISTENER : sessionListener;
        this.client = new WebsocketClient(messageHandler, bandwithListener, sessionListener);
        onMessageSwitch.put(ServerToClientModel.CREATE_CONTEXT, (message, frame) -> {
            log.info("UI Context created with ID {}", contextId = (int) frame.value);
            if (handleImplicitCommunication)
                sendCookies();
        });
        onMessageSwitch.put(ServerToClientModel.HISTORY_FIRE_EVENTS, (message, frame) -> {
            if ((boolean) frame.getValue()) {
                final String typeHistory = (String) findValueForModel(message, ServerToClientModel.TYPE_HISTORY);
                if (typeHistory == null)
                    return;
                if (handleImplicitCommunication)
                    sendTypeHistory(this.typeHistory = typeHistory);
            }
        });
        onMessageSwitch.put(ServerToClientModel.TYPE_ADD, (message, frame) -> {
            final PonyWebElement element = elements.get(frame.value);
            if (element == null)
                return;
            final Object parentId = findValueForModel(message, ServerToClientModel.PARENT_OBJECT_ID);
            if (parentId == null)
                return;
            element.parent = elements.get(parentId);
            if (element.parent == null)
                return;
            final Integer index = (Integer) findValueForModel(message, ServerToClientModel.INDEX);
            if (index == null) {
                element.parent.children.add(element);
            } else {
                element.parent.children.add(index, element);
            }
        });
        onMessageSwitch.put(ServerToClientModel.TYPE_REMOVE, (message, frame) -> {
            final PonyWebElement element = elements.get(frame.value);
            if (element == null || element.parent == null)
                return;
            element.parent.children.remove(element);
            element.parent = null;
        });
        onMessageSwitch.put(ServerToClientModel.ADD_COOKIE, (message, frame) -> {
            final String value = (String) findValueForModel(message, ServerToClientModel.VALUE);
            if (value == null)
                return;
            cookies.put((String) frame.value, value);
        });
        onMessageSwitch.put(ServerToClientModel.REMOVE_COOKIE, (message, frame) -> {
            cookies.remove(frame.value);
        });
        onMessageSwitch.put(ServerToClientModel.TYPE_CREATE, (message, frame) -> {
            final Byte widget = (Byte) findValueForModel(message, ServerToClientModel.WIDGET_TYPE);
            if (widget == null)
                return;
            final int elementId = (int) frame.value;
            elements.put(elementId, new PonyWebElement(this, elementId, WidgetType.fromRawValue(widget)));
        });
        onMessageSwitch.put(ServerToClientModel.TYPE_GC, (message, frame) -> {
            elements.remove(frame.value);
        });
        onMessageSwitch.put(ServerToClientModel.PUT_ATTRIBUTE_KEY, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            final String key = (String) frame.value;
            final String value = (String) findValueForModel(message, ServerToClientModel.ATTRIBUTE_VALUE);
            if (value == null)
                return;
            element.attributes.put(key, value);
        });
        onMessageSwitch.put(ServerToClientModel.REMOVE_ATTRIBUTE_KEY, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.attributes.remove(frame.value);
        });
        onMessageSwitch.put(ServerToClientModel.STYLE_NAME, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.styles.clear();
            element.styles.addAll(Arrays.asList(((String) frame.value).split(" ")));
        });
        onMessageSwitch.put(ServerToClientModel.ADD_STYLE_NAME, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.styles.addAll(Arrays.asList(((String) frame.value).split(" ")));
        });
        onMessageSwitch.put(ServerToClientModel.REMOVE_STYLE_NAME, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.styles.removeAll(Arrays.asList(((String) frame.value).split(" ")));
        });
        final BiConsumer<List<PonyFrame>, PonyFrame> onText = (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.text = (String) frame.value;
        };
        onMessageSwitch.put(ServerToClientModel.HTML, onText);
        onMessageSwitch.put(ServerToClientModel.TEXT, onText);
        onMessageSwitch.put(ServerToClientModel.OPEN, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            if (handleImplicitCommunication) {
                element.sendApplicationInstruction(ClientToServerModel.HANDLER_OPEN, "");
                sendCookies();
            }
        });
        onMessageSwitch.put(ServerToClientModel.ROUNDTRIP_LATENCY, (message, frame) -> {
            if (handleImplicitCommunication) {
                final JsonObject json = Json.createObjectBuilder() //
                        .add(ClientToServerModel.TERMINAL_LATENCY.toStringValue(), 0) //
                        .build();
                sendMessage(json);
            }
        });
        onMessageSwitch.put(ServerToClientModel.WIDGET_VISIBLE, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.displayed = (boolean) frame.value;
        });
        onMessageSwitch.put(ServerToClientModel.ENABLED, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.enabled = (boolean) frame.value;
        });
        onMessageSwitch.put(ServerToClientModel.FUNCTION_ARGS, (message, frame) -> {
            final PonyWebElement element = findElement(message);
            if (element == null)
                return;
            element.text = Arrays.toString((Object[]) frame.value);
        });
    }

    @Override
    public void get(final String url) {
        try {
            client.connect(new URI(url));
            this.url = url;
        } catch (final Exception e) {
            throw new PonyIOException("Unable to connect to " + url, e);
        }
    }

    public int getContextId() {
        return contextId;
    }

    @Override
    public String getCurrentUrl() {
        return this.url;
    }

    @Override
    public String getTitle() {
        return typeHistory;
    }

    @Override
    public List<WebElement> findElements(final By by) {
        return globalContext.findElements(by);
    }

    public List<PonyWebElement> findElementsAsPony(final By by) {
        return (List<PonyWebElement>) (Object) findElements(by);
    }

    @Override
    public PonyWebElement findElement(final By by) {
        return (PonyWebElement) globalContext.findElement(by);
    }

    public PonyWebElement findElementByPonyId(final int id) {
        return elements.get(id);
    }

    @Override
    public String getPageSource() {
        final StringWriter writer = new StringWriter();
        try {
            printAsXml(writer);
        } catch (final IOException e) {
            //unreachable
        }
        return writer.toString();
    }

    @Override
    public void close() {
        client.close();
    }

    @Override
    public void quit() {
    }

    public String getSessionId() {
        return client.getSessionId();
    }

    @Override
    public Set<String> getWindowHandles() {
        return null;
    }

    @Override
    public String getWindowHandle() {
        return null;
    }

    @Override
    public TargetLocator switchTo() {
        return null;
    }

    @Override
    public Navigation navigate() {
        return null;
    }

    @Override
    public Options manage() {
        return null;
    }

    private Object readValue(final ByteBuffer b, final int minSize, final Function<ByteBuffer, Object> function) {
        length += minSize;
        return function.apply(b);
    }

    private String getString(final ByteBuffer b) {
        int stringLength = readUnsignedByte(b);
        boolean ascii = true;
        if (stringLength > ValueTypeModel.STRING_ASCII_UINT8_MAX_LENGTH) {
            switch (stringLength) {
            case ValueTypeModel.STRING_ASCII_UINT16:
                length += 2;
                stringLength = readUnsignedShort(b);
                break;
            case ValueTypeModel.STRING_ASCII_INT32:
                length += 4;
                stringLength = b.getInt();
                break;
            case ValueTypeModel.STRING_UTF8_UINT8:
                length += 1;
                stringLength = readUnsignedByte(b);
                ascii = false;
                break;
            case ValueTypeModel.STRING_UTF8_UINT16:
                length += 2;
                stringLength = readUnsignedShort(b);
                ascii = false;
                break;
            case ValueTypeModel.STRING_UTF8_INT32:
                length += 4;
                stringLength = b.getInt();
                ascii = false;
                break;
            default:
                assert false; //unreachable
            }
        }
        length += stringLength;
        return getString(ascii ? StandardCharsets.ISO_8859_1 : StandardCharsets.UTF_8, b, stringLength);
    }

    private Object getString(final ByteBuffer b, final Charset charset,
            final ToIntFunction<ByteBuffer> readStringLength) {
        final int strLength = readStringLength.applyAsInt(b);
        length += strLength;
        return getString(charset, b, strLength);
    }

    private Object getArray(final ByteBuffer b) {
        final Object[] array = new Object[readUnsignedByte(b)];
        length += array.length; //elements types
        for (int i = 0; i < array.length; i++) {
            final ArrayValueModel model = readArrayValueModel(b);
            array[i] = readArrayElementValue(b, model);
        }
        return array;
    }

    private int getUint31(final ByteBuffer buffer) {
        final int value = buffer.getShort();
        if (value >= 0)
            return value;
        length += Short.BYTES;
        return (value << 16 | readUnsignedShort(buffer)) & 0x7F_FF_FF_FF;
    }

    private Object readArrayElementValue(final ByteBuffer b, final ArrayValueModel model) {
        length += model.getMinSize();
        switch (model) {
        case NULL:
            return null;
        case BOOLEAN_FALSE:
            return Boolean.FALSE;
        case BOOLEAN_TRUE:
            return Boolean.TRUE;
        case BYTE:
            return b.get();
        case SHORT:
            return b.getShort();
        case INTEGER:
            return b.getInt();
        case LONG:
            return b.getLong();
        case DOUBLE:
            return b.getDouble();
        case FLOAT:
            return b.getFloat();
        case STRING_ASCII_UINT8_LENGTH:
            return getString(b, StandardCharsets.ISO_8859_1, PonySDKWebDriver::readUnsignedByte);
        case STRING_ASCII_UINT16_LENGTH:
            return getString(b, StandardCharsets.ISO_8859_1, PonySDKWebDriver::readUnsignedShort);
        case STRING_UTF8_UINT8_LENGTH:
            return getString(b, StandardCharsets.UTF_8, PonySDKWebDriver::readUnsignedByte);
        case STRING_UTF8_UINT16_LENGTH:
            return getString(b, StandardCharsets.UTF_8, PonySDKWebDriver::readUnsignedShort);
        case STRING_UTF8_INT32_LENGTH:
            return getString(b, StandardCharsets.UTF_8, ByteBuffer::getInt);
        default:
            throw new IllegalArgumentException("ArrayValueModel " + model + " is not supported");
        }
    }

    private Object readModelValue(final ByteBuffer b, final ValueTypeModel type) {
        switch (type) {
        case NULL:
            return null;
        case BOOLEAN:
            return readValue(b, 1, buff -> buff.get() == BooleanModel.TRUE.getValue());
        case BYTE:
            return readValue(b, 1, ByteBuffer::get);
        case SHORT:
            return readValue(b, 2, ByteBuffer::getShort);
        case INTEGER:
            return readValue(b, 4, ByteBuffer::getInt);
        case LONG:
            return readValue(b, 8, ByteBuffer::getLong);
        case DOUBLE:
            return readValue(b, 8, ByteBuffer::getDouble);
        case FLOAT:
            return readValue(b, 4, ByteBuffer::getFloat);
        case STRING:
            return readValue(b, 1, this::getString);
        case ARRAY:
            return readValue(b, 1, this::getArray);
        case UINT31:
            return readValue(b, 2, this::getUint31);
        default:
            throw new IllegalArgumentException("ValueTypeModel " + type + " is not supported");
        }
    }

    private synchronized void onMessage(final ByteBuffer message) {
        bandwithListener.onReceive(message.remaining());
        while (message.hasRemaining()) {
            final ByteBuffer b = prepareBuffer(message);

            loop: while (b.hasRemaining()) {

                final int position = b.position();
                final ServerToClientModel model = readModel(b);
                length = 1;
                try {
                    final Object value = readModelValue(b, model.getTypeModel());
                    onMessage(model, value);
                } catch (final BufferUnderflowException e) {
                    b.position(position);
                    break loop;
                } catch (final PonyIOException e) {
                    log.error("Error when reading message", e);
                    close();
                    return;
                }
            }

            postpareBuffer(message, b);
        }

    }

    private ByteBuffer prepareBuffer(final ByteBuffer message) {
        ByteBuffer b;
        //buffer is in write mode
        if (buffer.position() == 0) { //buffer has no pending data => use message directly
            b = message;
        } else { //buffer has pending data => append message to it
            if (message.remaining() <= buffer.remaining()) {
                buffer.put(message);
            } else {
                final int limit = message.limit();
                message.limit(message.position() + buffer.remaining());
                buffer.put(message);
                message.limit(limit);
            }
            buffer.flip();
            b = buffer;
        }
        return b;
    }

    private void postpareBuffer(final ByteBuffer message, final ByteBuffer b) {
        if (b == message && b.hasRemaining()) {
            if (buffer.capacity() < length) {
                buffer = ByteBuffer.allocate(Math.max(buffer.capacity() << 1, length));
            }
            buffer.put(message);
        } else if (b == buffer && b.hasRemaining()) {
            if (buffer.capacity() < length) {
                buffer = ByteBuffer.allocate(Math.max(buffer.capacity() << 1, length));
                buffer.put(b);
            } else {
                b.compact();
            }
        } else if (b == buffer && !b.hasRemaining()) {
            b.clear();
        }
    }

    private void onMessage(final ServerToClientModel model, final Object value) {
        if (model == ServerToClientModel.END) {
            onMessage(messageInConstruction);
            messageInConstruction.clear();
        } else {
            messageInConstruction.add(new PonyFrame(model, value));
        }
    }

    private void onMessage(final List<PonyFrame> message) {
        log.debug("IN : {}", message);
        for (final PonyFrame frame : message) {
            onMessageSwitch.getOrDefault(frame.getModel(), DO_NOTHING_WITH_FRAME).accept(message, frame);
        }
        messageListener.onReceiveMessage(message);
    }

    public PonyWebElement findElement(final List<PonyFrame> message) {
        Object id = findValueForModel(message, ServerToClientModel.TYPE_UPDATE);
        if (id == null) {
            id = findValueForModel(message, ServerToClientModel.TYPE_CREATE);
            if (id == null)
                return null;
        }
        return elements.get(id);
    }

    public static Object findValueForModel(final List<PonyFrame> message, final ServerToClientModel model) {
        for (final PonyFrame event : message) {
            if (event.model == model)
                return event.getValue();
        }
        return null;
    }

    private static byte[] getLocalByteArray(final int minLength) {
        byte[] array = byteArrays.get();
        if (array.length < minLength)
            byteArrays.set(array = new byte[Math.max(array.length << 1, minLength)]);
        return array;
    }

    private static String getString(final Charset charset, final ByteBuffer b, final int length) {
        final byte[] bytes = getLocalByteArray(length);
        b.get(bytes, 0, length);
        return new String(bytes, 0, length, charset);
    }

    private ServerToClientModel readModel(final ByteBuffer buffer) {
        return ServerToClientModel.fromRawValue(readUnsignedByte(buffer));
    }

    private ArrayValueModel readArrayValueModel(final ByteBuffer buffer) {
        return ArrayValueModel.fromRawValue(buffer.get());
    }

    private static int readUnsignedByte(final ByteBuffer buffer) {
        return buffer.get() & 0xFF;
    }

    private static int readUnsignedShort(final ByteBuffer buffer) {
        return buffer.getShort() & 0xFFFF;
    }

    private void sendTypeHistory(final String value) {
        sendApplicationInstruction(
                Json.createObjectBuilder().add(ClientToServerModel.TYPE_HISTORY.toStringValue(), value).build());
    }

    public void sendCookies() {
        final JsonArrayBuilder builder = Json.createArrayBuilder();
        for (final Map.Entry<String, String> entry : cookies.entrySet()) {
            builder.add(Json.createObjectBuilder() //
                    .add(ClientToServerModel.COOKIE_NAME.toStringValue(), entry.getKey())
                    .add(ClientToServerModel.COOKIE_VALUE.toStringValue(), entry.getValue()) //
                    .build());
        }
        sendApplicationInstruction(Json.createObjectBuilder() //
                .add(ClientToServerModel.OBJECT_ID.toStringValue(), 0) //
                .add(ClientToServerModel.COOKIES.toStringValue(), builder.build())//
                .build());
    }

    public void sendApplicationInstruction(final JsonObject instruction) {
        final JsonObject json = Json.createObjectBuilder()
                .add(ClientToServerModel.APPLICATION_INSTRUCTIONS.toStringValue(), Json.createArrayBuilder() //
                        .add(instruction).build() //
                ).build();
        sendMessage(json);
    }

    public void sendMessage(final JsonObject json) {
        final String str = json.toString();
        log.debug("OUT : {}", str);
        sendMessage(str);
        messageListener.onSendMessage(json);
    }

    private void sendMessage(final String msg) {
        try {
            client.sendMessage(msg);

            // UTF-8 encoding is used (for ASCII characters : 1 char <=> 1 byte)
            // To avoid iterating all characters, I consider the amount of non-ASCII characters to be negligible
            bandwithListener.onSend(msg.length());
        } catch (IOException | RuntimeException e) {
            throw new PonyIOException("Failed to send message " + msg, e);
        }
    }

    public void printAsXml(final Writer writer) throws IOException {
        writer.write("<WEB>");
        writer.write('\n');
        for (final PonyWebElement e : elements.values()) {
            if (e.parent == null)
                e.printTree(1, writer);
        }
        writer.write("</WEB>");
    }

    public void clear() {
        elements.clear();
    }

    public boolean isHandleImplicitCommunication() {
        return handleImplicitCommunication;
    }

    private final static PonyMessageListener INDIFFERENT_MSG_LISTENER = new PonyMessageListener() {

        @Override
        public void onSendMessage(final JsonObject message) {
        }

        @Override
        public void onReceiveMessage(final List<PonyFrame> message) {
        }
    };

    private final static PonyBandwithListener INDIFFERENT_BANDWITH_LISTENER = new PonyBandwithListener() {

        @Override
        public void onSend(final int bytes) {
        }

        @Override
        public void onReceive(final int bytes) {
        }

        @Override
        public void onSendCompressed(final int bytes) {
        }

        @Override
        public void onReceiveCompressed(final int bytes) {
        }
    };

    private static final BiConsumer<List<PonyFrame>, PonyFrame> DO_NOTHING_WITH_FRAME = (message, frame) -> {
    };

    private static final PonySessionListener INDIFFERENT_SESSION_LISTENER = new PonySessionListener() {

        @Override
        public void onOpen(final Session session) {
        }

        @Override
        public void onError(final Session session, final Throwable thr) {
        }

        @Override
        public void onClose(final Session session, final CloseReason closeReason) {
        }
    };
}