/*
 * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/lti2/LTI2Util.java $
 * $Id: LTI2Util.java 134448 2014-02-12 18:32:12Z csev@umich.edu $
 *
 * Copyright (c) 2013 IMS GLobal Learning Consortium
 *
 * 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.imsglobal.lti2;

import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;

import org.imsglobal.lti.BasicLTIUtil;
import org.imsglobal.lti2.objects.consumer.ServiceOffered;
import org.imsglobal.lti2.objects.consumer.StandardServices;
import org.imsglobal.lti2.objects.consumer.ToolConsumer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

public class LTI2Util {

	// We use the built-in Java logger because this code needs to be very generic
	private static Logger M_log = Logger.getLogger(LTI2Util.class.toString());

	public static final String SCOPE_LtiLink = "LtiLink";
	public static final String SCOPE_ToolProxyBinding = "ToolProxyBinding";
	public static final String SCOPE_ToolProxy = "ToolProxy";

    private static final String EMPTY_JSON_OBJECT = "{\n}\n";

	// Validate the incoming tool_services against a tool consumer
	public static String validateServices(ToolConsumer consumer, JSONObject providerProfile)
	{
		// Mostly to catch casting errors from bad JSON
		try {
			JSONObject security_contract = (JSONObject) providerProfile.get(LTI2Constants.SECURITY_CONTRACT);
			if ( security_contract == null  ) {
				return "JSON missing security_contract";
			}
			JSONArray tool_services = (JSONArray) security_contract.get(LTI2Constants.TOOL_SERVICE);

			List<ServiceOffered> services_offered = consumer.getService_offered();

			if ( tool_services != null ) for (Object o : tool_services) {
				JSONObject tool_service = (JSONObject) o;
				String json_service = (String) tool_service.get(LTI2Constants.SERVICE);

				boolean found = false;
				for (ServiceOffered service : services_offered ) {
					String service_endpoint = service.getEndpoint();
					if ( service_endpoint.equals(json_service) ) {
						found = true;
						break;
					}
				}
				if ( ! found ) return "Service not allowed: "+json_service;
			}
			return null;
		}
		catch (Exception e) {
			return "Exception:"+ e.getLocalizedMessage();
		}
	}

	// Validate incoming capabilities requested against out ToolConsumer
	public static String validateCapabilities(ToolConsumer consumer, JSONObject providerProfile)
	{
		List<Properties> theTools = new ArrayList<Properties> ();
		Properties info = new Properties();

		// Mostly to catch casting errors from bad JSON
		try {
			String retval = parseToolProfile(theTools, info, providerProfile);
			if ( retval != null )  return retval;

			if ( theTools.size() < 1 ) return "No tools found in profile";

			// Check all the capabilities requested by all the tools comparing against consumer
			List<String> capabilities = consumer.getCapability_offered();
			for ( Properties theTool : theTools ) {
				String ec = (String) theTool.get("enabled_capability");
				JSONArray enabled_capability = (JSONArray) JSONValue.parse(ec);
				if ( enabled_capability != null ) for (Object o : enabled_capability) {
					ec = (String) o;
					if ( capabilities.contains(ec) ) continue;
					return "Capability not permitted="+ec;
				}
			}
			return null;
		}
		catch (Exception e ) {
			return "Exception:"+ e.getLocalizedMessage();
		}
	}

	public static void allowEmail(List<String> capabilities) {
		capabilities.add("Person.email.primary");
	}

	public static void allowName(List<String> capabilities) {
		capabilities.add("User.username");
		capabilities.add("Person.name.fullname");
		capabilities.add("Person.name.given");
		capabilities.add("Person.name.family");
		capabilities.add("Person.name.full");
	}

	public static void allowResult(List<String> capabilities) {
		capabilities.add("Result.sourcedId");
		capabilities.add("Result.autocreate");
		capabilities.add("Result.url");
	}

	public static void allowSettings(List<String> capabilities) {
		capabilities.add("LtiLink.custom.url");
		capabilities.add("ToolProxy.custom.url");
		capabilities.add("ToolProxyBinding.custom.url");
	}

	// If this code looks like a hack - it is because the spec is a hack.
	// There are five possible scenarios for GET and two possible scenarios
	// for PUT.  I begged to simplify the business logic but was overrulled.
	// So we write obtuse code.
	@SuppressWarnings({ "unchecked", "unused" })
	public static Object getSettings(HttpServletRequest request, String scope,
		JSONObject link_settings, JSONObject binding_settings, JSONObject proxy_settings,
		String link_url, String binding_url, String proxy_url)
	{
		// Check to see if we are doing the bubble
		String bubbleStr = request.getParameter("bubble");
		String acceptHdr = request.getHeader("Accept");
		String contentHdr = request.getContentType();

		if ( bubbleStr != null && bubbleStr.equals("all") &&
			acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) < 0 ) {
			return "Simple format does not allow bubble=all";
		}

		if ( SCOPE_LtiLink.equals(scope) || SCOPE_ToolProxyBinding.equals(scope)
			|| SCOPE_ToolProxy.equals(scope) ) {
			// All good
		} else {
			return "Bad Setttings Scope="+scope;
		}

		boolean bubble = bubbleStr != null && "GET".equals(request.getMethod());
		boolean distinct = bubbleStr != null && "distinct".equals(bubbleStr);
		boolean bubbleAll = bubbleStr != null && "all".equals(bubbleStr);

		// Check our output format
		boolean acceptComplex = acceptHdr == null || acceptHdr.indexOf(StandardServices.TOOLSETTINGS_FORMAT) >= 0;

		if ( distinct && link_settings != null && scope.equals(SCOPE_LtiLink) ) {
			Iterator<String> i = link_settings.keySet().iterator();
			while ( i.hasNext() ) {
				String key = (String) i.next();
				if ( binding_settings != null ) binding_settings.remove(key);
				if ( proxy_settings != null ) proxy_settings.remove(key);
			}
		}

		if ( distinct && binding_settings != null && scope.equals(SCOPE_ToolProxyBinding) ) {
			Iterator<String> i = binding_settings.keySet().iterator();
			while ( i.hasNext() ) {
				String key = (String) i.next();
				if ( proxy_settings != null ) proxy_settings.remove(key);
			}
		}

		// Lets get this party started...
		JSONObject jsonResponse =  null;
		if ( (distinct || bubbleAll) && acceptComplex ) {
			jsonResponse = new JSONObject();
			jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.TOOLSETTINGS_CONTEXT);
			JSONArray graph = new JSONArray();
			boolean started = false;
			if ( link_settings != null && SCOPE_LtiLink.equals(scope) ) {
				JSONObject cjson = new JSONObject();
				cjson.put(LTI2Constants.JSONLD_ID,link_url);
				cjson.put(LTI2Constants.TYPE,SCOPE_LtiLink);
				cjson.put(LTI2Constants.CUSTOM,link_settings);
				graph.add(cjson);
				started = true;
			}
			if ( binding_settings != null && ( started || SCOPE_ToolProxyBinding.equals(scope) ) ) {
				JSONObject cjson = new JSONObject();
				cjson.put(LTI2Constants.JSONLD_ID,binding_url);
				cjson.put(LTI2Constants.TYPE,SCOPE_ToolProxyBinding);
				cjson.put(LTI2Constants.CUSTOM,binding_settings);
				graph.add(cjson);
				started = true;
			}
			if ( proxy_settings != null && ( started || SCOPE_ToolProxy.equals(scope) ) ) {
				JSONObject cjson = new JSONObject();
				cjson.put(LTI2Constants.JSONLD_ID,proxy_url);
				cjson.put(LTI2Constants.TYPE,SCOPE_ToolProxy);
				cjson.put(LTI2Constants.CUSTOM,proxy_settings);
				graph.add(cjson);
			}
			jsonResponse.put(LTI2Constants.GRAPH,graph);

		} else if ( distinct ) {  // Simple format output
			jsonResponse = proxy_settings;
			if ( SCOPE_LtiLink.equals(scope) ) {
				jsonResponse.putAll(binding_settings);
				jsonResponse.putAll(link_settings);
			} else if ( SCOPE_ToolProxyBinding.equals(scope) ) {
				jsonResponse.putAll(binding_settings);
			}
		} else { // bubble not specified
			jsonResponse = new JSONObject();
			jsonResponse.put(LTI2Constants.CONTEXT,StandardServices.TOOLSETTINGS_CONTEXT);
			JSONObject theSettings = null;
			String endpoint = null;
			if ( SCOPE_LtiLink.equals(scope) ) {
				endpoint = link_url;
				theSettings = link_settings;
			} else if ( SCOPE_ToolProxyBinding.equals(scope) ) {
				endpoint = binding_url;
				theSettings = binding_settings;
			}
			if ( SCOPE_ToolProxy.equals(scope) ) {
				endpoint = proxy_url;
				theSettings = proxy_settings;
			}
			if ( acceptComplex ) {
				JSONArray graph = new JSONArray();
				JSONObject cjson = new JSONObject();
				cjson.put(LTI2Constants.JSONLD_ID,endpoint);
				cjson.put(LTI2Constants.TYPE,scope);
				cjson.put(LTI2Constants.CUSTOM,theSettings);
				graph.add(cjson);
				jsonResponse.put(LTI2Constants.GRAPH,graph);
			} else {
				jsonResponse = theSettings;
			}
		}
		return jsonResponse;
	}

	// Parse a provider profile with lots of error checking...
	public static String parseToolProfile(List<Properties> theTools, Properties info, JSONObject jsonObject)
	{
		try {
			return parseToolProfileInternal(theTools, info, jsonObject);
		} catch (Exception e) {
			M_log.warning("Internal error parsing tool proxy\n"+jsonObject.toString());
			e.printStackTrace();
			return "Internal error parsing tool proxy:"+e.getLocalizedMessage();
		}
	}

	// Parse a provider profile with lots of error checking...
	@SuppressWarnings("unused")
	private static String parseToolProfileInternal(List<Properties> theTools, Properties info, JSONObject jsonObject)
	{
		Object o = null;
		JSONObject tool_profile = (JSONObject) jsonObject.get("tool_profile");
		if ( tool_profile == null  ) {
			return "JSON missing tool_profile";
		}
		JSONObject product_instance = (JSONObject) tool_profile.get("product_instance");
		if ( product_instance == null  ) {
			return "JSON missing product_instance";
		}

		String instance_guid = (String) product_instance.get("guid");
		if ( instance_guid == null  ) {
			return "JSON missing product_info / guid";
		}
		info.put("instance_guid",instance_guid);

		JSONObject product_info = (JSONObject) product_instance.get("product_info");
		if ( product_info == null  ) {
			return "JSON missing product_info";
		}

		// Look for required fields
		JSONObject product_name = product_info == null ? null : (JSONObject) product_info.get("product_name");
		String productTitle = product_name == null ? null : (String) product_name.get("default_value");
		JSONObject description = product_info == null ? null : (JSONObject) product_info.get("description");
		String productDescription = description == null ? null : (String) description.get("default_value");

		JSONObject product_family = product_info == null ? null : (JSONObject) product_info.get("product_family");
		String productCode = product_family == null ? null : (String) product_family.get("code");
		JSONObject product_vendor = product_family == null ? null : (JSONObject) product_family.get("vendor");
		description = product_vendor == null ? null : (JSONObject) product_vendor.get("description");
		String vendorDescription = description == null ? null : (String) description.get("default_value");
		String vendorCode = product_vendor == null ? null : (String) product_vendor.get("code");

		if ( productTitle == null || productDescription == null ) {
			return "JSON missing product_name or description ";
		}
		if ( productCode == null || vendorCode == null || vendorDescription == null ) {
			return "JSON missing product code, vendor code or description";
		}

		info.put("product_name", productTitle);
		info.put("description", productDescription);  // Backwards compatibility
		info.put("product_description", productDescription);
		info.put("product_code", productCode);
		info.put("vendor_code", vendorCode);
		info.put("vendor_description", vendorDescription);

		o = tool_profile.get("base_url_choice");
		if ( ! (o instanceof JSONArray)|| o == null  ) {
			return "JSON missing base_url_choices";
		}
		JSONArray base_url_choices = (JSONArray) o;

		String secure_base_url = null;
		String default_base_url = null;
		for ( Object i : base_url_choices ) {
			JSONObject url_choice = (JSONObject) i;
			secure_base_url = (String) url_choice.get("secure_base_url");
			default_base_url = (String) url_choice.get("default_base_url");
		}

		String launch_url = secure_base_url;
		if ( launch_url == null ) launch_url = default_base_url;
		if ( launch_url == null ) {
			return "Unable to determine launch URL";
		}

		o = (JSONArray) tool_profile.get("resource_handler");
		if ( ! (o instanceof JSONArray)|| o == null  ) {
			return "JSON missing resource_handlers";
		}
		JSONArray resource_handlers = (JSONArray) o;

		// Loop through resource handlers, read, and check for errors
		for(Object i : resource_handlers ) {
			JSONObject resource_handler = (JSONObject) i;
			JSONObject resource_type_json = (JSONObject) resource_handler.get("resource_type");
			String resource_type_code = (String) resource_type_json.get("code");
			if ( resource_type_code == null ) {
				return "JSON missing resource_type code";
			}
			o = (JSONArray) resource_handler.get("message");
			if ( ! (o instanceof JSONArray)|| o == null ) {
				return "JSON missing resource_handler / message";
			}
			JSONArray messages = (JSONArray) o;

			JSONObject titleObject = (JSONObject) resource_handler.get("name");
			String title = titleObject == null ? null : (String) titleObject.get("default_value");
			if ( title == null || titleObject == null ) {
				return "JSON missing resource_handler / name / default_value";
			}

			JSONObject buttonObject = (JSONObject) resource_handler.get("short_name");
			String button = buttonObject == null ? null : (String) buttonObject.get("default_value");

			JSONObject descObject = (JSONObject) resource_handler.get("description");
			String resourceDescription = descObject == null ? null : (String) descObject.get("default_value");

			String path = null;
			JSONArray parameter = null;
			JSONArray enabled_capability = null;
			for ( Object m : messages ) {
				JSONObject message = (JSONObject) m;
				String message_type = (String) message.get("message_type");
				if ( ! "basic-lti-launch-request".equals(message_type) ) continue;
				if ( path != null ) {
					return "A resource_handler cannot have more than one basic-lti-launch-request message RT="+resource_type_code;
				}
				path = (String) message.get("path");
				if ( path == null ) {
					return "A basic-lti-launch-request message must have a path RT="+resource_type_code;
				}
				o = (JSONArray) message.get("parameter");
				if ( ! (o instanceof JSONArray)) {
					return "Must be an array: parameter RT="+resource_type_code;
				}
				parameter = (JSONArray) o;

				o = (JSONArray) message.get("enabled_capability");
				if ( ! (o instanceof JSONArray)) {
					return "Must be an array: enabled_capability RT="+resource_type_code;
				}
				enabled_capability = (JSONArray) o;
			}

			// Ignore everything except launch handlers
			if ( path == null ) continue;

			// Check the URI
			String thisLaunch = launch_url;
			if ( ! thisLaunch.endsWith("/") && ! path.startsWith("/") ) thisLaunch = thisLaunch + "/";
			thisLaunch = thisLaunch + path;
			try {
				URL url = new URL(thisLaunch);
			} catch ( Exception e ) {
				return "Bad launch URL="+thisLaunch;
			}

			// Passed all the tests...  Lets keep it...
			Properties theTool = new Properties();

			theTool.put("resource_type", resource_type_code); // Backwards compatibility
			theTool.put("resource_type_code", resource_type_code);
			if ( title == null ) title = productTitle;
			if ( title != null ) theTool.put("title", title);
			if ( button != null ) theTool.put("button", button);
			if ( resourceDescription == null ) resourceDescription = productDescription;
			if ( resourceDescription != null ) theTool.put("description", resourceDescription);
			if ( parameter != null ) theTool.put("parameter", parameter.toString());
			if ( enabled_capability != null ) theTool.put("enabled_capability", enabled_capability.toString());
			theTool.put("launch", thisLaunch);
			theTools.add(theTool);
		}
		return null;  // All good
	}

	public static JSONObject parseSettings(String settings)
	{
		if ( settings == null || settings.length() < 1 ) {
			settings = EMPTY_JSON_OBJECT;
		}
		return (JSONObject) JSONValue.parse(settings);
	}

	/* Two possible formats:

		key=val;key2=val2;

		key=val
		key2=val2
	*/
	public static boolean mergeLTI1Custom(Properties custom, String customstr)
	{
		if ( customstr == null || customstr.length() < 1 ) return true;

		String [] params = customstr.split("[\n;]");
		for (int i = 0 ; i < params.length; i++ ) {
			String param = params[i];
			if ( param == null ) continue;
			if ( param.length() < 1 ) continue;

			int pos = param.indexOf("=");
			if ( pos < 1 ) continue;
			if ( pos+1 > param.length() ) continue;
			String key = mapKeyName(param.substring(0,pos));
			if ( key == null ) continue;

			if ( custom.containsKey(key) ) continue;

			String value = param.substring(pos+1);
			if ( value == null ) continue;
			value = value.trim();
			if ( value.length() < 1 ) continue;
			setProperty(custom, key, value);
		}
		return true;
	}

	/*
	  "custom" :
	  {
		"isbn" : "978-0321558145",
		"style" : "jazzy"
	  }
	*/
	public static boolean mergeLTI2Custom(Properties custom, String customstr)
	{
		if ( customstr == null || customstr.length() < 1 ) return true;
		JSONObject json = null;
		try {
			json = (JSONObject) JSONValue.parse(customstr.trim());
		} catch(Exception e) {
			M_log.warning("mergeLTI2Custom could not parse\n"+customstr);
			M_log.warning(e.getLocalizedMessage());
			return false;
		}

		// This could happen if the old settings service was used
		// on an LTI 2.x placement to put in settings that are not
		// JSON - we just ignore it.
		if ( json == null ) return false;
		Iterator<?> keys = json.keySet().iterator();
		while( keys.hasNext() ){
			String key = (String)keys.next();
			if ( custom.containsKey(key) ) continue;
			Object value = json.get(key);
			if ( value instanceof String ){
				setProperty(custom, key, (String) value);
			}
		}
		return true;
	}

	/*
	  "parameter" :
	  [
		{ "name" : "result_url",
		  "variable" : "Result.url"
		},
		{ "name" : "discipline",
		  "fixed" : "chemistry"
		}
	  ]
	*/
	public static boolean mergeLTI2Parameters(Properties custom, String customstr) {
		if ( customstr == null || customstr.length() < 1 ) return true;
		JSONArray json = null;
		try {
			json = (JSONArray) JSONValue.parse(customstr.trim());
		} catch(Exception e) {
			M_log.warning("mergeLTI2Parameters could not parse\n"+customstr);
			M_log.warning(e.getLocalizedMessage());
			return false;
		}
		Iterator<?> parameters = json.iterator();
		while( parameters.hasNext() ){
			Object o = parameters.next();
			JSONObject parameter = null;
			try {
				parameter = (JSONObject) o;
			} catch(Exception e) {
				M_log.warning("mergeLTI2Parameters did not find list of objects\n"+customstr);
				M_log.warning(e.getLocalizedMessage());
				return false;
			}

			String name = (String) parameter.get("name");

			if ( name == null ) continue;
			if ( custom.containsKey(name) ) continue;
			String fixed = (String) parameter.get("fixed");
			String variable = (String) parameter.get("variable");
			if ( variable != null ) {
				setProperty(custom, name, variable);
				continue;
			}
			if ( fixed != null ) {
				setProperty(custom, name, fixed);
			}
		}
		return true;
	}

	public static void substituteCustom(Properties custom, Properties lti2subst)
	{
		if ( custom == null || lti2subst == null ) return;
		Enumeration<?> e = custom.propertyNames();
		while (e.hasMoreElements()) {
			String key = (String) e.nextElement();
			String value =  custom.getProperty(key);
			if ( value == null || value.length() < 1 ) continue;
			String newValue = lti2subst.getProperty(value);
			if ( newValue == null ||  newValue.length() < 1 ) continue;
			setProperty(custom, key, (String) newValue);
		}
	}

	// Place the custom values into the launch
	public static void addCustomToLaunch(Properties ltiProps, Properties custom)
	{
        Enumeration<?> e = custom.propertyNames();
        while (e.hasMoreElements()) {
            String keyStr = (String) e.nextElement();
            String value =  custom.getProperty(keyStr);
            setProperty(ltiProps,"custom_"+keyStr,value);
        }
	}

	@SuppressWarnings("deprecation")
	public static void setProperty(Properties props, String key, String value) {
		BasicLTIUtil.setProperty(props, key, value);
	}

	public static String mapKeyName(String keyname) {
		return BasicLTIUtil.mapKeyName(keyname);
	}

}


