// Copyright 2010 Google Inc.
//
// 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.enterprise.connector.util.connectortype;
import com.google.common.base.Preconditions;
import com.google.enterprise.connector.spi.ConnectorType;
import com.google.enterprise.connector.spi.XmlUtils;
import java.io.IOException;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The interfaces and classes contained here are useful for building
* {@link ConnectorType} implementations. This is based on code originally
* developed for the FileConnector
*
* TODO(Max): Refactor the FileConnector to use this.
*/
public class ConnectorFields {
/**
* This is a non-instantiable utility class.
*/
private ConnectorFields() {
throw new IllegalStateException();
}
private static final Logger LOG = Logger.getLogger(ConnectorFields.class.getName());
public static boolean hasContent(String s) {
/*
* We determine content by the presence of non-whitespace characters.
* Our field values come from HTML input boxes which get mapped to
* empty strings when there is no user entry.
*/
return (s != null) && (s.trim().length() != 0);
}
/**
* This represents a single configuration field. This interface is
* currently only needed so tests do not need access to AbstractField.
*/
public static interface Field {
String getName();
boolean isMandatory();
String getLabel(ResourceBundle bundle);
/**
* @return a tr element with two td elements inside for
* Config form
*/
String getSnippet(ResourceBundle bundle, boolean highlightError);
}
/**
* Holds information common to fields like their names and ways to
* show their name as an HTML label. Does not hold value of field; instead
* subclass has to handle value.
*/
public abstract static class AbstractField implements Field {
private final String name;
private final boolean mandatory; // is field necessary for valid configuration?
public AbstractField(String name, boolean mandatory) {
this.name = name;
this.mandatory = mandatory;
}
@Override
public String getName() {
return name;
}
@Override
public boolean isMandatory() {
return mandatory;
}
@Override
public String getLabel(ResourceBundle bundle) {
return bundle.getString(getName());
}
public void setBoldLabel(boolean boldLabel) {
this.boldLabel = boldLabel;
}
/**
* Makes HTML that could be used as a field label.
* Intended to be helpful inside {@link #getSnippet}.
*
* @return an HTML td element with a label element inside.
*/
protected String getLabelHtml(ResourceBundle bundle, boolean highlightError) {
return getLabelHtml(bundle, highlightError, "");
}
protected boolean renderLabelTag = true;
protected boolean boldLabel = true;
protected String getLabelHtml(ResourceBundle bundle, boolean highlightError, String message) {
StringBuffer sb = new StringBuffer();
sb.append("<td valign=\"top\">");
if (highlightError) {
sb.append("<font color=\"red\">");
}
if (boldLabel) {
sb.append("<b>");
}
if (renderLabelTag) {
sb.append("<label for=\"");
sb.append(getName());
sb.append("\">");
}
sb.append(xmlEncodeAttributeValue(getLabel(bundle)));
if (renderLabelTag) {
sb.append("</label>");
}
if (boldLabel) {
sb.append("</b>");
}
sb.append(xmlEncodeAttributeValue(message));
if (highlightError) {
sb.append("</font>");
}
sb.append("</td>");
return sb.toString();
}
@Override
public abstract String getSnippet(ResourceBundle bundle,
boolean highlightError);
/** @param config immutable Map of configuration parameters */
public abstract void setValueFrom(Map<String, String> config);
/** @param valueString String parameter */
public abstract void setValueFromString(String valueString);
/** Does this field store some user input? */
public abstract boolean hasValue();
/**
* Returns the provided attribute value with XML special characters
* escaped. This really just provides a convenience wrapper around
* {@link XmlUtils#xmlAppendAttr}.
*/
public static String xmlEncodeAttributeValue(String v) {
try {
StringBuilder sb = new StringBuilder();
XmlUtils.xmlAppendAttrValue(v, sb);
return sb.toString();
} catch (IOException ioe) {
/*
* The IOException can occur because XmlUtils.xmlAppendAttrValue
* appends to an Appendable which may throw an IOException. In our case
* we pass in a StringBuilder so no IOException should occur.
*/
LOG.log(Level.SEVERE,
"Xml escaping encountered unexpected error ", ioe);
throw new IllegalStateException(
"Xml escaping encountered unexpected error ", ioe);
}
}
}
protected static String wrapPreviousValue(String v) {
String result = "";
if (hasContent(v)) {
result = " value=\"" + v + "\"";
}
return result;
}
/**
* Represents a string parameter. The parameter's
* value is gathered using a single HTML input element.
*/
public static class SingleLineField extends AbstractField {
private static final String ONE_LINE_INPUT_HTML =
"<td><input name=\"%s\" id=\"%s\" type=\"%s\"%s></input></td>";
private static final String FORMAT = "<tr> %s " + ONE_LINE_INPUT_HTML + "</tr>";
private final boolean isPassword;
private String defaultValue;
protected String value; // user's one input line value
public SingleLineField(String name, boolean mandatory, boolean isPassword) {
super(name, mandatory);
this.isPassword = isPassword;
this.defaultValue = "";
this.value = "";
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
@Override
public void setValueFrom(Map<String, String> config) {
String newValue = config.get(getName());
if (hasContent(newValue)) {
setValueFromString(newValue);
} else {
this.value = defaultValue;
}
}
@Override
public boolean hasValue() {
return hasContent(value);
}
@Override
public String getSnippet(ResourceBundle bundle, boolean highlightError) {
return String.format(FORMAT, getLabelHtml(bundle, highlightError),
getName(),
getName(),
isPassword ? "password" : "text", wrapPreviousValue(xmlEncodeAttributeValue(value)));
}
public String getValue() {
return value;
}
@Override
public void setValueFromString(String valueString) {
if (valueString == null) {
this.value = "";
} else {
this.value = valueString.trim();
}
}
}
/**
* A SingleLineField specialized for representing an integer, with optional
* default value.
*/
public static class IntField extends SingleLineField {
private Integer intValue;
public IntField(String name, boolean mandatory, int defaultInt) {
super(name, mandatory, false);
setValueFromInt(defaultInt);
}
@Override
public void setValueFrom(Map<String, String> config) {
throw new UnsupportedOperationException();
}
@Override
public void setValueFromString(String valueString) {
throw new UnsupportedOperationException();
}
public void setValueFromInt(int i) {
intValue = i;
value = intValue.toString();
}
public Integer getIntegerValue() {
return intValue;
}
}
/**
* Represents a parameter populated through an Enum. The parameter's
* value is gathered using a dropdown. A default value specifies which value
* will be preselected.
*/
public static class EnumField<E extends Enum<E>> extends AbstractField {
private E value;
private final Class<E> enumClass;
private final E defaultValue;
public EnumField(String name, boolean mandatory, Class<E> enumClass, E defaultValue) {
super(name, mandatory);
Preconditions.checkNotNull(enumClass);
value = null;
this.enumClass = enumClass;
this.defaultValue = defaultValue;
}
@Override
public boolean hasValue() {
return value != null;
}
@Override
public String getSnippet(ResourceBundle bundle, boolean highlightError) {
E selectedValue;
if (value != null) {
selectedValue = value;
} else {
selectedValue = defaultValue;
}
StringBuffer sb = new StringBuffer();
sb.append("<tr>");
sb.append(getLabelHtml(bundle, highlightError));
sb.append("<td><select name=\"");
sb.append(getName());
sb.append("\" id=\"");
sb.append(getName());
sb.append("\">");
for (E e : enumClass.getEnumConstants()) {
sb.append("\n<option value=\"");
sb.append(e.toString());
sb.append("\"");
if (e == selectedValue) {
sb.append(" selected=\"selected\"");
}
sb.append(">");
sb.append(bundle.getString(e.toString()));
sb.append("</option>");
}
sb.append("\n</select></td></tr>");
return sb.toString();
}
public E getValue() {
if (hasValue()) {
return value;
}
return defaultValue;
}
public void setValue(E value) {
this.value = value;
}
@Override
public void setValueFrom(Map<String, String> config) {
// TODO(Max): so far this is only being used in contexts where this operation isn't needed.
throw new UnsupportedOperationException();
}
@Override
public void setValueFromString(String valueString) {
// TODO(Max): so far this is only being used in contexts where this operation isn't needed.
throw new UnsupportedOperationException();
}
}
/**
* Represents a parameter populated through a set of keys. The parameter's
* value is gathered using a bunch of checkboxes.
*/
public static class MultiCheckboxField extends AbstractField {
private SortedSet<String> selectedKeys;
private SortedSet<String> keys;
private final String message;
private Callback callback;
public interface Callback {
Map<String, String> getAttributes(String key);
}
public MultiCheckboxField(String name, boolean mandatory, Set<String> keys, String message) {
super(name, mandatory);
setKeys(keys);
setSelectedKeys(null);
this.message = message;
this.renderLabelTag = false;
}
public MultiCheckboxField(String name, boolean mandatory, Set<String> keys,
String message, Callback callback) {
this(name, mandatory, keys, message);
this.callback = callback;
}
private void makeSingleCheckboxHtml(StringBuffer sb, String boxname, String key,
boolean selected) {
sb.append("<label>");
sb.append("<input type=\"checkbox\" name=\"");
sb.append(boxname);
sb.append("\" value=\"");
sb.append(key);
sb.append("\"");
if (selected) {
sb.append(" checked=\"checked\"");
}
if (callback != null) {
Map<String, String> attributes = callback.getAttributes(key);
try {
for (Map.Entry<String, String> attr : attributes.entrySet()) {
XmlUtils.xmlAppendAttr(attr.getKey(), attr.getValue(), sb);
}
} catch (IOException e) {
// StringBuffer.append does not throw IOExceptions.
throw new AssertionError(e);
}
}
sb.append("/> ");
sb.append(key);
sb.append("</label>");
}
private String getCheckboxesHtml(String name, ResourceBundle bundle) {
StringBuffer sb = new StringBuffer();
sb.append("<td>");
int i = 0;
String boxName;
for (String key : selectedKeys) {
sb.append("\n");
boxName = getName() + "_" + i;
i++;
makeSingleCheckboxHtml(sb, boxName, key, true);
sb.append("<br/>");
}
for (String key : keys) {
if (!selectedKeys.contains(key)) {
sb.append("\n");
boxName = getName() + "_" + i;
i++;
makeSingleCheckboxHtml(sb, boxName, key, false);
sb.append("<br/>");
}
}
sb.append("</td>");
return sb.toString();
}
@Override
public String getSnippet(ResourceBundle bundle, boolean highlightError) {
if (keys == null || keys.size() < 1) {
return "";
}
StringBuffer sb = new StringBuffer();
sb.append("<tr>");
String htmlTableName = getName() + "_table";
String processedMessage = "";
if (ConnectorFields.hasContent(message)) {
processedMessage = "<br/>" + bundle.getString(message);
}
sb.append(getLabelHtml(bundle, highlightError, processedMessage));
sb.append(getCheckboxesHtml(htmlTableName, bundle));
sb.append("</tr>");
return sb.toString();
}
@Override
public boolean hasValue() {
return selectedKeys.size() > 0;
}
public boolean isEmpty() {
return keys.size() < 1;
}
private static SortedSet<String> makeSortedSet(Set<String> s) {
if (s == null) {
return new TreeSet<String>();
} else {
return new TreeSet<String>(s);
}
}
public void setKeys(Set<String> keys) {
this.keys = makeSortedSet(keys);
}
public void setSelectedKeys(Set<String> selectedKeys) {
this.selectedKeys = makeSortedSet(selectedKeys);
}
@Override
public void setValueFrom(Map<String, String> config) {
throw new UnsupportedOperationException();
}
@Override
public void setValueFromString(String valueString) {
throw new UnsupportedOperationException();
}
}
}