package org.graylog2.log4j2;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AppenderLoggingException;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.net.Severity;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.status.StatusLogger;
import org.graylog2.gelfclient.GelfConfiguration;
import org.graylog2.gelfclient.GelfMessage;
import org.graylog2.gelfclient.GelfMessageBuilder;
import org.graylog2.gelfclient.GelfMessageLevel;
import org.graylog2.gelfclient.GelfTransports;
import org.graylog2.gelfclient.transport.GelfTransport;

import java.io.File;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

@Plugin(name = "GELF", category = "Core", elementType = "appender", printObject = true)
public class GelfAppender extends AbstractAppender {
    private static final long serialVersionUID = 4796033328540158817L;

    private static Pattern IPV4_PATTERN = Pattern.compile("[:0-9a-f]*(\\d{1,3}\\.){3}(\\d{1,3})");
    private static Pattern IPV6_PATTERN = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}");

    private static final Logger LOG = StatusLogger.getLogger();

    private final GelfConfiguration gelfConfiguration;
    private final String hostName;
    private final boolean includeSource;
    private final boolean includeThreadContext;
    private final boolean includeStackTrace;
    private final boolean includeExceptionCause;
    private final Map<String, Object> additionalFields;

    private GelfTransport client;

    protected GelfAppender(final String name,
                           final Layout<? extends Serializable> layout,
                           final Filter filter,
                           final boolean ignoreExceptions,
                           final GelfConfiguration gelfConfiguration,
                           final String hostName,
                           final boolean includeSource,
                           final boolean includeThreadContext,
                           final boolean includeStackTrace,
                           final KeyValuePair[] additionalFields,
                           final boolean includeExceptionCause) {
        super(name, filter, layout, ignoreExceptions);
        this.gelfConfiguration = gelfConfiguration;
        this.hostName = hostName;
        this.includeSource = includeSource;
        this.includeThreadContext = includeThreadContext;
        this.includeStackTrace = includeStackTrace;
        this.includeExceptionCause = includeExceptionCause;

        if (null != additionalFields) {
            this.additionalFields = new HashMap<>();
            for (KeyValuePair pair : additionalFields) {
                this.additionalFields.put(pair.getKey(), pair.getValue());
            }
        } else {
            this.additionalFields = Collections.emptyMap();
        }
    }

    @Override
    public void append(LogEvent event) {
        final Layout<? extends Serializable> layout = getLayout();
        final String formattedMessage;
        if (layout == null) {
            formattedMessage = event.getMessage().getFormattedMessage();
        } else {
            formattedMessage = new String(layout.toByteArray(event), StandardCharsets.UTF_8);
        }

        final GelfMessageBuilder builder = new GelfMessageBuilder(formattedMessage, hostName)
                .timestamp(event.getTimeMillis() / 1000d)
                .level(GelfMessageLevel.fromNumericLevel(Severity.getSeverity(event.getLevel()).getCode()))
                .additionalField("loggerName", event.getLoggerName())
                .additionalField("threadName", event.getThreadName());

        final Marker marker = event.getMarker();
        if (marker != null) {
            builder.additionalField("marker", marker.getName());
        }

        if (includeThreadContext) {
            for (Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
                builder.additionalField(entry.getKey(), entry.getValue());
            }

            // Guard against https://issues.apache.org/jira/browse/LOG4J2-1530
            final ThreadContext.ContextStack contextStack = event.getContextStack();
            if (contextStack != null) {
                final List<String> contextStackItems = contextStack.asList();
                if (contextStackItems != null && !contextStackItems.isEmpty()) {
                    builder.additionalField("contextStack", contextStackItems.toString());
                }
            }
        }

        if (includeSource) {
            final StackTraceElement source = event.getSource();
            if (source != null) {
                builder.additionalField("sourceFileName", source.getFileName());
                builder.additionalField("sourceMethodName", source.getMethodName());
                builder.additionalField("sourceClassName", source.getClassName());
                builder.additionalField("sourceLineNumber", source.getLineNumber());
            }
        }

        @SuppressWarnings("all")
        final Throwable thrown = event.getThrown();
        if (includeStackTrace && thrown != null) {
            String stackTrace;
            if (includeExceptionCause) {
                final StringWriter stringWriter = new StringWriter();
                final PrintWriter printWriter = new PrintWriter(stringWriter);
                thrown.printStackTrace(printWriter);
                stackTrace = stringWriter.toString();
            } else {
                stackTrace = getSimpleStacktraceAsString(thrown);
            }

            builder.additionalField("exceptionClass", thrown.getClass().getCanonicalName());
            builder.additionalField("exceptionMessage", thrown.getMessage());
            builder.additionalField("exceptionStackTrace", stackTrace);

            builder.fullMessage(formattedMessage);
        }

        if (!additionalFields.isEmpty()) {
            builder.additionalFields(additionalFields);
        }

        final GelfMessage gelfMessage = builder.build();
        try {
            final boolean sent = client.trySend(gelfMessage);
            if (!sent) {
                LOG.debug("Couldn't send message: {}", gelfMessage);
            }
        } catch (Exception e) {
            throw new AppenderLoggingException("failed to write log event to GELF server: " + e.getMessage(), e);
        }
    }

    protected String getSimpleStacktraceAsString(final Throwable thrown) {
        final StringBuilder stackTraceBuilder = new StringBuilder();
        for (StackTraceElement stackTraceElement : thrown.getStackTrace()) {
            new Formatter(stackTraceBuilder).format("%s.%s(%s:%d)%n",
                    stackTraceElement.getClassName(),
                    stackTraceElement.getMethodName(),
                    stackTraceElement.getFileName(),
                    stackTraceElement.getLineNumber());
        }
        return stackTraceBuilder.toString();
    }

    protected void setClient(GelfTransport client) {
        this.client = requireNonNull(client);
    }

    @Override
    public void start() {
        super.start();
        client = GelfTransports.create(gelfConfiguration);
    }

    @Override
    public void stop() {
        super.stop();
        if (client != null) {
            client.stop();
        }
    }

    @Override
    public String toString() {
        return GelfAppender.class.getSimpleName() + "{"
                + "name=" + getName()
                + ",server=" + gelfConfiguration.getRemoteAddress().getHostName()
                + ",port=" + gelfConfiguration.getRemoteAddress().getPort()
                + ",protocol=" + gelfConfiguration.getTransport().toString()
                + ",hostName=" + hostName
                + ",queueSize=" + gelfConfiguration.getQueueSize()
                + ",connectTimeout=" + gelfConfiguration.getConnectTimeout()
                + ",reconnectDelay=" + gelfConfiguration.getReconnectDelay()
                + ",sendBufferSize=" + gelfConfiguration.getSendBufferSize()
                + ",tcpNoDelay=" + gelfConfiguration.isTcpNoDelay()
                + ",tcpKeepAlive=" + gelfConfiguration.isTcpKeepAlive()
                + ",tlsEnabled=" + gelfConfiguration.isTlsEnabled()
                + ",tlsCertVerificationEnabled=" + gelfConfiguration.isTlsCertVerificationEnabled()
                + ",tlsTrustCertChainFilename=" + (gelfConfiguration.getTlsTrustCertChainFile() != null ?
                gelfConfiguration.getTlsTrustCertChainFile().getPath() : "null")
                + "}";
    }

    /**
     * Factory method for creating a {@link GelfTransport} provider within the plugin manager.
     *
     * @param name                             The name of the Appender.
     * @param filter                           A Filter to determine if the event should be handled by this Appender.
     * @param layout                           The Layout to use to format the LogEvent defaults to {@code "%m%n"}.
     * @param ignoreExceptions                 The default is {@code true}, causing exceptions encountered while appending events
     *                                         to be internally logged and then ignored. When set to {@code false} exceptions will
     *                                         be propagated to the caller, instead. Must be set to {@code false} when wrapping this
     *                                         Appender in a {@link org.apache.logging.log4j.core.appender.FailoverAppender}.
     * @param server                           The server name of the GELF server, defaults to {@code localhost}.
     * @param port                             The port the GELF server is listening on, defaults to {@code 12201}.
     * @param hostName                         The host name of the machine generating the logs, defaults to local host name
     *                                         or {@code localhost} if it couldn't be detected.
     * @param protocol                         The transport protocol to use, defaults to {@code UDP}.
     * @param tlsEnabled                       Whether TLS should be enabled, defaults to {@code false}.
     * @param tlsEnableCertificateVerification Whether TLS certificate chain should be checked, defaults to {@code true}.
     * @param tlsTrustCertChainFilename        A X.509 certificate chain file in PEM format for certificate verification, defaults to {@code null}
     * @param queueSize                        The size of the internally used queue, defaults to {@code 512}.
     * @param connectTimeout                   The connection timeout for TCP connections in milliseconds, defaults to {@code 1000}.
     * @param reconnectDelay                   The time to wait between reconnects in milliseconds, defaults to {@code 500}.
     * @param sendBufferSize                   The size of the socket send buffer in bytes, defaults to {@code -1} (deactivate).
     * @param tcpNoDelay                       Whether Nagle's algorithm should be used for TCP connections, defaults to {@code false}.
     * @param tcpKeepAlive                     Whether to try keeping alive TCP connections, defaults to {@code false}.
     * @param includeSource                    Whether the source of the log message should be included, defaults to {@code true}.
     * @param includeThreadContext             Whether the contents of the {@link org.apache.logging.log4j.ThreadContext} should be included, defaults to {@code true}.
     * @param includeStackTrace                Whether a full stack trace should be included, defaults to {@code true}.
     * @param includeExceptionCause            Whether the included stack trace should contain causing exceptions, defaults to {@code false}.
     * @param additionalFields                 Additional static key=value pairs that will be added to every log message.
     * @return a new GELF provider
     */
    @PluginFactory
    @SuppressWarnings("unused")
    public static GelfAppender createGelfAppender(@PluginElement("Filter") Filter filter,
                                                  @PluginElement("Layout") Layout<? extends Serializable> layout,
                                                  @PluginElement(value = "AdditionalFields") final KeyValuePair[] additionalFields,
                                                  @PluginAttribute(value = "name") String name,
                                                  @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) Boolean ignoreExceptions,
                                                  @PluginAttribute(value = "server", defaultString = "localhost") String server,
                                                  @PluginAttribute(value = "port", defaultInt = 12201) Integer port,
                                                  @PluginAttribute(value = "protocol", defaultString = "UDP") String protocol,
                                                  @PluginAttribute(value = "hostName") String hostName,
                                                  @PluginAttribute(value = "queueSize", defaultInt = 512) Integer queueSize,
                                                  @PluginAttribute(value = "connectTimeout", defaultInt = 1000) Integer connectTimeout,
                                                  @PluginAttribute(value = "reconnectDelay", defaultInt = 500) Integer reconnectDelay,
                                                  @PluginAttribute(value = "sendBufferSize", defaultInt = -1) Integer sendBufferSize,
                                                  @PluginAttribute(value = "tcpNoDelay", defaultBoolean = false) Boolean tcpNoDelay,
                                                  @PluginAttribute(value = "tcpKeepAlive", defaultBoolean = false) Boolean tcpKeepAlive,
                                                  @PluginAttribute(value = "includeSource", defaultBoolean = true) Boolean includeSource,
                                                  @PluginAttribute(value = "includeThreadContext", defaultBoolean = true) Boolean includeThreadContext,
                                                  @PluginAttribute(value = "includeStackTrace", defaultBoolean = true) Boolean includeStackTrace,
                                                  @PluginAttribute(value = "includeExceptionCause", defaultBoolean = false) Boolean includeExceptionCause,
                                                  @PluginAttribute(value = "tlsEnabled", defaultBoolean = false) Boolean tlsEnabled,
                                                  @PluginAttribute(value = "tlsEnableCertificateVerification", defaultBoolean = true) Boolean tlsEnableCertificateVerification,
                                                  @PluginAttribute(value = "tlsTrustCertChainFilename") String tlsTrustCertChainFilename) {
        if (name == null) {
            LOGGER.error("No name provided for ConsoleAppender");
            return null;
        }

        if (!"UDP".equalsIgnoreCase(protocol) && !"TCP".equalsIgnoreCase(protocol)) {
            LOG.warn("Invalid protocol {}, falling back to UDP", protocol);
            protocol = "UDP";
        }
        if (hostName == null || hostName.trim().isEmpty()) {
            try {
                final String canonicalHostName = InetAddress.getLocalHost().getCanonicalHostName();
                if (isFQDN(canonicalHostName)) {
                    hostName = canonicalHostName;
                } else {
                    hostName = InetAddress.getLocalHost().getHostName();
                }
            } catch (UnknownHostException e) {
                LOG.warn("Couldn't detect local host name, falling back to \"localhost\"");
                hostName = "localhost";
            }
        }

        final InetSocketAddress serverAddress = new InetSocketAddress(server, port);
        final GelfTransports gelfProtocol = GelfTransports.valueOf(protocol.toUpperCase());
        final GelfConfiguration gelfConfiguration = new GelfConfiguration(serverAddress)
                .transport(gelfProtocol)
                .queueSize(queueSize)
                .connectTimeout(connectTimeout)
                .reconnectDelay(reconnectDelay)
                .sendBufferSize(sendBufferSize)
                .tcpNoDelay(tcpNoDelay)
                .tcpKeepAlive(tcpKeepAlive);

        if (tlsEnabled) {
            if (gelfProtocol.equals(GelfTransports.TCP)) {
                gelfConfiguration.enableTls();
                if (!tlsEnableCertificateVerification) {
                    LOG.warn("TLS certificate validation is disabled. This is unsecure!");
                    gelfConfiguration.disableTlsCertVerification();
                }
                if (tlsEnableCertificateVerification && tlsTrustCertChainFilename != null) {
                    gelfConfiguration.tlsTrustCertChainFile(new File(tlsTrustCertChainFilename));
                }
            } else {
                LOG.warn("Enabling of TLS is invalid for UDP Transport");
            }
        }

        return new GelfAppender(name, layout, filter, ignoreExceptions, gelfConfiguration, hostName, includeSource,
                includeThreadContext, includeStackTrace, additionalFields, includeExceptionCause);
    }

    static boolean isFQDN(String canonicalHostName) {
        return canonicalHostName.contains(".") &&
                !IPV4_PATTERN.matcher(canonicalHostName).matches() &&
                !IPV6_PATTERN.matcher(canonicalHostName).matches();
    }
}