package com.jpardogo.listbuddies.lib.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.widget.AutoScrollHelper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewStub;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;

import com.jpardogo.listbuddies.lib.R;
import com.jpardogo.listbuddies.lib.adapters.CircularLoopAdapter;
import com.jpardogo.listbuddies.lib.helpers.ListBuddiesAutoScrollHelper;
import com.jpardogo.listbuddies.lib.provider.ScrollConfigOptions;

/**
 * LinerLayout that contains 2 ListViews. This ListViews auto-scroll while the user is not interacting with them.
 * When the user interact with one of the ListViews a parrallax effect is created during its scroll.
 */
public class ListBuddiesLayout extends LinearLayout implements View.OnTouchListener, ObservableListView.ListViewObserverDelegate {

    private static final String TAG = ListBuddiesLayout.class.getSimpleName();
    private static final long PRESS_DELAY = 100;
    private static final float CANCEL_CLICK_LIMIT = 8;
    public static final int DEFAULT_SPEED = 2;
    public static final int ATTR_NOT_SET = -1;

    private OnBuddyItemClickListener mItemBuddyListener;
    private ObservableListView mListViewLeft;
    private ObservableListView mListViewRight;
    private boolean mActionDown;
    private boolean isRightListEnabled = false;
    private boolean isLeftListEnabled = false;
    private boolean isUserInteracting = true;
    private boolean isAutoScrollLeftListFaster;
    private boolean isManualScrollLeftListFaster;
    private View mDownView;
    private View mGapView;
    private ViewStub mViewStubGap;
    private Rect mRect = new Rect();
    private ListBuddiesAutoScrollHelper mScrollHelper;
    private int[] mListViewCoords = new int[2];
    private int mGap;
    private int mGapColor;
    private int mSpeed = DEFAULT_SPEED;
    private int mSpeedLeft;
    private int mSpeedRight;
    private int mDividerHeight;
    private int mLastViewTouchId;
    private int mDownPosition;
    private int mChildCount;
    private float mDownEventY;
    private Drawable mDivider;

    public ListBuddiesLayout(Context context) {
        super(context);
        init(context);
    }

    public ListBuddiesLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setListBuddiesAttributes(context, attrs);
        init(context);
        applyViewParams();
    }

    private void setListBuddiesAttributes(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListBuddiesOptions, 0, 0);
        mGap = a.getDimensionPixelSize(R.styleable.ListBuddiesOptions_gap, getResources().getDimensionPixelSize(R.dimen.default_margin_between_lists));
        mSpeed = a.getInteger(R.styleable.ListBuddiesOptions_speed, DEFAULT_SPEED);
        int configOptionFasterList = a.getInteger(R.styleable.ListBuddiesOptions_autoScrollFaster, ScrollConfigOptions.LEFT.getConfigValue());
        isAutoScrollLeftListFaster = configOptionFasterList == ScrollConfigOptions.LEFT.getConfigValue();
        isManualScrollLeftListFaster = a.getInteger(R.styleable.ListBuddiesOptions_scrollFaster, configOptionFasterList) == ScrollConfigOptions.LEFT.getConfigValue();
        mDivider = a.getDrawable(R.styleable.ListBuddiesOptions_listsDivider);
        mDividerHeight = a.getDimensionPixelSize(R.styleable.ListBuddiesOptions_listsDividerHeight, ATTR_NOT_SET);
        mGapColor = a.getColor(R.styleable.ListBuddiesOptions_gapColor, ATTR_NOT_SET);
        a.recycle();
    }

    private void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.listbuddies, this, true);
        mListViewLeft = (ObservableListView) findViewById(R.id.list_left);
        mListViewRight = (ObservableListView) findViewById(R.id.list_right);
        mLastViewTouchId = mListViewRight.getId();
        calibrateSpeed();
        setListeners();
        setScrollHelpers();
        startAutoScroll();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setMotionEventSplittingEnabled(false);
        }
    }

    private void calibrateSpeed() {
        //Parallax doesnt work with speed 1
        if (mSpeed == 1) {
            mSpeed = DEFAULT_SPEED;
        }

        if (isAutoScrollLeftListFaster) {
            mSpeedLeft = mSpeed;
            mSpeedRight = mSpeed / 2;
        } else {
            mSpeedLeft = mSpeed / 2;
            mSpeedRight = mSpeed;
        }
    }

    private void setListeners() {
        mListViewLeft.setOnTouchListener(this);
        mListViewLeft.setObserver(this);
        setOnListScrollListener(mListViewLeft, true);
        mListViewRight.setOnTouchListener(this);
        mListViewRight.setObserver(this);
        setOnListScrollListener(mListViewRight, false);
    }

    private void setOnListScrollListener(ObservableListView list, final boolean isLeftList) {
        list.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int state) {
                switch (state) {
                    case SCROLL_STATE_IDLE:
                        setListState(true, isLeftList);
                        forceScrollIfNeeded(isOtherListEnable(isLeftList));
                        break;

                    case SCROLL_STATE_TOUCH_SCROLL:
                        setListState(false, isLeftList);
                        isUserInteracting = true;
                        break;
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                                 int totalItemCount) {
            }
        });
    }

    private void forceScrollIfNeeded(boolean isListEnabled) {
        if (isUserInteracting && isListEnabled) {
            isUserInteracting = false;
            if (!mActionDown) {
                forceScroll();
            }
        }
    }

    private boolean isOtherListEnable(boolean isLeftList) {
        boolean result;
        if (isLeftList) {
            result = isRightListEnabled;
        } else {
            result = isLeftListEnabled;
        }
        return result;
    }

    private void setListState(boolean isEnabled, boolean isLeftList) {
        if (isLeftList) {
            isLeftListEnabled = isEnabled;
        } else {
            isRightListEnabled = isEnabled;
        }
    }

    private void setScrollHelpers() {
        mScrollHelper = new ListBuddiesAutoScrollHelper(mListViewLeft) {
            @Override
            public void scrollTargetBy(int deltaX, int deltaY) {
                mListViewLeft.smoothScrollBy(mSpeedLeft, 0);
                mListViewRight.smoothScrollBy(mSpeedRight, 0);
            }

            @Override
            public boolean canTargetScrollHorizontally(int i) {
                return false;
            }

            @Override
            public boolean canTargetScrollVertically(int i) {
                return true;
            }
        };

        mScrollHelper.setEnabled(isEnable());
        mScrollHelper.setEdgeType(AutoScrollHelper.EDGE_TYPE_OUTSIDE);
    }

    private boolean isEnable() {
        return mSpeed != 0;
    }

    private void startAutoScroll() {
        mListViewLeft.post(new Runnable() {
            @Override
            public void run() {
                forceScroll();
            }
        });
    }

    private void forceScroll() {
        MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_MOVE, 570, -1, 0);
        mScrollHelper.onTouch(mListViewLeft, event);
    }

    private void applyViewParams() {
        setGap();
        setDividerAndHeight(mDivider, mDividerHeight);
    }

    private void setDividerAndHeight(Drawable drawable, int dividerHeight) {
        setListsDivider(drawable);
        setHeightDivider(dividerHeight);
    }

    private void setListsDivider(Drawable drawable) {
        if (drawable != null) {
            mListViewLeft.setDivider(drawable);
            mListViewRight.setDivider(drawable);
        }
    }

    private void setHeightDivider(int dividerHeight) {
        if (dividerHeight > ATTR_NOT_SET) {
            mListViewLeft.setDividerHeight(dividerHeight);
            mListViewRight.setDividerHeight(dividerHeight);
        }
    }

    private void setGap() {
        if (mGapColor != ATTR_NOT_SET) {
            createGap();
            setGapColor();
        } else {
            emptyGap();
        }
    }

    private void setGapColor() {
        if (mGapView != null && mGapColor != ATTR_NOT_SET) {
            mGapView.setBackgroundColor(mGapColor);
        }
    }

    private void emptyGap() {
        setLeftListMargin(mGap);
        if (mGapView != null) {
            mGapView.setVisibility(View.GONE);
        }
    }

    private void setLeftListMargin(int marginRigth) {
        if (mListViewLeft != null) {
            LinearLayout.LayoutParams params = (LayoutParams) mListViewLeft.getLayoutParams();
            params.setMargins(0, 0, marginRigth, 0);
            mListViewLeft.setLayoutParams(params);
        }
    }

    private void createGap() {
        if (mViewStubGap == null) {
            mViewStubGap = (ViewStub) findViewById(R.id.gap);
        }
        if (mGapView == null) {
            mGapView = mViewStubGap.inflate();
        }

        setLeftListMargin(0);
        setGapWidth(mGap);
        if (mGapView != null) {
            mGapView.setVisibility(View.VISIBLE);
        }
    }

    private void setGapWidth(int width) {
        if (mGapView != null) {
            LinearLayout.LayoutParams params = (LayoutParams) mGapView.getLayoutParams();
            params.width = width;
            mGapView.setLayoutParams(params);
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ListView list = (ListView) v;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                actionDown(list, event);
                break;
            case MotionEvent.ACTION_UP:
                actionUp(list);
                break;
            case MotionEvent.ACTION_MOVE:
                actionMove(event);
                break;
        }
        return mScrollHelper.onTouch(v, event);
    }

    private void actionDown(ListView list, MotionEvent event) {
        mActionDown = true;
        toogleListView(list);
        mLastViewTouchId = list.getId();
        startClickSelection(event, list, event.getY());
    }

    /**
     * Each time we touch the opposite ListView than the last one we have selected
     * we need to activate it as the enable one
     */
    private void toogleListView(View v) {
        if (mLastViewTouchId != v.getId()) {
            if (mLastViewTouchId == mListViewLeft.getId()) {
                isLeftListEnabled = true;
                isRightListEnabled = false;
            } else {
                isLeftListEnabled = false;
                isRightListEnabled = true;
            }
        }
    }

    private void startClickSelection(MotionEvent event, ListView list, float eventY) {
        if (!isUserInteracting || mSpeed == 0) {
            findViewClicked(event, eventY, list);

            setSelectorPressed(list);
        }
    }

    private void findViewClicked(MotionEvent event, float eventY, ListView list) {
        mChildCount = list.getChildCount();
        mListViewCoords = new int[2];
        list.getLocationOnScreen(mListViewCoords);
        int x = (int) event.getRawX() - mListViewCoords[0];
        int y = (int) event.getRawY() - mListViewCoords[1];
        View child;
        for (int i = 0; i < mChildCount; i++) {
            child = list.getChildAt(i);
            child.getHitRect(mRect);
            if (mRect.contains(x, y)) {
                mDownView = child;
                mDownEventY = eventY;
                break;
            }
        }
    }

    private void setSelectorPressed(ListView list) {
        if (mDownView != null) {
            mDownPosition = list.getPositionForView(mDownView);
            mDownView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (isUserInteracting) {
                        if (mDownView != null) {
                            mDownView.setPressed(true);
                        }
                    }
                }
            }, PRESS_DELAY);
        }
    }

    private void actionUp(ListView list) {
        mActionDown = false;
        performClick(list);
    }

    private void actionMove(MotionEvent event) {
        cancelClick(event.getY());
    }

    private void cancelClick(float eventY) {
        if (mDownView != null && (Math.abs(mDownEventY - eventY) > CANCEL_CLICK_LIMIT)) {
            mDownView.setPressed(false);
            mDownView = null;
        }
    }

    private void performClick(ListView list) {
        //In some occasions setOnListScrollListener-SCROLL_STATE_TOUCH_SCROLL
        //doesn't get called when we perform a click, so we need to double check
        if (!isUserInteracting) {
            isUserInteracting = true;
        }
        if (mDownView != null || (mSpeed == 0 && mDownView != null)) {
            mDownView.setPressed(false);
            if (mItemBuddyListener != null) {
                int buddy = 0;
                if (list.getId() != mListViewLeft.getId()) {
                    buddy = 1;
                }
                int position = getPosition(list, mDownPosition);
                mItemBuddyListener.onBuddyItemClicked(list, mDownView, buddy, position, mDownView.getId());
            }
        }
    }

    private int getPosition(ListView list, int position) {
        return ((CircularLoopAdapter) list.getAdapter()).getCircularPosition(position);
    }

    /**
     * Receives the distance scroll on listView.
     */
    @Override
    public void onListScroll(View view, float deltaY) {
        int speed;
        if (view.getId() == mListViewLeft.getId() && !isLeftListEnabled) {
            speed = getSpeed(false, deltaY);

            mListViewRight.smoothScrollBy(speed, 0);
        } else if (view.getId() == mListViewRight.getId() && !isRightListEnabled) {
            speed = getSpeed(true, deltaY);
            mListViewLeft.smoothScrollBy(speed, 0);
        }
    }

    private int getSpeed(boolean isCalculationForLeft, float deltaY) {
        int speed;

        if (isManualScrollLeftListFaster && isCalculationForLeft || !isManualScrollLeftListFaster && !isCalculationForLeft) {
            speed = (int) -deltaY * 2;
        } else {
            speed = (int) -deltaY / 2;
        }
        return speed;
    }

    public interface OnBuddyItemClickListener {
        //Buddy corresponde with the list (0-left, 1-right)
        void onBuddyItemClicked(AdapterView<?> parent, View view, int buddy, int position, long id);
    }

    /**
     * Public setters
     */

    public void setOnItemClickListener(OnBuddyItemClickListener listener) {
        mItemBuddyListener = listener;
    }

    public ListBuddiesLayout setGap(int value) {
        mGap = value;
        setGap();
        return this;
    }

    public ListBuddiesLayout setSpeed(int value) {
        mSpeed = value;
        calibrateSpeed();
        return this;
    }

    public ListBuddiesLayout setDividerHeight(int value) {
        mDividerHeight = value;
        setDividerAndHeight(mDivider, value);
        return this;
    }

    public ListBuddiesLayout setDivider(Drawable drawable) {
        mDivider = drawable;
        setDividerAndHeight(drawable, mDividerHeight);
        return this;
    }

    public ListBuddiesLayout setGapColor(int color) {
        mGapColor = color;
        setGap();
        return this;
    }

    public ListBuddiesLayout setAutoScrollFaster(int option) {
        isAutoScrollLeftListFaster = option == 1;
        calibrateSpeed();
        return this;
    }

    public ListBuddiesLayout setManualScrollFaster(int option) {
        isManualScrollLeftListFaster = option == 1;
        return this;
    }

    public void setAdapters(CircularLoopAdapter adapter, CircularLoopAdapter adapter2) {
        mListViewLeft.setAdapter(adapter);
        mListViewRight.setAdapter(adapter2);
        mListViewLeft.setSelection(Integer.MAX_VALUE / 2);
        mListViewRight.setSelection(Integer.MAX_VALUE / 2);
    }

    /**
     * Public getters
     */
    public ListView getListViewLeft() {
        return mListViewLeft;
    }

    public ListView getListViewRight() {
        return mListViewRight;
    }

    public int getGapColor() {
        return mGapColor;
    }

    public int getSpeed() {
        return mSpeed;
    }

    public int getGap() {
        return mGap;
    }

    public int getDividerHeight() {
        return mDividerHeight;
    }

    public Drawable getDivider() {
        return mDivider;
    }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeUrls.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.common.io.BaseEncoding;