/*
 * Copyright 2014 Blaž Šolar
 *
 * 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.wefika.horizontalpicker;

import android.animation.ArgbEvaluator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.text.TextDirectionHeuristicCompat;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.EdgeEffect;
import android.widget.OverScroller;

import java.lang.ref.WeakReference;
import java.util.List;

/**
 * Created by Blaž Šolar on 24/01/14.
 */
public class HorizontalPicker extends View {

    public static final String TAG = "HorizontalTimePicker";

    /**
     * The coefficient by which to adjust (divide) the max fling velocity.
     */
    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 4;

    /**
     * The the duration for adjusting the selector wheel.
     */
    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;

    /**
     * Determines speed during touch scrolling.
     */
    private VelocityTracker mVelocityTracker;

    /**
     * @see android.view.ViewConfiguration#getScaledMinimumFlingVelocity()
     */
    private int mMinimumFlingVelocity;

    /**
     * @see android.view.ViewConfiguration#getScaledMaximumFlingVelocity()
     */
    private int maximumFlingVelocity;

    private final int overscrollDistance;

    private int touchSlop;

    private CharSequence[] values;
    private BoringLayout[] layouts;

    private TextPaint textPaint;
    private BoringLayout.Metrics boringMetrics;
    private TextUtils.TruncateAt ellipsize;

    private int itemWidth;
    private RectF itemClipBounds;
    private RectF itemClipBoundsOffset;

    private float lastDownEventX;

    private OverScroller flingScrollerX;
    private OverScroller adjustScrollerX;

    private int previousScrollerX;

    private boolean scrollingX;
    private int pressedItem = -1;

    private ColorStateList textColor;

    private OnItemSelected onItemSelected;
    private OnItemClicked onItemClicked;

    private int selectedItem;

    private EdgeEffect leftEdgeEffect;
    private EdgeEffect rightEdgeEffect;

    private Marquee marquee;
    private int marqueeRepeatLimit = 3;

    private float dividerSize = 0;

    private int sideItems = 1;

    private TextDirectionHeuristicCompat textDir;

    private final PickerTouchHelper touchHelper;

    public HorizontalPicker(Context context) {
        this(context, null);
    }