import com.google.common.net.UrlEscapers;
import com.google.errorprone.annotations.CompileTimeConstant;

import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.CheckReturnValue;

/**
 * Protocol conversions and factory methods for {@link SafeUrl}.
 */
@CheckReturnValue
@GwtCompatible
public final class SafeUrls {

  // Don't forget to update the public docs when updating this set.
  private static final Set<String> DEFAULT_SAFE_SCHEMES = createUnmodifiableSet(
      "http", "https", "mailto", "ftp");

  private static final Set<CustomSafeUrlScheme> EMPTY_CUSTOM_SCHEMES = Collections.emptySet();

  private SafeUrls() {}

  private static final Set<String> createUnmodifiableSet(String ...schemes) {
    HashSet<String> set = new HashSet<String>();
    for (String scheme : schemes) {
      set.add(scheme);
    }
    return Collections.unmodifiableSet(set);
  }

  /**
   * Deserializes a SafeUrlProto into a SafeUrl instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(SafeUrl)} or its
   * equivalent in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static SafeUrl fromProto(SafeUrlProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseSafeUrlWrappedValue());
  }

  /**
   * Serializes a SafeUrl into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a SafeUrl using
   * {@link #fromProto(SafeUrlProto)}.
   */
  public static SafeUrlProto toProto(SafeUrl url) {
    return SafeUrlProto.newBuilder()
        .setPrivateDoNotAccessOrElseSafeUrlWrappedValue(url.getSafeUrlString())
        .build();
  }

  /**
   * Creates a SafeUrl from the given compile-time constant string {@code url}.
   *
   *
No runtime validation or sanitization is performed on {@code url}; being under application
   * control, it is simply assumed to comply with the SafeUrl contract.
   */
  public static SafeUrl fromConstant(@CompileTimeConstant final String url) {
    return create(url);
  }

  /**
   * Creates a SafeUrl object from the given {@code url}, validating that the input string matches
   * a pattern of commonly used safe URLs. If {@code url} fails validation, this method returns a
   * SafeUrl, {@link SafeUrl#INNOCUOUS}, which contains an innocuous string,
   * {@link SafeUrl#INNOCUOUS_STRING}.
   *
   *
Specifically, {@code url} may be a URL with any of the default safe schemes (http, https,
   * ftp, mailto), or a relative URL (i.e., a URL without a scheme; specifically, a scheme-relative,
   * absolute-path-relative, or path-relative URL).
   *
   * @see http://url.spec.whatwg.org/#concept-relative-url
   */
  public static SafeUrl sanitize(String url) {
    return sanitize(url, EMPTY_CUSTOM_SCHEMES);
  }

  /**
   * Creates a SafeUrl object from the given {@code url}, validating that the input string matches
   * a pattern of commonly used safe URLs. If {@code url} fails validation, this method returns a
   * SafeUrl, {@link SafeUrl#INNOCUOUS}, which contains an innocuous string,
   * {@link SafeUrl#INNOCUOUS_STRING}.
   *
   *
{@code url} is sanitized as in {@link #sanitize(String)}, additionally permitting the
   * custom schemes listed in {@code extraAllowedSchemes}.
   */
  public static SafeUrl sanitize(String url, Set<CustomSafeUrlScheme> extraAllowedSchemes) {
    if (!isSafeUrl(url, extraAllowedSchemes)) {
      return SafeUrl.INNOCUOUS;
    }
    return create(url);
  }

  /**
   * Sanitizes the given {@code url}, validating that the input string matches a pattern of commonly
   * used safe URLs. If {@code url} fails validation, this method returns
   * {@code about:invalid#identifier}, with the given {@code identifier}. The identifier allows
   * users to trace a sanitized value to the library that performed the sanitization and hence
   * should be a unique string like "zLibraryNamez".
   *
   *
Specifically, {@code url} may be a URL with any of the default safe schemes (http, https,
   * ftp, mailto), or a relative URL (i.e., a URL without a scheme; specifically, a scheme-relative,
   * absolute-path-relative, or path-relative URL).
   *
   * @see http://url.spec.whatwg.org/#concept-relative-url
   */
  public static String sanitizeAsString(String url, @CompileTimeConstant final String identifier) {
    if (!isSafeUrl(url, EMPTY_CUSTOM_SCHEMES)) {
      return "about:invalid#" + identifier;
    }
    return url;
  }

  /**
   * Creates a {@code data:text/html} URL whose content is populated from the given
   * {@code SafeHtml} object.
   *
   *
The resulting {@code data}-scheme URL's content is UTF-8-encoded, but the
   * encoding of non-ASCII characters is done using the standard %xx hex encoding.
   *
   * @see http://tools.ietf.org/html/rfc2397
   * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs
   */
  public static SafeUrl createHtmlDataUrl(SafeHtml html) {
    // Use urlPathSegmentEscaper because all other Escapers convert spaces to "+" instead of "%20",
    // which are rendered as normal "+"s in the browser instead of being rendered as spaces.
    String dataUrl =
        "data:text/html;charset=UTF-8,"
            + UrlEscapers.urlPathSegmentEscaper().escape(html.getSafeHtmlString());
    return create(dataUrl);
  }

  /**
   * Creates a {@code data:text/html} URL whose content is populated from the given
   * {@code SafeHtml} object.
   *
   *
The resulting {@code data}-scheme URL's content is UTF-8-encoded, and further encoded using
   * base-64 transfer encoding.
   *
   * @see http://tools.ietf.org/html/rfc2397
   * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs
   */
  public static SafeUrl createHtmlDataUrlBase64(SafeHtml html) {
    try {
      String dataUrl =
          "data:text/html;charset=UTF-8;base64,"
              + BaseEncoding.base64().encode(html.getSafeHtmlString().getBytes("UTF-8"));
      return create(dataUrl);
    } catch (UnsupportedEncodingException e) {
      // Should never happen.  We use getBytes(String) instead of getBytes(CharSet) because
      // there's no java.nio.charset.StandardCharsets in older Android SDKs.
      throw new RuntimeException(e);
    }
  }

  /**
  * Matches a subset of URLs that will not cause script execution if used in URL context within a
  * HTML document. Specifically, this method returns true if the {@code url}:
  *

  *
Starts with a default safe protocol (http, https, ftp, mailto) or one of the schemes
  * specified in {@code extraAllowedSchemes}.
  *
Contains no protocol.  A protocol must be followed by a colon and colons are allowed
  *     only after one of the characters [/?#].
  *     A colon after a hash (#) must be in the fragment.
  *     Otherwise, a colon after a (?) must be in a query.
  *     Otherwise, a colon after a single solidus (/) must be in a path.
  *     Otherwise, a colon after a double solidus (*) must be in the authority (before port).
  *

  *
  *
We don't use a regex so that we don't need to depend on GWT, which does not support Java's
  * Pattern and requires using its RegExp class.
   */
  private static boolean isSafeUrl(String url, Set<CustomSafeUrlScheme> extraAllowedSchemes) {
    String lowerCased = url.toLowerCase();

    // If some Unicode character lower cases to something that ends up matching these ASCII ones,
    // it's harmless.
    for (String scheme : DEFAULT_SAFE_SCHEMES) {
      if (lowerCased.startsWith(scheme + ":")) {
        return true;
      }
    }

    for (CustomSafeUrlScheme scheme : extraAllowedSchemes) {
      /**
       * For "-" in a custom URL scheme, it's not possible to write a proto enum with "-" in the
       * field name. In proto, it has to be "_". But we can safely convert all "_" in the proto name
       * to "-", since according to the URL Living Standard, a URL-scheme string must be one ASCII
       * alpha, followed by zero or more of ASCII alphanumeric, "+", "-", and ".".
       *
       * @see https://url.spec.whatwg.org/#url-syntax
       */
      if (lowerCased.startsWith(scheme.name().toLowerCase().replace('_', '-') + ":")) {
        return true;
      }
    }

    for (int i = 0; i < url.length(); i++) {
      switch (url.charAt(i)) {
        case '/':
        case '?':
        case '#':
          // After this the string can end or contain anything else, it won't be interpreted
          // as the scheme.
          return true;
        case ':':
          // This character is not allowed before seeing one of the above characters.
          return false;
        default:
          // Other characters ok.
          continue;
      }
    }
    return true;
  }