    public HorizontalPicker(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.horizontalPickerStyle);
    }

    public HorizontalPicker(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // create the selector wheel paint
        TextPaint paint = new TextPaint();
        paint.setAntiAlias(true);
        textPaint = paint;

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.HorizontalPicker,
                defStyle, 0
        );

        CharSequence[] values;
        int ellipsize = 3; // END default value
        int sideItems = this.sideItems;

        try {
            textColor = a.getColorStateList(R.styleable.HorizontalPicker_android_textColor);
            if (textColor == null) {
                textColor = ColorStateList.valueOf(0xFF000000);
            }

            values = a.getTextArray(R.styleable.HorizontalPicker_values);
            ellipsize = a.getInt(R.styleable.HorizontalPicker_android_ellipsize, ellipsize);
            marqueeRepeatLimit = a.getInt(R.styleable.HorizontalPicker_android_marqueeRepeatLimit, marqueeRepeatLimit);
            dividerSize = a.getDimension(R.styleable.HorizontalPicker_dividerSize, dividerSize);
            sideItems = a.getInt(R.styleable.HorizontalPicker_sideItems, sideItems);

            float textSize = a.getDimension(R.styleable.HorizontalPicker_android_textSize, -1);
            if(textSize > -1) {
                setTextSize(textSize);
            }
        } finally {
            a.recycle();
        }

        switch (ellipsize) {
            case 1:
                setEllipsize(TextUtils.TruncateAt.START);
                break;
            case 2:
                setEllipsize(TextUtils.TruncateAt.MIDDLE);
                break;
            case 3:
                setEllipsize(TextUtils.TruncateAt.END);
                break;
            case 4:
                setEllipsize(TextUtils.TruncateAt.MARQUEE);
                break;
        }

        Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt();
        boringMetrics = new BoringLayout.Metrics();
        boringMetrics.ascent = fontMetricsInt.ascent;
        boringMetrics.bottom = fontMetricsInt.bottom;
        boringMetrics.descent = fontMetricsInt.descent;
        boringMetrics.leading = fontMetricsInt.leading;
        boringMetrics.top = fontMetricsInt.top;
        boringMetrics.width = itemWidth;

        setWillNotDraw(false);

        flingScrollerX = new OverScroller(context);
        adjustScrollerX = new OverScroller(context, new DecelerateInterpolator(2.5f));

        // initialize constants
        ViewConfiguration configuration = ViewConfiguration.get(context);
        touchSlop = configuration.getScaledTouchSlop();
        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        maximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
        overscrollDistance = configuration.getScaledOverscrollDistance();

        previousScrollerX = Integer.MIN_VALUE;

        setValues(values);
        setSideItems(sideItems);

        touchHelper = new PickerTouchHelper(this);
        ViewCompat.setAccessibilityDelegate(this, touchHelper);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int height;
        if(heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            int heightText = (int) (Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent));
            heightText += getPaddingTop() + getPaddingBottom();

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(heightSize, heightText);
            } else {
                height = heightText;
            }
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int saveCount = canvas.getSaveCount();
        canvas.save();

        int selectedItem = this.selectedItem;

        float itemWithPadding = itemWidth + dividerSize;

        // translate horizontal to center
        canvas.translate(itemWithPadding * sideItems, 0);

        if (values != null) {
            for (int i = 0; i < values.length; i++) {

                // set text color for item
                textPaint.setColor(getTextColor(i));

                // get text layout
                BoringLayout layout = layouts[i];

                int saveCountHeight = canvas.getSaveCount();
                canvas.save();

                float x = 0;

                float lineWidth = layout.getLineWidth(0);
                if (lineWidth > itemWidth) {
                    if (isRtl(values[i])) {
                        x += (lineWidth - itemWidth) / 2;
                    } else {
                        x -= (lineWidth - itemWidth) / 2;
                    }
                }

                if (marquee != null && i == selectedItem) {
                    x += marquee.getScroll();
                }

                // translate vertically to center
                canvas.translate(-x, (canvas.getHeight() - layout.getHeight()) / 2);

                RectF clipBounds;
                if (x == 0) {
                    clipBounds = itemClipBounds;
                } else {
                    clipBounds = itemClipBoundsOffset;
                    clipBounds.set(itemClipBounds);
                    clipBounds.offset(x, 0);
                }

                canvas.clipRect(clipBounds);
                layout.draw(canvas);

                if (marquee != null && i == selectedItem && marquee.shouldDrawGhost()) {
                    canvas.translate(marquee.getGhostOffset(), 0);
                    layout.draw(canvas);
                }

                // restore vertical translation
                canvas.restoreToCount(saveCountHeight);

                // translate horizontal for 1 item
                canvas.translate(itemWithPadding, 0);
            }
        }

        // restore horizontal translation
        canvas.restoreToCount(saveCount);

        drawEdgeEffect(canvas, leftEdgeEffect, 270);
        drawEdgeEffect(canvas, rightEdgeEffect, 90);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        super.onRtlPropertiesChanged(layoutDirection);

        textDir = getTextDirectionHeuristic();
    }

    /**
     * TODO cache values
     * @param text
     * @return
     */
    private boolean isRtl(CharSequence text) {
        if (textDir == null) {
            textDir = getTextDirectionHeuristic();
        }

        return textDir.isRtl(text, 0, text.length());
    }

    private TextDirectionHeuristicCompat getTextDirectionHeuristic() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {

            return TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR;

        } else {

            // Always need to resolve layout direction first
            final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);

            switch (getTextDirection()) {
                default:
                case TEXT_DIRECTION_FIRST_STRONG:
                    return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL :
                            TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR);
                case TEXT_DIRECTION_ANY_RTL:
                    return TextDirectionHeuristicsCompat.ANYRTL_LTR;
                case TEXT_DIRECTION_LTR:
                    return TextDirectionHeuristicsCompat.LTR;
                case TEXT_DIRECTION_RTL:
                    return TextDirectionHeuristicsCompat.RTL;
                case TEXT_DIRECTION_LOCALE:
                    return TextDirectionHeuristicsCompat.LOCALE;
            }
        }
    }

    private void remakeLayout() {

        if (layouts != null && layouts.length > 0 && getWidth() > 0)  {
            for (int i = 0; i < layouts.length; i++) {
                layouts[i].replaceOrMake(values[i], textPaint, itemWidth,
                        Layout.Alignment.ALIGN_CENTER, 1f, 1f, boringMetrics, false, ellipsize,
                        itemWidth);
            }
        }

    }

    private void drawEdgeEffect(Canvas canvas, EdgeEffect edgeEffect, int degrees) {

        if (canvas == null || edgeEffect == null || (degrees != 90 && degrees != 270)) {
            return;
        }

        if(!edgeEffect.isFinished()) {
            final int restoreCount = canvas.getSaveCount();
            final int width = getWidth();
            final int height = getHeight();

            canvas.rotate(degrees);

            if (degrees == 270) {
                canvas.translate(-height, Math.max(0, getScrollX()));
            } else { // 90
                canvas.translate(0, -(Math.max(getScrollRange(), getScaleX()) + width));
            }

            edgeEffect.setSize(height, width);
            if(edgeEffect.draw(canvas)) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    postInvalidateOnAnimation();
                } else {
                    postInvalidate();
                }
            }

            canvas.restoreToCount(restoreCount);
        }

    }

    /**
     * Calculates text color for specified item based on its position and state.
     *
     * @param item Index of item to get text color for
     * @return Item text color
     */
    private int getTextColor(int item) {

        int scrollX = getScrollX();

        // set color of text
        int color = textColor.getDefaultColor();
        int itemWithPadding = (int) (itemWidth + dividerSize);
        if (scrollX > itemWithPadding * item - itemWithPadding / 2 &&
                scrollX < itemWithPadding * (item + 1) - itemWithPadding / 2) {
            int position = scrollX - itemWithPadding / 2;
            color = getColor(position, item);
        } else if(item == pressedItem) {
            color = textColor.getColorForState(new int[] { android.R.attr.state_pressed }, color);
        }

        return color;

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        calculateItemSize(w, h);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if(!isEnabled()) {
            return false;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_MOVE:

                float currentMoveX = event.getX();

                int deltaMoveX = (int) (lastDownEventX - currentMoveX);

                if(scrollingX ||
                        (Math.abs(deltaMoveX) > touchSlop) && values != null && values.length > 0) {

                    if(!scrollingX) {
                        deltaMoveX = 0;
                        pressedItem = -1;
                        scrollingX = true;
                        getParent().requestDisallowInterceptTouchEvent(true);
                        stopMarqueeIfNeeded();
                    }

                    final int range = getScrollRange();

                    if(overScrollBy(deltaMoveX, 0, getScrollX(), 0, range, 0,
                            overscrollDistance, 0, true)) {
                        mVelocityTracker.clear();
                    }

                    final float pulledToX = getScrollX() + deltaMoveX;
                    if(pulledToX < 0) {
                        leftEdgeEffect.onPull((float) deltaMoveX / getWidth());
                        if(!rightEdgeEffect.isFinished()) {
                            rightEdgeEffect.onRelease();
                        }
                    } else if(pulledToX > range) {
                        rightEdgeEffect.onPull((float) deltaMoveX / getWidth());
                        if(!leftEdgeEffect.isFinished()) {
                            leftEdgeEffect.onRelease();
                        }
                    }

                    lastDownEventX = currentMoveX;
                    invalidate();

                }

                break;
            case MotionEvent.ACTION_DOWN:

                if(!adjustScrollerX.isFinished()) {
                    adjustScrollerX.forceFinished(true);
                } else if(!flingScrollerX.isFinished()) {
                    flingScrollerX.forceFinished(true);
                } else {
                    scrollingX = false;
                }

                lastDownEventX = event.getX();

                if(!scrollingX) {
                    pressedItem = getPositionFromTouch(event.getX());
                }
                invalidate();

                break;
            case MotionEvent.ACTION_UP:

                VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, maximumFlingVelocity);
                int initialVelocityX = (int) velocityTracker.getXVelocity();

                if(scrollingX && Math.abs(initialVelocityX) > mMinimumFlingVelocity) {
                    flingX(initialVelocityX);
                } else if (values != null) {
                    float positionX = event.getX();
                    if(!scrollingX) {

                        int itemPos = getPositionOnScreen(positionX);
                        int relativePos = itemPos - sideItems;

                        if (relativePos == 0) {
                            selectItem();
                        } else {
                            smoothScrollBy(relativePos);
                        }

                    } else if(scrollingX) {
                        finishScrolling();
                    }
                }

                mVelocityTracker.recycle();
                mVelocityTracker = null;

                if(leftEdgeEffect != null) {
                    leftEdgeEffect.onRelease();
                    rightEdgeEffect.onRelease();
                }

            case MotionEvent.ACTION_CANCEL:
                pressedItem = -1;
                invalidate();

                if(leftEdgeEffect != null) {
                    leftEdgeEffect.onRelease();
                    rightEdgeEffect.onRelease();
                }

                break;
        }

        return true;
    }

    private void selectItem() {
        // post to the UI Thread to avoid potential interference with the OpenGL Thread
        if (onItemClicked != null) {
            post(new Runnable() {
                @Override
                public void run() {
                    onItemClicked.onItemClicked(getSelectedItem());
                }
            });
        }

        adjustToNearestItemX();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (!isEnabled()) {
            return super.onKeyDown(keyCode, event);
        }

        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                selectItem();
                return true;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                smoothScrollBy(-1);
                return true;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                smoothScrollBy(1);
                return true;
            default:
                return super.onKeyDown(keyCode, event);
        }

    }

    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {

        if (touchHelper.dispatchHoverEvent(event)) {
            return true;
        }

        return super.dispatchHoverEvent(event);
    }

    @Override
    public void computeScroll() {
        computeScrollX();
    }

    @Override
    public void getFocusedRect(Rect r) {
        super.getFocusedRect(r); // TODO this should only be current item
    }

    public void setOnItemSelectedListener(OnItemSelected onItemSelected) {
        this.onItemSelected = onItemSelected;
    }

    public void setOnItemClickedListener(OnItemClicked onItemClicked) {
        this.onItemClicked = onItemClicked;
    }

    public int getSelectedItem() {
        int x = getScrollX();
        return getPositionFromCoordinates(x);
    }

    public void setSelectedItem(int index) {
        selectedItem = index;
        scrollToItem(index);
    }

    public int getMarqueeRepeatLimit() {
        return marqueeRepeatLimit;
    }

    public void setMarqueeRepeatLimit(int marqueeRepeatLimit) {
        this.marqueeRepeatLimit = marqueeRepeatLimit;
    }

    /**
     * @return Number of items on each side of current item.
     */
    public int getSideItems() {
        return sideItems;
    }

    public void setSideItems(int sideItems) {
        if (this.sideItems < 0) {
            throw new IllegalArgumentException("Number of items on each side must be grater or equal to 0.");
        } else if (this.sideItems != sideItems) {
            this.sideItems = sideItems;
            calculateItemSize(getWidth(), getHeight());
        }
    }

    /**
     * @return
     */
    public CharSequence[] getValues() {
        return values;
    }

    /**
     * Sets values to choose from
     * @param values New values to choose from
     */
    public void setValues(CharSequence[] values) {

        if (this.values != values) {
            this.values = values;

            if (this.values != null) {
                layouts = new BoringLayout[this.values.length];
                for (int i = 0; i < layouts.length; i++) {
                    layouts[i] = new BoringLayout(this.values[i], textPaint, itemWidth, Layout.Alignment.ALIGN_CENTER,
                            1f, 1f, boringMetrics, false, ellipsize, itemWidth);
                }
            } else {
                layouts = new BoringLayout[0];
            }

            // start marque only if has already been measured
            if (getWidth() > 0) {
                startMarqueeIfNeeded();
            }

            requestLayout();
            invalidate();
        }

    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        setSelectedItem(ss.mSelItem);

    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState savedState = new SavedState(superState);
        savedState.mSelItem = selectedItem;

        return savedState;

    }

    @Override
    public void setOverScrollMode(int overScrollMode) {
        if(overScrollMode != OVER_SCROLL_NEVER) {
            Context context = getContext();
            leftEdgeEffect = new EdgeEffect(context);
            rightEdgeEffect = new EdgeEffect(context);
        } else {
            leftEdgeEffect = null;
            rightEdgeEffect = null;
        }

        super.setOverScrollMode(overScrollMode);
    }

    public TextUtils.TruncateAt getEllipsize() {
        return ellipsize;
    }

    public void setEllipsize(TextUtils.TruncateAt ellipsize) {
        if (this.ellipsize != ellipsize) {
            this.ellipsize = ellipsize;

            remakeLayout();
            invalidate();
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.scrollTo(scrollX, scrollY);

        if(!flingScrollerX.isFinished() && clampedX) {
            flingScrollerX.springBack(scrollX, scrollY, 0, getScrollRange(), 0, 0);
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged(); //TODO
    }

    private int getPositionFromTouch(float x) {
        return getPositionFromCoordinates((int) (getScrollX() - (itemWidth + dividerSize) * (sideItems + .5f) + x));
    }

    private void computeScrollX() {
        OverScroller scroller = flingScrollerX;
        if(scroller.isFinished()) {
            scroller = adjustScrollerX;
            if(scroller.isFinished()) {
                return;
            }
        }

        if(scroller.computeScrollOffset()) {

            int currentScrollerX = scroller.getCurrX();
            if(previousScrollerX == Integer.MIN_VALUE) {
                previousScrollerX = scroller.getStartX();
            }

            int range = getScrollRange();
            if(previousScrollerX >= 0 && currentScrollerX < 0) {
                leftEdgeEffect.onAbsorb((int) scroller.getCurrVelocity());
            } else if(previousScrollerX <= range && currentScrollerX > range) {
                rightEdgeEffect.onAbsorb((int) scroller.getCurrVelocity());
            }

            overScrollBy(currentScrollerX - previousScrollerX, 0, previousScrollerX, getScrollY(),
                    getScrollRange(), 0, overscrollDistance, 0, false);
            previousScrollerX = currentScrollerX;

            if(scroller.isFinished()) {
                onScrollerFinishedX(scroller);
            }

            postInvalidate();
//            postInvalidateOnAnimation(); // TODO
        }
    }

    private void flingX(int velocityX) {

        previousScrollerX = Integer.MIN_VALUE;
        flingScrollerX.fling(getScrollX(), getScrollY(), -velocityX, 0, 0,
                (int) (itemWidth + dividerSize) * (values.length - 1), 0, 0, getWidth() / 2, 0);

        invalidate();
    }

    private void adjustToNearestItemX() {

        int x = getScrollX();
        int item = Math.round(x / (itemWidth + dividerSize * 1f));

        if(item < 0) {
            item = 0;
        } else if(item > values.length) {
            item = values.length;
        }

        selectedItem = item;

        int itemX = (itemWidth + (int) dividerSize) * item;

        int deltaX = itemX - x;

        previousScrollerX = Integer.MIN_VALUE;
        adjustScrollerX.startScroll(x, 0, deltaX, 0, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
        invalidate();
    }

    private void calculateItemSize(int w, int h) {

        int items = sideItems * 2 + 1;
        int totalPadding = ((int) dividerSize * (items - 1));
        itemWidth = (w - totalPadding) / items;

        itemClipBounds = new RectF(0, 0, itemWidth, h);
        itemClipBoundsOffset = new RectF(itemClipBounds);

        scrollToItem(selectedItem);

        remakeLayout();
        startMarqueeIfNeeded();

    }

    private void onScrollerFinishedX(OverScroller scroller) {
        if(scroller == flingScrollerX) {
            finishScrolling();
        }
    }

    private void finishScrolling() {

        adjustToNearestItemX();
        scrollingX = false;
        startMarqueeIfNeeded();
        // post to the UI Thread to avoid potential interference with the OpenGL Thread
        if (onItemSelected != null) {
            post(new Runnable() {
                @Override
                public void run() {
                    onItemSelected.onItemSelected(getPositionFromCoordinates(getScrollX()));
                }
            });
        }
    }

    private void startMarqueeIfNeeded() {

        stopMarqueeIfNeeded();

        int item = getSelectedItem();

        if (layouts != null && layouts.length > item) {
            Layout layout = layouts[item];
            if (ellipsize == TextUtils.TruncateAt.MARQUEE
                    && itemWidth < layout.getLineWidth(0)) {
                marquee = new Marquee(this, layout, isRtl(values[item]));
                marquee.start(marqueeRepeatLimit);
            }
        }

    }

    private void stopMarqueeIfNeeded() {

        if (marquee != null) {
            marquee.stop();
            marquee = null;
        }

    }

    private int getPositionOnScreen(float x) {
        return (int) (x / (itemWidth + dividerSize));
    }

    private void smoothScrollBy(int i) {
        int deltaMoveX = (itemWidth + (int) dividerSize) * i;
        deltaMoveX = getRelativeInBound(deltaMoveX);

        previousScrollerX = Integer.MIN_VALUE;
        flingScrollerX.startScroll(getScrollX(), 0, deltaMoveX, 0);
        stopMarqueeIfNeeded();
        invalidate();
    }

    /**
     * Calculates color for specific position on time picker
     * @param scrollX
     * @return
     */
    private int getColor(int scrollX, int position) {
        int itemWithPadding = (int) (itemWidth + dividerSize);
        float proportion = Math.abs(((1f * scrollX % itemWithPadding) / 2) / (itemWithPadding / 2f));
        if(proportion > .5) {
            proportion = (proportion - .5f);
        } else {
            proportion = .5f - proportion;
        }
        proportion *= 2;

        int defaultColor;
        int selectedColor;

        if(pressedItem == position) {
            defaultColor = textColor.getColorForState(new int[] { android.R.attr.state_pressed }, textColor.getDefaultColor());
            selectedColor = textColor.getColorForState(new int[] { android.R.attr.state_pressed, android.R.attr.state_selected }, defaultColor);
        } else {
            defaultColor = textColor.getDefaultColor();
            selectedColor = textColor.getColorForState(new int[] { android.R.attr.state_selected }, defaultColor);
        }
        return (Integer) new ArgbEvaluator().evaluate(proportion, selectedColor, defaultColor);
    }

    /**
     * Sets text size for items
     * @param size New item text size in px.
     */
    private void setTextSize(float size) {
        if(size != textPaint.getTextSize()) {
            textPaint.setTextSize(size);

            requestLayout();
            invalidate();
        }
    }

    /**
     * Calculates item from x coordinate position.
     * @param x Scroll position to calculate.
     * @return Selected item from scrolling position in {param x}
     */
    private int getPositionFromCoordinates(int x) {
        return Math.round(x / (itemWidth + dividerSize));
    }

    /**
     * Scrolls to specified item.
     * @param index Index of an item to scroll to
     */
    private void scrollToItem(int index) {
        scrollTo((itemWidth + (int) dividerSize) * index, 0);
        // invalidate() not needed because scrollTo() already invalidates the view
    }

    /**
     * Calculates relative horizontal scroll position to be within our scroll bounds.
     * {@link com.wefika.horizontalpicker.HorizontalPicker#getInBoundsX(int)}
     * @param x Relative scroll position to calculate
     * @return Current scroll position + {param x} if is within our scroll bounds, otherwise it
     * will return min/max scroll position.
     */
    private int getRelativeInBound(int x) {
        int scrollX = getScrollX();
        return getInBoundsX(scrollX + x) - scrollX;
    }

    /**
     * Calculates x scroll position that is still in range of view scroller
     * @param x Scroll position to calculate.
     * @return {param x} if is within bounds of over scroller, otherwise it will return min/max
     * value of scoll position.
     */
    private int getInBoundsX(int x) {

        if(x < 0) {
            x = 0;
        } else if(x > ((itemWidth + (int) dividerSize) * (values.length - 1))) {
            x = ((itemWidth + (int) dividerSize) * (values.length - 1));
        }
        return x;
    }

    private int getScrollRange() {
        int scrollRange = 0;
        if(values != null && values.length != 0) {
            scrollRange = Math.max(0, ((itemWidth + (int) dividerSize) * (values.length - 1)));
        }
        return scrollRange;
    }

    public interface OnItemSelected {

        public void onItemSelected(int index);

    }

    public interface OnItemClicked {

        public void onItemClicked(int index);

    }

    private static final class Marquee extends Handler {
        // TODO: Add an option to configure this
        private static final float MARQUEE_DELTA_MAX = 0.07f;
        private static final int MARQUEE_DELAY = 1200;
        private static final int MARQUEE_RESTART_DELAY = 1200;
        private static final int MARQUEE_RESOLUTION = 1000 / 30;
        private static final int MARQUEE_PIXELS_PER_SECOND = 30;

        private static final byte MARQUEE_STOPPED = 0x0;
        private static final byte MARQUEE_STARTING = 0x1;
        private static final byte MARQUEE_RUNNING = 0x2;

        private static final int MESSAGE_START = 0x1;
        private static final int MESSAGE_TICK = 0x2;
        private static final int MESSAGE_RESTART = 0x3;

        private final WeakReference<HorizontalPicker> mView;
        private final WeakReference<Layout> mLayout;

        private byte mStatus = MARQUEE_STOPPED;
        private final float mScrollUnit;
        private float mMaxScroll;
        private float mMaxFadeScroll;
        private float mGhostStart;
        private float mGhostOffset;
        private float mFadeStop;
        private int mRepeatLimit;

        private float mScroll;

        private boolean mRtl;

        Marquee(HorizontalPicker v, Layout l, boolean rtl) {
            final float density = v.getContext().getResources().getDisplayMetrics().density;
            float scrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
            if (rtl) {
                mScrollUnit = -scrollUnit;
            } else {
                mScrollUnit = scrollUnit;
            }

            mView = new WeakReference<HorizontalPicker>(v);
            mLayout = new WeakReference<Layout>(l);
            mRtl = rtl;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_START:
                    mStatus = MARQUEE_RUNNING;
                    tick();
                    break;
                case MESSAGE_TICK:
                    tick();
                    break;
                case MESSAGE_RESTART:
                    if (mStatus == MARQUEE_RUNNING) {
                        if (mRepeatLimit >= 0) {
                            mRepeatLimit--;
                        }
                        start(mRepeatLimit);
                    }
                    break;
            }
        }

        void tick() {
            if (mStatus != MARQUEE_RUNNING) {
                return;
            }

            removeMessages(MESSAGE_TICK);

            final HorizontalPicker view = mView.get();
            final Layout layout = mLayout.get();
            if (view != null && layout != null && (view.isFocused() || view.isSelected())) {
                mScroll += mScrollUnit;
                if (Math.abs(mScroll) > mMaxScroll) {
                    mScroll = mMaxScroll;
                    if (mRtl) {
                        mScroll *= -1;
                    }
                    sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
                } else {
                    sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
                }
                view.invalidate();
            }
        }

        void stop() {
            mStatus = MARQUEE_STOPPED;
            removeMessages(MESSAGE_START);
            removeMessages(MESSAGE_RESTART);
            removeMessages(MESSAGE_TICK);
            resetScroll();
        }

        private void resetScroll() {
            mScroll = 0.0f;
            final HorizontalPicker view = mView.get();
            if (view != null) view.invalidate();
        }

        void start(int repeatLimit) {
            if (repeatLimit == 0) {
                stop();
                return;
            }
            mRepeatLimit = repeatLimit;
            final HorizontalPicker view = mView.get();
            final Layout layout = mLayout.get();
            if (view != null && layout != null) {
                mStatus = MARQUEE_STARTING;
                mScroll = 0.0f;
                final int textWidth = view.itemWidth;
                final float lineWidth = layout.getLineWidth(0);
                final float gap = textWidth / 3.0f;
                mGhostStart = lineWidth - textWidth + gap;
                mMaxScroll = mGhostStart + textWidth;
                mGhostOffset = lineWidth + gap;
                mFadeStop = lineWidth + textWidth / 6.0f;
                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;

                if (mRtl) {
                    mGhostOffset *= -1;
                }

                view.invalidate();
                sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
            }
        }

        float getGhostOffset() {
            return mGhostOffset;
        }

        float getScroll() {
            return mScroll;
        }

        float getMaxFadeScroll() {
            return mMaxFadeScroll;
        }

        boolean shouldDrawLeftFade() {
            return mScroll <= mFadeStop;
        }

        boolean shouldDrawGhost() {
            return mStatus == MARQUEE_RUNNING && Math.abs(mScroll) > mGhostStart;
        }

        boolean isRunning() {
            return mStatus == MARQUEE_RUNNING;
        }

        boolean isStopped() {
            return mStatus == MARQUEE_STOPPED;
        }
    }

    public static class SavedState extends BaseSavedState {

        private int mSelItem;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            mSelItem = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeInt(mSelItem);
        }

        @Override
        public String toString() {
            return  "HorizontalPicker.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " selItem=" + mSelItem
                    + "}";
        }

        @SuppressWarnings("hiding")
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    private static class PickerTouchHelper extends ExploreByTouchHelper {

        private HorizontalPicker mPicker;

        public PickerTouchHelper(HorizontalPicker picker) {
            super(picker);
            mPicker = picker;
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {

            float itemWidth = mPicker.itemWidth + mPicker.dividerSize;
            float position = mPicker.getScrollX() + x - itemWidth * mPicker.sideItems;

            float item = position / itemWidth;

            if (item < 0 || item > mPicker.values.length) {
                return INVALID_ID;
            }

            return (int) item;

        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {

            float itemWidth = mPicker.itemWidth + mPicker.dividerSize;
            float position = mPicker.getScrollX() - itemWidth * mPicker.sideItems;

            int first = (int) (position / itemWidth);

            int items = mPicker.sideItems * 2 + 1;

            if (position % itemWidth != 0) { // if start next item is starting to appear on screen
                items++;
            }

            if (first < 0) {
                items += first;
                first = 0;
            } else if (first + items > mPicker.values.length) {
                items = mPicker.values.length - first;
            }

            for (int i = 0; i < items; i++) {
                virtualViewIds.add(first + i);
            }

        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            event.setContentDescription(mPicker.values[virtualViewId]);
        }

        @Override
        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {

            float itemWidth = mPicker.itemWidth + mPicker.dividerSize;
            float scrollOffset = mPicker.getScrollX() - itemWidth * mPicker.sideItems;

            int left = (int) (virtualViewId * itemWidth - scrollOffset);
            int right = left + mPicker.itemWidth;

            node.setContentDescription(mPicker.values[virtualViewId]);
            node.setBoundsInParent(new Rect(left, 0, right, mPicker.getHeight()));
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);

        }

        @Override
        protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
            return false;
        }

    }

}


/* Copyright 2014 Kevin Ormbrek
 *
 * 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 org.robotframework.remoteserver.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

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

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.server.XmlRpcHandlerMapping;
import org.apache.xmlrpc.webserver.XmlRpcServlet;
import org.apache.xmlrpc.webserver.XmlRpcServletServer;
import org.robotframework.remoteserver.context.RemoteServerContext;
import org.robotframework.remoteserver.library.DefaultRemoteLibraryFactory;
import org.robotframework.remoteserver.library.RemoteLibrary;
import org.robotframework.remoteserver.library.RemoteLibraryFactory;
import org.robotframework.remoteserver.xmlrpc.ReflectiveHandlerMapping;
import org.robotframework.remoteserver.xmlrpc.TypeFactory;

/**
 * This servlet can be used with servlet containers such as GlassFish,
 * WebSphere, Tiny Java Web Server, etc. The paths for the library mapping are
 * relative to the servlet path. When a remote stop is performed,
 * System.exit(0) is executed.
 */
public class RemoteServerServlet extends XmlRpcServlet implements RemoteServerContext {
    private static final long serialVersionUID = -7981676271855172976L;
    private static final ThreadLocal<HttpServletRequest> request = new ThreadLocal<HttpServletRequest>();
    private static final ThreadLocal<RemoteLibrary> currLibrary = new ThreadLocal<RemoteLibrary>();
    private Map<String, RemoteLibrary> libraryMap = new ConcurrentHashMap<String, RemoteLibrary>();
    private boolean allowStop = true;

    @Override
    public RemoteLibrary putLibrary(String path, Object library) {
        checkPath(path);
        RemoteLibraryFactory libraryFactory = createLibraryFactory();
        RemoteLibrary remoteLibrary = libraryFactory.createRemoteLibrary(library);
        return libraryMap.put(path, remoteLibrary);
    }

    @Override
    public RemoteLibrary removeLibrary(String path) {
        return libraryMap.remove(path);
    }

    @Override
    public Map<String, RemoteLibrary> getLibraryMap() {
        return new HashMap<String, RemoteLibrary>(libraryMap);
    }

    @Override
    public boolean getAllowStop() {
        return allowStop;
    }

    @Override
    public void setAllowStop(boolean allowed) {
        allowStop = allowed;
    }

    @Override
    protected XmlRpcServletServer newXmlRpcServer(ServletConfig pConfig) throws XmlRpcException {
        XmlRpcServletServer server = new XmlRpcServletServer();
        server.setTypeFactory(new TypeFactory(this.getXmlRpcServletServer()));
        return server;
    }

    @Override
    protected XmlRpcHandlerMapping newXmlRpcHandlerMapping() throws XmlRpcException {
        ReflectiveHandlerMapping map = new ReflectiveHandlerMapping();
        map.setRequestProcessorFactoryFactory(new RemoteServerRequestProcessorFactoryFactory(this));
        map.addHandler("keywords", ServerMethods.class);
        map.removePrefixes();
        return map;
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        request.set(req);
        try {
            super.service(req, resp);
        } finally {
            request.remove();
        }
    }

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /*
         * when the client is Jython 2.5.x (old xmlrpclib using HTTP/1.0), the
         * server's sockets got stuck in FIN_WAIT_2 for some time, eventually
         * hitting the limit of open sockets on some Windows systems. adding
         * this header gets the web server to close the socket.
         */
        String path = req.getPathInfo() == null ? req.getServletPath() : req.getPathInfo();
        path = cleanPath(path);
        if (libraryMap.containsKey(path)) {
            currLibrary.set(libraryMap.get(path));
            if ("HTTP/1.0".equals(req.getProtocol()))
                resp.addHeader("Connection", "close");
            super.doPost(req, resp);
        } else {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("No library mapped to %s", path));
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        String body = getPage();
        resp.setContentLength(body.length());
        PrintWriter out = resp.getWriter();
        out.print(body);
    }

    @Override
    public HttpServletRequest getRequest() {
        return request.get();
    }

    private String getPage() {
        Map<String, RemoteLibrary> map = new TreeMap<String, RemoteLibrary>(getLibraryMap());
        StringBuilder sb = new StringBuilder();
        sb.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">"
                + "<TITLE>jrobotremoteserver</TITLE>" + "
jrobotremoteserver serving:
"
                + "

Path	Library
");
        if (map.isEmpty()) {
            sb.append("
No libraries mapped
");
        } else {
            for (String path : map.keySet()) {
                sb.append("
");
                sb.append(path.toString());
                sb.append("	");
                sb.append(StringEscapeUtils.escapeHtml(map.get(path).getName()));
                sb.append("
");
            }
        }
        sb.append("
");
        return sb.toString();
    }

    /**
     * Returns the library to use in the current context. This should only be
     * used while a request is being processed and only on the same thread that
     * is handling the request.
     *
     * @return the library to use in the current context
     */
    public RemoteLibrary getLibrary() {
        return currLibrary.get();
    }

    protected RemoteLibraryFactory createLibraryFactory() {
        return new DefaultRemoteLibraryFactory();
    }

    /**
     * Cleans up the path of an incoming request. Repeating /s are reduced to
     * one /. Trailing /s are removed. A null or empty path is
     * converted to /.
     *
     * @param path
     *            the path the client requested
     * @return cleaned up path
     */
    private static String cleanPath(String path) {
        path = path == null ? "/" : path;
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        path = path.replaceAll("/+", "/");
        if (path.length() > 1 && path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }

    private static void checkPath(String path) {
        if (path == null || !path.startsWith("/")) {
            throw new IllegalPathException(String.format("Path [%s] does not start with a /.", path));
        } else if (path.contains("//")) {
            throw new IllegalPathException(String.format("Path [%s] contains repeated forward slashes.", path));
        } else if (!path.equals("/") && path.endsWith("/")) {
            throw new IllegalPathException(String.format("Path [%s] ends with a /.", path));
        } else if (!path.matches("[a-zA-Z0-9-._~/]+")) {
            throw new IllegalPathException(
                    String.format(
                            "Path [%s] contains disallowed characters (must contain only alphanumeric or any of these: -._~/).",
                            path));
        }
    }

}


package com.example;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;

import org.apache.commons.io.IOUtils;
import org.robotframework.javalib.library.AnnotationLibrary;
import org.robotframework.remoteserver.RemoteServer;

public class MyRemoteLibrary extends AnnotationLibrary {
    public MyRemoteLibrary() {
        // tell AnnotationLibrary where to find the keywords
        super("com/example/keywords/*.class");
    }

    @Override
    public String getKeywordDocumentation(String keywordName) {
        if (keywordName.equals("__intro__"))
            return getIntro();
        return super.getKeywordDocumentation(keywordName);
    }

    /* Starts jrobotremoteserver with an example library and returns. The application will shutdown when all of the
     * web server's threads exit.
     */
    public static void main(String[] args) throws Exception {
        RemoteServer.configureLogging();
        RemoteServer server = new RemoteServer();
        server.addLibrary(MyRemoteLibrary.class, 8270);
        server.start();
    }

    // The introduction is stored in a text file resource because it is easier to edit than String constants.
    private String getIntro() {
        try {
            InputStream introStream = MyRemoteLibrary.class.getResourceAsStream("__intro__.txt");
            StringWriter writer = new StringWriter();
            IOUtils.copy(introStream, writer, Charset.defaultCharset());
            return writer.toString();
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}


/* Copyright 2014 Kevin Ormbrek
 *
 * 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 org.robotframework.remoteserver.servlet;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.robotframework.javalib.util.StdStreamRedirecter;

/**
 * Contains the XML-RPC methods that implement the remote library interface.
 *
 * @author David Luu
 *
 */
public class ServerMethods {

    private Log log;
    private RemoteServerServlet servlet;
    private static List<String> genericExceptions = Arrays.asList(new String[] { "AssertionError",
            "AssertionFailedError", "Exception", "Error", "RuntimeError", "RuntimeException", "DataError",
            "TimeoutError", "RemoteError" });
    String[] logLevelPrefixes = new String[] { "*TRACE*", "*DEBUG*", "*INFO*", "*HTML*", "*WARN*" };

    public ServerMethods(RemoteServerServlet servlet) {
        log = LogFactory.getLog(ServerMethods.class);
        this.servlet = servlet;
    }

    /**
     * Get an array containing the names of the keywords that the library
     * implements.
     *
     * @return String array containing keyword names in the library
     */
    public String[] get_keyword_names() {
        try {
            String[] names = servlet.getLibrary().getKeywordNames();
            if (names == null || names.length == 0)
                throw new RuntimeException("No keywords found in the test library");
            String[] newNames = Arrays.copyOf(names, names.length + 1);
            newNames[names.length] = "stop_remote_server";
            return newNames;
        } catch (Throwable e) {
            log.warn("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Run the given keyword and return the results.
     *
     * @param keyword
     *            keyword to run
     * @param args
     *            arguments packed in an array to pass to the keyword method
     * @param kwargs
     *            keyword arguments to pass to the keyword method
     * @return remote result Map containing the execution results
     */
    public Map<String, Object> run_keyword(String keyword, Object[] args, Map<String, Object> kwargs) {
        Map<String, Object> result = new HashMap<String, Object>();
        StdStreamRedirecter redirector = new StdStreamRedirecter();
        redirector.redirectStdStreams();
        try {
            result.put("status", "PASS");
            Object retObj = "";
            if (keyword.equalsIgnoreCase("stop_remote_server")) {
                retObj = stopRemoteServer();
            } else {
                try {
                    retObj = servlet.getLibrary().runKeyword(keyword, args, kwargs);
                } catch (Exception e) {
                    if (illegalArgumentIn(e)) {
                        for (int i = 0; i < args.length; i++)
                            args[i] = arraysToLists(args[i]);
                        retObj = servlet.getLibrary().runKeyword(keyword, args, kwargs);
                    } else {
                        throw (e);
                    }
                }
            }
            if (retObj != null && !retObj.equals("")) {
                result.put("return", retObj);
            }
        } catch (Throwable e) {
            result.put("status", "FAIL");
            Throwable thrown = e.getCause() == null ? e : e.getCause();
            result.put("error", getError(thrown));
            result.put("traceback", ExceptionUtils.getStackTrace(thrown));
            boolean continuable = isFlagSet("ROBOT_CONTINUE_ON_FAILURE", thrown);
            if (continuable) {
                result.put("continuable", true);
            }
            boolean fatal = isFlagSet("ROBOT_EXIT_ON_FAILURE", thrown);
            if (fatal) {
                result.put("fatal", true);
            }
        } finally {
            String stdOut = StringUtils.defaultString(redirector.getStdOutAsString());
            String stdErr = StringUtils.defaultString(redirector.getStdErrAsString());
            if (!stdOut.isEmpty() || !stdErr.isEmpty()) {
                StringBuilder output = new StringBuilder(stdOut);
                if (!stdOut.isEmpty() && !stdErr.isEmpty()) {
                    if (!stdOut.endsWith("\n")) {
                        output.append("\n");
                    }
                    boolean addLevel = true;
                    for (String prefix : logLevelPrefixes) {
                        if (stdErr.startsWith(prefix)) {
                            addLevel = false;
                            break;
                        }
                    }
                    if (addLevel) {
                        output.append("*INFO*");
                    }
                }
                result.put("output", output.append(stdErr).toString());
            }
            redirector.resetStdStreams();
        }
        return result;
    }

    /**
     * Run the given keyword and return the results.
     *
     * @param keyword
     *            keyword to run
     * @param args
     *            arguments packed in an array to pass to the keyword method
     * @return remote result Map containing the execution results
     */
    public Map<String, Object> run_keyword(String keyword, Object[] args) {
        return run_keyword(keyword, args, null);
    }

    /**
     * Get an array of argument specifications for the given keyword.
     *
     * @param keyword
     *            The keyword to lookup.
     * @return A string array of argument specifications for the given keyword.
     */
    public String[] get_keyword_arguments(String keyword) {
        if (keyword.equalsIgnoreCase("stop_remote_server")) {
            return new String[0];
        }
        try {
            String[] args = servlet.getLibrary().getKeywordArguments(keyword);
            return args == null ? new String[0] : args;
        } catch (Throwable e) {
            log.warn("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Get documentation for given keyword.
     *
     * @param keyword
     *            The keyword to get documentation for.
     * @return A documentation string for the given keyword.
     */
    public String get_keyword_documentation(String keyword) {
        if (keyword.equalsIgnoreCase("stop_remote_server")) {
            return "Stops the remote server.\n\nThe server may be configured so that users cannot stop it.";
        }
        try {
            String doc = servlet.getLibrary().getKeywordDocumentation(keyword);
            return doc == null ? "" : doc;
        } catch (Throwable e) {
            log.warn("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Stops the remote server if it is configured to allow that.
     *
     * @return remote result Map containing the execution results
     */
    public Map<String, Object> stop_remote_server() {
        return run_keyword("stop_remote_server", null);
    }

    protected boolean stopRemoteServer() throws Exception {
        if (servlet.getAllowStop()) {
            System.out.print("Robot Framework remote server stopping");
            new Thread("remote-server-stopper") {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    System.exit(0);
                }
            }.start();
        } else {
            System.out.print("This Robot Framework remote server does not allow stopping");
        }
        return true;
    }

    private String getError(Throwable thrown) {
        String simpleName = thrown.getClass().getSimpleName();
        boolean suppressName = isFlagSet("ROBOT_SUPPRESS_NAME", thrown);
        if (genericExceptions.contains(simpleName) || suppressName) {
            return StringUtils.defaultIfEmpty(thrown.getMessage(), simpleName);
        } else {
            return String.format("%s: %s", thrown.getClass().getName(), thrown.getMessage());
        }
    }

    private boolean isFlagSet(String name, Throwable thrown) {
        boolean flag = false;
        try {
            flag = thrown.getClass().getField(name).getBoolean(thrown);
        } catch (Exception e) {
            // ignore
        }
        return flag;
    }

    protected Object arraysToLists(Object arg) {
        if (arg instanceof Object[]) {
            Object[] array = (Object[]) arg;
            List<Object> list = Arrays.asList(array);
            for (int i = 0; i < list.size(); i++)
                list.set(i, arraysToLists(list.get(i)));
            return list;
        } else if (arg instanceof Map<?, ?>) {
            Map<?, ?> oldMap = (Map<?, ?>) arg;
            Map<Object, Object> newMap = new HashMap<Object, Object>();
            for (Object key : oldMap.keySet())
                newMap.put(key, arraysToLists(oldMap.get(key)));
            return newMap;
        } else
            return arg;
    }

    private boolean illegalArgumentIn(Throwable t) {
        if (t.getClass().equals(IllegalArgumentException.class)) {
            return true;
        }
        Throwable inner = t;
        while (inner.getCause() != null) {
            inner = inner.getCause();
            if (inner.getClass().equals(IllegalArgumentException.class)) {
                return true;
            }
        }
        return false;
    }
}


/* Copyright 2014 Kevin Ormbrek
 *
 * 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 org.robotframework.remoteserver;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.robotframework.remoteserver.cli.CommandLineHelper;
import org.robotframework.remoteserver.library.RemoteLibrary;
import org.robotframework.remoteserver.logging.Jetty2Log4J;
import org.robotframework.remoteserver.servlet.IllegalPathException;
import org.robotframework.remoteserver.servlet.RemoteServerServlet;

/**
 * Remote server for Robot Framework implemented in Java. Takes one or more test
 * libraries and exposes their methods via XML-RPC using an embedded web server.
 * To use a different web server, use
 * {@link org.robotframework.remoteserver.servlet.RemoteServerServlet} instead.
 *
 * @see jrobotremoteserver
 *      wiki
 * @see Remote
 *      Library wiki page
 * @see User
 *      Guide for Robot Framework
 * @see XML-RPC
 *      Specification
 */
public class RemoteServer {
    private static Log log = LogFactory.getLog(RemoteServer.class);
    protected Server server = new Server();
    private RemoteServerServlet servlet = new RemoteServerServlet();
    private SelectChannelConnector connector = new SelectChannelConnector();

    public RemoteServer() {
        connector.setName("jrobotremoteserver");
        server.setConnectors(new Connector[] { connector });
        ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/", false, false);
        servletContextHandler.addServlet(new ServletHolder(servlet), "/");
    }

    /**
     * Returns the actual port the server is listening on.
     *
     * @return The actual port the server's connector is listening on or -1 if
     *         it has not been opened, or -2 if it has been closed.
     */
    public Integer getLocalPort() {
        return connector.getLocalPort();
    }

    /**
     * Sets the port to listen on.
     *
     * @param port
     *            The port to listen on for connections or 0 if any available
     *            port may be used. Defaults port is 0.
     */
    public void setPort(int port) {
        connector.setPort(port);
    }

    /**
     * Returns true if this server allows remote stopping.
     *
     * @return true if this server allows remote stopping
     */
    public boolean getAllowStop() {
        return servlet.getAllowStop();
    }

    /**
     * Allow or disallow stopping the server remotely.
     *
     * @param allowed
     *            true to allow stopping the server remotely
     */
    public void setAllowStop(boolean allowed) {
        servlet.setAllowStop(allowed);
    }

    /**
     * Returns the hostname set with {@link #setHost(String)}.
     *
     * @return the hostname set with {@link #setHost(String)}
     */
    public String getHost() {
        return connector.getHost();
    }

    /**
     * Set the hostname of the interface to bind to. Usually not needed and
     * determined automatically. For exotic network configuration, network with
     * VPN, specifying the host might be necessary.
     *
     * @param hostName
     *            the hostname or address representing the interface to which
     *            all connectors will bind, or null for all interfaces.
     */
    public void setHost(String hostName) {
        connector.setHost(hostName);
    }

    /**
     * Main method for command line usage.
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        configureLogging();
        CommandLineHelper helper = new CommandLineHelper(args);
        if (helper.getHelpRequested()) {
            System.out.print(helper.getUsage());
            System.exit(0);
        }
        RemoteServer remoteServer = new RemoteServer();
        String error = helper.getError();
        if (error == null) {
            try {
                for (String path : helper.getLibraryMap().keySet())
                    remoteServer.putLibrary(path, helper.getLibraryMap().get(path));
            } catch (IllegalPathException e) {
                error = e.getMessage();
            }
        }
        if (error != null) {
            System.out.println("Error: " + error);
            System.out.println();
            System.out.println(helper.getUsage());
            System.exit(1);
        }
        remoteServer.setPort(helper.getPort());
        remoteServer.setAllowStop(helper.getAllowStop());
        remoteServer.setHost(helper.getHost());
        remoteServer.start();
    }

    /**
     * Map the given test library to the specified path. Paths must:
     *

     *
start with a /

     *
contain only alphanumeric characters or any of these: / - . _ ~

     *
not end in a /

     *
not contain a repeating sequence of /s

     *

     *
     * Example: putLibrary("/myLib", new MyLibrary());
     *
     * @param library
     *            instance of the test library
     * @param path
     *            path to map the test library to
     * @return the previous library mapped to the path, or null if there was no
     *         mapping for the path
     */
    public RemoteLibrary putLibrary(String path, Object library) {
        RemoteLibrary oldLibrary = servlet.putLibrary(path, library);
        String name = servlet.getLibraryMap().get(path).getName();
        log.info(String.format("Mapped path %s to library %s.", path, name));
        return oldLibrary;
    }

    /**
     * Removes the library mapped to the given path if the mapping exists.
     *
     * @param path
     *            path for the library whose mapping is to be removed
     * @return the previous library associated with the path, or null if there
     *         was no mapping for the path.
     */
    public RemoteLibrary removeLibrary(String path) {
        return servlet.removeLibrary(path);
    }

    /**
     * Gets a copy of the current library map. Keys in the map are the paths and
     * the values are {@link RemoteLibrary} wrappers of the libraries being
     * served.
     *
     * @return a copy of the current library map
     */
    public Map<String, RemoteLibrary> getLibraryMap() {
        return servlet.getLibraryMap();
    }

    /**
     * This has been deprecated. Please use {@link #putLibrary} and
     * {@link #setPort} instead.
     *
     * Map the given test library to / and sets the port for the server. The
     * server must be stopped when calling this.
     *
     * @param className
     *            class name of the test library
     * @param port
     *            port for the server to listen on
     */
    @Deprecated
    public void addLibrary(String className, int port) {
        Class<?> clazz;
        try {
            clazz = Class.forName(className);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        addLibrary(clazz, port);
    }

    /**
     * This has been deprecated. Please use {@link #putLibrary} and
     * {@link #setPort} instead.
     *
     * Map the given test library to / and sets the port for the server. The
     * server must be stopped when calling this.
     *
     * @param clazz
     *            class of the test library
     * @param port
     *            port for the server to listen on
     */
    @Deprecated
    public void addLibrary(Class<?> clazz, int port) {
        if (!server.isStopped()) // old behavior
            throw new IllegalStateException("Cannot add a library once the server is started");
        if (connector.getPort() != 0 && connector.getPort() != port) {
            throw new RuntimeException(
                    "Serving on multiple ports is no longer supported. Please use putLibrary with different paths instead.");
        }
        if (servlet.getLibraryMap().keySet().contains("/")) {
            throw new RuntimeException("A library has already been mapped to /.");
        }
        Object library;
        try {
            library = clazz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        setPort(port);
        putLibrary("/", library);
    }

    /**
     * A non-blocking method for stopping the remote server that allows requests
     * to complete within the given timeout before shutting down the server. New
     * connections will not be accepted after calling this.
     *
     * @param timeoutMS
     *            the milliseconds to wait for existing request to complete
     *            before stopping the server
     */
    public void stop(int timeoutMS) throws Exception {
        log.info("Robot Framework remote server stopping");
        if (timeoutMS > 0) {
            server.setGracefulShutdown(timeoutMS);
            Thread stopper = new Thread() {
                @Override
                public void run() {
                    try {
                        server.stop();
                    } catch (Throwable e) {
                        log.error(String.format("Failed to stop the server: %s", e.getMessage()), e);
                    }
                }
            };
            stopper.start();
        } else {
            server.stop();
        }
    }

    /**
     * Stops the remote server immediately.
     *
     * @throws Exception
     */
    public void stop() throws Exception {
        stop(0);
    }

    /**
     * Starts the remote server. Add test libraries first before calling this.
     *
     * @throws Exception
     */
    public void start() throws Exception {
        log.info("Robot Framework remote server starting");
        server.start();
        log.info(String.format("Robot Framework remote server started on port %d.", getLocalPort()));
    }

    /**
     * Configures logging systems used by <tt>RemoteServer</tt> and its
     * dependencies. Specifically,
     *

     *
Configure Log4J to log to the console

     *
Set Log4J's log level to INFO

     *
Redirect the Jetty's logging to Log4J

     *
Set Jakarta Commons Logging to log to Log4J

     *

     * This is convenient if you do not want to configure the logging yourself.
     * This will only affect future instances of
     * {@link org.eclipse.jetty.util.log.Logger} and
     * {@link org.apache.commons.logging.Log}, so this should be called as early
     * as possible.
     */
    public static void configureLogging() {
        Logger root = Logger.getRootLogger();
        root.removeAllAppenders();
        BasicConfigurator.configure();
        root.setLevel(Level.INFO);
        org.eclipse.jetty.util.log.Log.setLog(new Jetty2Log4J());
        LogFactory.releaseAll();
        LogFactory.getFactory().setAttribute("org.apache.commons.logging.Log",
                "org.apache.commons.logging.impl.Log4JLogger");
        log = LogFactory.getLog(RemoteServer.class);
    }

}


/* Copyright 2014 Kevin Ormbrek
 *
 * 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 org.robotframework.remoteserver.xmlrpc;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.server.AbstractReflectiveHandlerMapping;

public class ReflectiveHandlerMapping extends AbstractReflectiveHandlerMapping {

    /**
     * Removes the prefixes from all keys in this handler mapping assuming a String was used as the key and period was
     * used as a separator. Example: AccountsReceivable.Billing.getInvoice -> getInvoice
     */
    @SuppressWarnings("unchecked")
    public void removePrefixes() {
	Map<String, Object> newHandlerMap = new HashMap<String, Object>();
	for (Entry<String, Object> entry : (Set<Entry<String, Object>>) this.handlerMap.entrySet()) {
	    String newKey = (String) entry.getKey();
	    if (entry.getKey() instanceof String) {
		String key = (String) entry.getKey();
		if (key.contains(".")) {
		    newKey = key.substring(key.lastIndexOf(".") + 1);
		}
	    }
	    newHandlerMap.put(newKey, entry.getValue());
	}
	this.handlerMap = newHandlerMap;
    }

    /**
     * Adds handlers for the given object to the mapping. The handlers are build by invoking
     * {@link #registerPublicMethods(String, Class)}.
     *
     * @param pKey
     *            The class key, which is passed to {@link #registerPublicMethods(String, Class)}.
     * @param pClass
     *            Class, which is responsible for handling the request.
     */
    public void addHandler(String pKey, Class<?> pClass) throws XmlRpcException {
	registerPublicMethods(pKey, pClass);
    }
}