  /**
   * Creates a SafeUrl by doing an unchecked conversion from the given {@code url}. Also called
   * from SafeUrlBuilder.
   */
  static SafeUrl create(String url) {
    return new SafeUrl(url);
  }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeStyleBuilder.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.common.base.Joiner;
import com.google.common.base.Joiner.MapJoiner;
import com.google.common.net.PercentEscaper;
import com.google.errorprone.annotations.CompileTimeConstant;

import java.util.LinkedHashMap;
import javax.annotation.CheckReturnValue;
import javax.annotation.concurrent.NotThreadSafe;

/**
 * A builder for values of type {@link SafeStyle}.
 *
 *
The builder allows a sequence of CSS declarations to be constructed by combining trusted
 * and untrusted values. Trusted values are passed via {@link CompileTimeConstant} strings and
 * enums. Untrusted values are passed via regular strings and are subject to runtime sanitization
 * and, if deemed unsafe, replaced by an innocuous value, {@link #INNOCUOUS_PROPERTY_STRING}.
 *
 *
This builder does not guarantee semantically valid CSS, only that the generated SafeStyle
 * fulfills its type contract.
 *
 *
This builder currently lacks support for most CSS properties, if you need support for a
 * missing one please contact safe-html-types-discuss@googlegroups.com.
 */
@NotThreadSafe @GwtCompatible
public final class SafeStyleBuilder {

  // TODO(mlourenco): Change SafeUrl to also use "JSafeHtml".
  private static final String INNOCUOUS_PROPERTY_STRING = "zJSafeHtmlzinvalid";

  private final LinkedHashMap<String, String> properties = new LinkedHashMap<String, String>();

  // TODO(mlourenco): Consider whether we want to avoid or discourage the  following asymmetry:
  // .backgroundAttachmendAppend("1", "2").backgroundImageAppendConstant("1,2")

  /**
   * Appends {@code values} to the {@code background-attachment} property. Values will be comma
   * separated.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-attachment"
   */
  public SafeStyleBuilder backgroundAttachmentAppend(String value, String... otherValues) {
    appendToProperty("background-attachment", sanitizeAndJoinEnumValues(value, otherValues));
    return this;
  }

  /**
   * Sets {@code constant} as the {@code background-color} property.
   *
   *
Only minimal runtime validation is performed on {@code constant}; being under application
   * control, it is assumed to be valid and to not break the syntactic structure of the underlying
   * CSS (by, for example, including comment markers).
   *
   * @throws IllegalArgumentException if {@code constant} contains blacklisted characters or comment
   *     markers: '<', '>', '"', '\'', ';', "//", "/*" or "*/"
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-color"
   */
  public SafeStyleBuilder backgroundColorFromConstant(@CompileTimeConstant final String constant) {
    properties.put("background-color", checkConstantValue(constant));
    return this;

  }

  /**
   * Sets {@code value} as the {@code background-color} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-color"
   */
  public SafeStyleBuilder backgroundColor(String value) {
    properties.put("background-color", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Appends {@code constant} to the {@code background-image} property, if necessary inserting a
   * leading comma. Note that {@code constant} itself can contain commas, to separate multiple
   * values being set on the property. Note that since "//" is not allowed, this method cannot
   * be used to append URLs, instead use {@link #backgroundImageAppendUrl(String)}
   *
   *
Only minimal runtime validation is performed on {@code constant}; being under application
   * control, it is assumed to be valid and to not break the syntactic structure of the underlying
   * CSS (by, for example, including comment markers).
   *
   * @throws IllegalArgumentException if {@code constant} contains blacklisted characters or comment
   *     markers: '<', '>', '"', '\'', ';', "//", "/*" or "*/"
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-image"
   */
  public SafeStyleBuilder backgroundImageAppendConstant(
      @CompileTimeConstant final String constant) {
    appendToProperty("background-image", checkConstantValue(constant));
    return this;
  }

  /**
   * Appends a {@code url} value to the {@code background-image} property, if necessary inserting
   * a leading comma. The {@code url} value will be inserted inside a {@code url} function call.
   *
   *
The {@code url} is validated as safe, as determined by {@link SafeUrls#sanitize(String)}.
   * It also percent-encoded to prevent it from interefering with the structure of the surrounding
   * CSS.
   *
   *
TODO(mlourenco): The right thing to do would be to CSS-escape but percent-encoding is
   * easier for now because we don't have a CSS-escaper. As URLs in CSS are likely to point to
   * domains we control it seems extremely unlikely that this will break anything.
   *
   * @see "http://dev.w3.org/csswg/css-backgrounds/#background-image"
   */
  public SafeStyleBuilder backgroundImageAppendUrl(String url) {
    url = SafeUrls.sanitize(url).getSafeUrlString();
    try {
      url = ESCAPER_BACKGROUND_IMAGE.escape(url);
    } catch (IllegalArgumentException e) {  // Happens if url contains invalid surrogate sequences.
      url = INNOCUOUS_PROPERTY_STRING;
    }
    String urlValue = "url(" + url + ")";
    appendToProperty("background-image", urlValue);
    return this;
  }

  /** All RFC 3986 unreserved characters. */
  private static final String UNRESERVED_CHARACTERS = "-._~";

  /**
   * Reserved characters in RFC 3986. '\'', '(' and ')' are excluded, they only appear in the
   * obsolete mark production in Appendix D.2 of RFC 3986, so they can be encoded without
   * changing semantics. ';' and ',' are excluded too since those are meta-characters in the
   * context of CSS properties and background-image, respectively.
   */
  private static final String RESERVED_CHARACTERS_BACKGROUND_IMAGE = "/:?#[]@!$&*+=";

  /**
   * Encoding '%' characters that are already part of a valid percent-encoded sequence changes the
   * semantics of a URL, and hence we need to preserve them. Note that this may allow non-encoded
   * '%' characters to remain in the URL (i.e., occurrences of '%' that are not part of a valid
   * percent-encoded sequence, for example, 'ab%xy').
   */
  private static final String OTHER_CHARACTERS = "%";

  /** Percent-encodes and UTF-16 validates, like encodeURI() in JavaScript. */
  private static final PercentEscaper ESCAPER_BACKGROUND_IMAGE = new PercentEscaper(
      UNRESERVED_CHARACTERS + RESERVED_CHARACTERS_BACKGROUND_IMAGE + OTHER_CHARACTERS, false);

  /**
   * Appends {@code values} to the {@code background-size} property. Values will be comma
   * separated.
   *
   *
All values must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-size"
   */
  public SafeStyleBuilder backgroundSizeAppend(String value, String... otherValues) {
    appendToProperty("background-size", sanitizeAndJoinRegularValues(value, otherValues));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border"
   */
  public SafeStyleBuilder border(String value) {
    properties.put("border", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border-top} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-top"
   */
  public SafeStyleBuilder borderTop(String value) {
    properties.put("border-top", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border-bottom} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom"
   */
  public SafeStyleBuilder borderBottom(String value) {
    properties.put("border-bottom", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border-right} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-right"
   */
  public SafeStyleBuilder borderRight(String value) {
    properties.put("border-right", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border-left} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-left"
   */
  public SafeStyleBuilder borderLeft(String value) {
    properties.put("border-left", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code bottom} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/bottom"
   */
  public SafeStyleBuilder bottom(String value) {
    properties.put("bottom", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code padding} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/padding"
   */
  public SafeStyleBuilder padding(String value) {
    properties.put("padding", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code margin} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/margin"
   */
  public SafeStyleBuilder margin(String value) {
    properties.put("margin", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code float} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/float"
   */
  public SafeStyleBuilder setFloat(String value) {
    properties.put("float", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code display} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/display"
   */
  public SafeStyleBuilder display(String value) {
    properties.put("display", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code background-repeat} property
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat"
   */
  public SafeStyleBuilder backgroundRepeat(String value) {
    properties.put("background-repeat", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code constant} as the {@code border-color} property.
   *
   *
Only minimal runtime validation is performed on {@code constant}; being under application
   * control, it is assumed to be valid and to not break the syntactic structure of the underlying
   * CSS (by, for example, including comment markers).
   *
   * @throws IllegalArgumentException if {@code constant} contains blacklisted characters or comment
   *     markers: '<', '>', '"', '\'', ';', "//", "/*" or "*/"
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-color"
   */
  public SafeStyleBuilder borderColorFromConstant(@CompileTimeConstant final String constant) {
    properties.put("border-color", checkConstantValue(constant));
    return this;

  }

  /**
   * Sets {@code value} as the {@code border-color} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-color"
   */
  public SafeStyleBuilder borderColor(String value) {
    properties.put("border-color", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code border-collapse} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/border-collapse"
   */
  public SafeStyleBuilder borderCollapse(String value) {
    properties.put("border-collapse", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code color} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/color"
   */
  public SafeStyleBuilder color(String value) {
    properties.put("color", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Appends {@code values} to the {@code font-family} property. Values will be comma separated.
   *
   *
If a value consists only of only ASCII alphabetic or '-' characters, it is appended as is,
   * since it might be a generic font family (like {@code serif}) which must not be quoted. If a
   * value contains other characters it will be quoted, since quoting might be required (for
   * example, if the font name contains a space) and unnecessary quoting is allowed.
   *
   *
Values must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/font-family"
   */
  public SafeStyleBuilder fontFamilyAppend(String value, String... otherValues) {
    sanitizeAndAppendToFontFamily(value);
    for (String otherValue : otherValues) {
      sanitizeAndAppendToFontFamily(otherValue);
    }
    return this;
  }

  private void sanitizeAndAppendToFontFamily(String value) {
    if (isEnumValue(value)) {
      appendToProperty("font-family", value);
    } else if (isRegularValue(value)) {
      appendToProperty("font-family", "\"" + value + "\"");
    } else {
      appendToProperty("font-family", INNOCUOUS_PROPERTY_STRING);
    }
  }

  /**
   * Sets {@code value} as the {@code height} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/height"
   */
  public SafeStyleBuilder height(String value) {
    properties.put("height", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code background-position} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/background-position"
   */
  public SafeStyleBuilder backgroundPosition(String value) {
    properties.put("background-position", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code left} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/left"
   */
  public SafeStyleBuilder left(String value) {
    properties.put("left", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code line-height} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/line-height"
   */
  public SafeStyleBuilder lineHeight(String value) {
    properties.put("line-height", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code overflow} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/overflow"
   */
  public SafeStyleBuilder overflow(String value) {
    properties.put("overflow", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code overflow-x} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/overflow"
   */
  public SafeStyleBuilder overflowX(String value) {
    properties.put("overflow-x", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code overflow-y} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/overflow"
   */
  public SafeStyleBuilder overflowY(String value) {
    properties.put("overflow-y", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code text-align} property.
   *
   *
The values must consist of only ASCII alphabetic or '-' characters. A non-conforming value
   * will be replaced with {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/text-align"
   */
  public SafeStyleBuilder textAlign(String value) {
    properties.put("text-align", sanitizeEnumValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code right} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/right"
   */
  public SafeStyleBuilder right(String value) {
    properties.put("right", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code top} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/top"
   */
  public SafeStyleBuilder top(String value) {
    properties.put("top", sanitizeRegularValue(value));
    return this;
  }

  /**
   * Sets {@code value} as the {@code width} property.
   *
   *
The value must contain only whitelisted characters; those are alphanumerics, space,
   * tab, and the set {@code [+-.!#%_/*]}. In addition, comment markers - {@code //}, {@code /*},
   * and */- are disallowed too. Non-conforming values are replaced with
   * {@link #INNOCUOUS_PROPERTY_STRING}.
   *
   * @see "https://developer.mozilla.org/en-US/docs/Web/CSS/width"
   */
  public SafeStyleBuilder width(String value) {
    properties.put("width", sanitizeRegularValue(value));
    return this;
  }

  @CheckReturnValue
  public SafeStyle build() {
    StringBuilder sb = new StringBuilder();
    if (!properties.isEmpty()) {
      // SafeStyle contract requires a trailing ';'.
      JOINER.appendTo(sb, properties).append(";");
    }
    return SafeStyles.create(sb.toString());
  }

  private static final MapJoiner JOINER = Joiner.on(";").withKeyValueSeparator(":");

  private void appendToProperty(String property, String safeValue) {
    if (properties.containsKey(property)) {
      // LinkedHashMap maintains initial insertion order when an item is reinserted.
      properties.put(property, properties.get(property) + "," + safeValue);
    } else {
      properties.put(property, safeValue);
    }
  }

  /**
   * Throws IllegalArgumentException if {@code value} contains any of a few blacklisted characters
   * which would change the syntactic structure of the CSS or entirely break out of the HTML style
   * attribute.
   */
  private static String checkConstantValue(@CompileTimeConstant final String value) {
    for (int i = 0; i < value.length(); i++) {
      char c = value.charAt(i);
      if (c == '<' || c == '>' || c == '"' || c == '\'' || c == ';') {
        throw new IllegalArgumentException(
            "Value contains HTML/CSS meta-characters ([<>\"';]): " + value);
      } else if (
            value.startsWith("/*", i) || value.startsWith("*/", i) || value.startsWith("//", i)) {
          throw new IllegalArgumentException(
              "Value contains CSS comment marker (/*, */ or //): " + value);
      }
    }
    return value;
  }

  /**
   * Sanitizes and joins string varargs where each value is subjected to sanitization via
   * {@link #sanitizeEnumValue(String)}.
   */
  private static String sanitizeAndJoinEnumValues(String value, String[] otherValues) {
    StringBuilder sb = new StringBuilder(sanitizeEnumValue(value));
    for (int i = 0; i < otherValues.length; i++) {
      sb.append(',').append(sanitizeEnumValue(otherValues[i]));
    }
    return sb.toString();
  }

  /**
   * Sanitizes and joins string varargs where each value is subjected to sanitization via
   * {@link #sanitizeRegularValue(String)}.
   */
  private static String sanitizeAndJoinRegularValues(String value, String[] otherValues) {
    StringBuilder sb = new StringBuilder(sanitizeRegularValue(value));
    for (int i = 0; i < otherValues.length; i++) {
      sb.append(',').append(sanitizeRegularValue(otherValues[i]));
    }
    return sb.toString();
  }

  /**
   * For properties that can only be set to an enum we just allow alphabetic and '-' characters.
   */
  private static String sanitizeEnumValue(String value) {
    if (isEnumValue(value)) {
      return value;
    } else {
      return INNOCUOUS_PROPERTY_STRING;
    }
  }

  private static boolean isEnumValue(String value) {
    if (value.isEmpty()) {
      return false;
    }
    for (int i = 0; i < value.length(); i++) {
      char c = value.charAt(i);
      if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-')) {
        return false;
      }
    }
    return true;
  }

  /**
   * This method implements the same logic as jslayout's CSS sanitizer for "regular" properties,
   * except that commas are not whitelisted here, as they could be used inject extra values into
   * a property.
   *
   *

   *
Escape sequences and comments are not allowed. While "//" is not a comment marker in the
   * CSS spec, we disallow it as well. This restrictiveness minimizes the chance that browser
   * peculiarities, or bugs, parsing these complex sequences will let sanitization be bypassed.
   *
'(' and ')' which can be used to call functions are also disallowed.
   *
Characters which could be matched on CSS error recovery
   * (http://www.w3.org/TR/css3-syntax/#error-handling) of a previously malformed token, like '@'
   * and ':' are not present.
   *

   */
  private static String sanitizeRegularValue(String value) {
    if (isRegularValue(value)) {
      return value;
    } else {
      return INNOCUOUS_PROPERTY_STRING;
    }
  }

  private static boolean isRegularValue(String value) {
    boolean hasNonWhitespace = false;
    for (int i = 0; i < value.length(); i++) {
      char c = value.charAt(i);
      if (value.startsWith("/*", i) || value.startsWith("*/", i) || value.startsWith("//", i)) {
        return false;
      } else if (c == ' ' || c == '\t') {
        continue;
      } else if (c == '/' || c == '*' || c == '+' || c == '-' || c == '.' || c == '!' || c == '#'
          || c == '%' || c == '_') {
        hasNonWhitespace = true;
        continue;
      } else if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
        hasNonWhitespace = true;
        continue;
      } else {
        return false;
      }
    }
    return hasNonWhitespace;
  }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeStyles.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.errorprone.annotations.CompileTimeConstant;

import javax.annotation.CheckReturnValue;

/**
 * Protocol conversions and factory methods for {@link SafeStyle}.
 */
@CheckReturnValue
@GwtCompatible
public final class SafeStyles {

  private SafeStyles() {}

  /**
   * Creates a SafeStyle from the given compile-time constant string {@code style}.
   *
   *
{@code style} should be in the format {@code name: value; [name: value; ...]} and must
   * not have any < or > characters in it. This is so that SafeStyle's contract is preserved,
   * allowing the SafeStyle to correctly be interpreted as a sequence of CSS declarations and
   * without affecting the syntactic structure of any surrounding CSS and HTML.
   *
   *
This method performs basic sanity checks on the format of {@code style} but does not
   * constrain the format of {@code name} and {@code value}, except for disallowing tag characters.
   *
   * @throws IllegalArgumentException if some of the constraints of the format specified above
   *     are not met
   */
  public static SafeStyle fromConstant(@CompileTimeConstant final String style) {
    if (style.length() == 0) {
      return SafeStyle.EMPTY;
    }
    for (int i = 0; i < style.length(); i++) {
      if (style.charAt(i) == '<' || style.charAt(i) == '>') {
        throw new IllegalArgumentException("Forbidden characters in style string: " + style);
      }
    }
    if (style.charAt(style.length() - 1) != ';') {
      throw new IllegalArgumentException("Last character of style string is not ';': " + style);
    }
    if (!style.contains(":")) {
      throw new IllegalArgumentException("Style string must contain at least one ':', to "
        + "specify a \"name: value\" pair: " + style);
    }
    return create(style);
  }

  /**
   * Deserializes a SafeStyleProto into a SafeStyle instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(SafeStyle)} or its equivalent
   * in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static SafeStyle fromProto(SafeStyleProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseSafeStyleWrappedValue());
  }

  /**
   * Serializes a SafeStyle into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a SafeStyle using
   * {@link #fromProto(SafeStyleProto)}.
   */
  public static SafeStyleProto toProto(SafeStyle style) {
    return SafeStyleProto.newBuilder()
        .setPrivateDoNotAccessOrElseSafeStyleWrappedValue(style.getSafeStyleString()).build();
  }

  static SafeStyle create(String style) {
    return new SafeStyle(style);
  }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeScripts.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.io.Resources;
import com.google.errorprone.annotations.CompileTimeConstant;
import java.io.IOException;
import java.nio.charset.Charset;
import javax.annotation.CheckReturnValue;

/**
 * Protocol conversions and factory methods for {@link SafeScript}.
 */
@CheckReturnValue
@GwtCompatible(emulated = true)
public final class SafeScripts {

  private SafeScripts() {}

  /**
   * Creates a SafeScript from the given compile-time constant string {@code script}.
   */
  public static SafeScript fromConstant(@CompileTimeConstant final String script) {
    if (script.length() == 0) {
      return SafeScript.EMPTY;
    }
    return create(script);
  }

  /**
   * Creates a SafeScript from the given compile-time constant {@code resourceName} using the given
   * {@code charset}.
   *
   *
This performs ZERO VALIDATION of the data. We assume that resources should be safe because
   * they are part of the binary, and therefore not attacker controlled.
   *
   * @param contextClass Class relative to which to load the resource.
   */
  @GwtIncompatible("Resources")
  public static SafeScript fromResource(
      Class<?> contextClass, @CompileTimeConstant final String resourceName, Charset charset)
      throws IOException {
    return create(Resources.toString(Resources.getResource(contextClass, resourceName), charset));
  }

  /**
   * Deserializes a SafeScriptProto into a SafeScript instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(SafeScript)} or its equivalent
   * in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static SafeScript fromProto(SafeScriptProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseSafeScriptWrappedValue());
  }

  /**
   * Serializes a SafeScript into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a SafeScript using
   * {@link #fromProto(SafeScriptProto)}.
   */
  public static SafeScriptProto toProto(SafeScript script) {
    return SafeScriptProto.newBuilder()
        .setPrivateDoNotAccessOrElseSafeScriptWrappedValue(script.getSafeScriptString()).build();
  }

  static SafeScript create(String script) {
    return new SafeScript(script);
  }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/SafeStyleSheets.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.io.Resources;
import com.google.errorprone.annotations.CompileTimeConstant;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import javax.annotation.CheckReturnValue;

/**
 * Protocol conversions and factory methods for {@link SafeStyleSheet}.
 */
@CheckReturnValue
@GwtCompatible(emulated = true)
public final class SafeStyleSheets {

  private SafeStyleSheets() {}

  /**
   * Creates a SafeStyleSheet from the given compile-time constant string {@code style}.
   *
   *
{@code styleSheet} must not have any < characters in it so that the syntactic
   * structure of any surrounding CSS and HTML is not affected.
   *
   * @throws IllegalArgumentException if {@code styleSheet} contains <
   */
  public static SafeStyleSheet fromConstant(@CompileTimeConstant final String styleSheet) {
    if (styleSheet.length() == 0) {
      return SafeStyleSheet.EMPTY;
    }
    for (int i = 0; i < styleSheet.length(); i++) {
      if (styleSheet.charAt(i) == '<') {
        throw new IllegalArgumentException(
            "Forbidden '<' character in style sheet string: " + styleSheet);
      }
    }
    return create(styleSheet);
  }

  /**
   * Creates a SafeStyleSheet from the given compile-time constant {@code resourceName} using
   * the given {@code charset}.
   *
   *
This performs ZERO VALIDATION of the data. We assume that resources should be safe because
   * they are part of the binary, and therefore not attacker controlled.
   *
   * @param contextClass Class relative to which to load the resource.
   */
  @GwtIncompatible("Resources")
  public static SafeStyleSheet fromResource(
      Class<?> contextClass, @CompileTimeConstant final String resourceName, Charset charset)
      throws IOException {
    return create(Resources.toString(Resources.getResource(contextClass, resourceName), charset));
  }

  /**
   * Deserializes a SafeStyleSheetProto into a SafeStyleSheet instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(SafeStyleSheet)} or its
   * equivalent in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static SafeStyleSheet fromProto(SafeStyleSheetProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseSafeStyleSheetWrappedValue());
  }

  /**
   * Serializes a SafeStyleSheet into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a SafeStyleSheet using
   * {@link #fromProto(SafeStyleSheetProto)}.
   */
  public static SafeStyleSheetProto toProto(SafeStyleSheet style) {
    return SafeStyleSheetProto.newBuilder()
        .setPrivateDoNotAccessOrElseSafeStyleSheetWrappedValue(
            style.getSafeStyleSheetString())
        .build();
  }

  /**
   * Creates a new SafeStyleSheet which contains, in order, the string representations of the given
   * {@code stylesheets}.
   */
  public static SafeStyleSheet concat(SafeStyleSheet... stylesheets) {
    return concat(Arrays.asList(stylesheets));
  }

  /**
   * Creates a new SafeStyleSheet which contains, in order, the string representations of the given
   * {@code stylesheets}.
   */
  public static SafeStyleSheet concat(Iterable<SafeStyleSheet> stylesheets) {
    int concatLength = 0;
    for (SafeStyleSheet stylesheet : stylesheets) {
      concatLength += stylesheet.getSafeStyleSheetString().length();
    }

    StringBuilder result = new StringBuilder(concatLength);
    for (SafeStyleSheet stylesheet : stylesheets) {
      result.append(stylesheet.getSafeStyleSheetString());
    }
    return create(result.toString());
  }

  static SafeStyleSheet create(String style) {
    return new SafeStyleSheet(style);
  }
}


// **** GENERATED CODE, DO NOT MODIFY ****
// This file was generated via preprocessing from input:
// java/com/google/common/html/types/TrustedResourceUrls.java.tpl
// ***************************************
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.google.common.html.types;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.errorprone.annotations.CompileTimeConstant;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

/**
 * Protocol conversions and factory methods for {@link TrustedResourceUrl}.
 */
@CheckReturnValue
@GwtCompatible(emulated = true)
public final class TrustedResourceUrls {

  private TrustedResourceUrls() {}

  /**
   * Deserializes a TrustedResourceUrlProto into a TrustedResourceUrl instance.
   *
   *
Protocol-message forms are intended to be opaque. The fields of the protocol message should
   * be considered encapsulated and are not intended for direct inspection or manipulation. Protocol
   * message forms of this type should be produced by {@link #toProto(TrustedResourceUrl)} or its
   * equivalent in other implementation languages.
   *
   *
Important: It is unsafe to invoke this method on a protocol message that has been
   * received from an entity outside the application's trust domain. Data coming from the browser
   * is outside the application's trust domain.
   */
  public static TrustedResourceUrl fromProto(TrustedResourceUrlProto proto) {
    return create(proto.getPrivateDoNotAccessOrElseTrustedResourceUrlWrappedValue());
  }

  /**
   * Serializes a TrustedResourceUrl into its opaque protocol message representation.
   *
   *
Protocol message forms of this type are intended to be opaque. The fields of the returned
   * protocol message should be considered encapsulated and are not intended for direct inspection
   * or manipulation. Protocol messages can be converted back into a TrustedResourceUrl using
   * {@link #fromProto(TrustedResourceUrlProto)}.
   */
  public static TrustedResourceUrlProto toProto(TrustedResourceUrl url) {
    return TrustedResourceUrlProto.newBuilder()
        .setPrivateDoNotAccessOrElseTrustedResourceUrlWrappedValue(
            url.getTrustedResourceUrlString())
        .build();
  }

  /**
   * Creates a TrustedResourceUrl from the given compile-time constant string {@code url}.
   *
   *
No runtime validation or sanitization is performed on {@code url}; being under application
   * control, it is simply assumed to comply with the TrustedResourceUrl contract.
   */
  public static TrustedResourceUrl fromConstant(@CompileTimeConstant final String url) {
    return create(url);
  }

  /**
   * Creates a {@link TrustedResourceUrl} from the value of an environment variable. In a server
   * setting, environment variables are part of the application's deployment configuration and are
   * hence considered application-controlled. If the variable is not defined returns null.
   *
   *
   *
No runtime validation or sanitization is performed on the value of the environment variable;
   * being under application control, it is simply assumed to comply with the TrustedResourceUrl
   * contract.
   *
   * @throws SecurityException  if a security manager exists and its checkPermission method doesn't
   *     allow access to the environment variable name
   */
  @GwtIncompatible("System.getEnv")
  @Nullable
  public static TrustedResourceUrl fromEnvironmentVariable(
      @CompileTimeConstant final String variableName) {
    String var = System.getenv(variableName);
    if (var == null) {
      return null;
    }
    return create(var);
  }

  /** Also called from TrustedResourceUrlBuilder. */
  static TrustedResourceUrl create(String url) {
    return new TrustedResourceUrl(url);
  }
}