/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.esri.gpt.catalog.schema; import java.lang.reflect.InvocationTargetException; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.esri.gpt.framework.jsf.MessageBroker; import com.esri.gpt.framework.util.LogUtil; import com.esri.gpt.framework.util.Val; import com.esri.gpt.framework.xml.DomUtil; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.html.HtmlGraphicImage; import javax.faces.component.html.HtmlOutputText; import javax.faces.component.html.HtmlPanelGroup; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; /** * Super-class for an input component associated with a metadata schema. */ public abstract class Input extends UiComponent { // class variables ============================================================= /** Delimited text area input type = "delimitedTextArea" */ public static final String INPUTTYPE_DELIMITEDTEXTAREA = "delimitedTextArea"; /** Interactive map input type = "map" */ public static final String INPUTTYPE_MAP = "map"; /** Select many checkbox input type = "selectManyCheckbox" */ public static final String INPUTTYPE_SELECTMANYCHECKBOX = "selectManyCheckbox"; /** Select one menu input type = "selectOneMenu" */ public static final String INPUTTYPE_SELECTONEMEMU = "selectOneMenu"; /** Select one menu with an "Other" option for inputting text = "selectWithOther" */ public static final String INPUTTYPE_SELECTWITHOTHER = "selectWithOther"; /** Text input type = "text" */ public static final String INPUTTYPE_TEXT = "text"; /** Text area input type = "textArea" */ public static final String INPUTTYPE_TEXTAREA = "textArea"; /** Text array input type = "textArray" */ public static final String INPUTTYPE_TEXTARRAY = "textArray"; /** Unknown input type = "unknown" */ public static final String INPUTTYPE_UNKNOWN = "unknown"; // instance variables ========================================================== private String _defaultValue = ""; private boolean _editable = true; private String _facesId = ""; private String _hintMode = "inline"; private String _hintResourceKey = ""; private String _onChange = ""; private String _onClick = ""; private String _testResourceKey = ""; // constructors ================================================================ /** Default constructor. */ public Input() { this(null); } /** * Construct by duplicating an existing object. * @param objectToDuplicate the object to duplicate */ public Input(Input objectToDuplicate) { super(objectToDuplicate); if (objectToDuplicate != null) { setDefaultValue(objectToDuplicate.getDefaultValue()); setEditable(objectToDuplicate.getEditable()); setFacesId(objectToDuplicate.getFacesId()); setHintResourceKey(objectToDuplicate.getHintResourceKey()); setHintMode(objectToDuplicate.getHintMode()); setTestResourceKey(objectToDuplicate.getTestResourceKey()); setOnChange(objectToDuplicate.getOnChange()); setOnClick(objectToDuplicate.getOnClick()); } } // properties ================================================================== /** * Gets the default value. * @return the default value */ public String getDefaultValue() { return _defaultValue; } /** * Sets the default value. * <br/>The value is trimmed. * <br/>Null values are treated a empty strings. * @param value the default value */ public void setDefaultValue(String value) { _defaultValue = Val.chkStr(value); } /** * Gets the editable status. * @return true if this input component is editable */ public boolean getEditable() { return _editable; } /** * Sets the editable status. * @param editable true if this input component is editable */ public void setEditable(boolean editable) { _editable = editable; } /** * Gets the Faces ID for the component. * @return the Faces ID */ public String getFacesId() { return _facesId; } /** * Sets the Faces ID for the component. * <br/> The '.' character will be replaced with the '.' character. * @param id Faces ID */ public void setFacesId(String id) { _facesId = Val.chkStr(id); _facesId = _facesId.replace('.','_'); } /** * Gets the mode for an optional hint ("inline","tip","toggle"). * @return the hint mode */ public String getHintMode() { return _hintMode; } /** * Sets the mode for an optional hint ("inline","tip","toggle"). * associated with the component. * @param mode the hint mode */ public void setHintMode(String mode) { mode = Val.chkStr(mode).toLowerCase(); if (mode.equals("inline") || mode.equals("tip") || mode.equals("toggle")) { _hintMode = mode; } else { _hintMode = "inline"; } } /** * Gets the UI property bundle resource key for a hint * associated with the component. * @return the hint resource key */ public String getHintResourceKey() { return _hintResourceKey; } /** * Sets the UI property bundle resource key for a hint * associated with the component. * <br/>The key will be trimmed. * <br/>A null key is treated as an empty string. * @param key the hint resource key */ public void setHintResourceKey(String key) { _hintResourceKey = Val.chkStr(key); } /** * Gets the Javascript associated with the onchange event. * @return the Javascript */ public String getOnChange() { return _onChange; } /** * Sets the Javascript associated with the onclick event. * @param js the Javascript */ public void setOnChange(String js) { _onChange = Val.chkStr(js); } /** * Gets the Javascript associated with the onclick event. * @return the Javascript */ public String getOnClick() { return _onClick; } /** * Sets the Javascript associated with the onclick event. * @param js the Javascript */ public void setOnClick(String js) { _onClick = Val.chkStr(js); } /** * Gets the UI property bundle resource key for a test link button * associated with the component. * @return the hint resource key */ public String getTestResourceKey() { return _testResourceKey; } /** * Sets the UI property bundle resource key for a test link button * associated with the component. * <br/>The key will be trimmed. * <br/>A null key is treated as an empty string. * @param key the hint resource key */ public void setTestResourceKey(String key) { _testResourceKey = Val.chkStr(key); } // methods ===================================================================== /** * Applies a hint to an input component if a hint resource key was configured. * <p/> * If a hintResourceKey was configured, the supplied inputComponent and an * HtmlOutputText representing the hint will be wrapped within an * HtmlPanelGroup and the HtmlPanelGroup will be returned. * <p/> * If a hintResourceKey was not configured, the supplied inputComponent will * be returned. * @param context the UI context * @param inputComponent the subject input component * @return the resultant UI component */ protected UIComponent applyHint(UiContext context, UIComponent inputComponent) { if ((inputComponent != null) && (getHintResourceKey().length() > 0)) { MessageBroker msgBroker = context.extractMessageBroker(); String sHint = msgBroker.retrieveMessage(getHintResourceKey()); if (this.getHintMode().equalsIgnoreCase("tip")) { // use reflection to set the tip (i.e. the HTML "title" attribute) if ((sHint != null) && (sHint.length() > 0)) { try { java.lang.reflect.Method method; Class[] sig = {java.lang.String.class}; Object[] args = {sHint}; method = inputComponent.getClass().getDeclaredMethod("setTitle",sig); if (method != null) { method.invoke(inputComponent,args); } } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } } else if (this.getHintMode().equalsIgnoreCase("toggle")) { // toggled hint (icon to open/close an element containing the hint) HtmlPanelGroup panel = new HtmlPanelGroup(); panel.getChildren().add(inputComponent); HtmlOutputText space = new HtmlOutputText(); space.setEscape(false); space.setValue(" "); panel.getChildren().add(space); HtmlGraphicImage img = new HtmlGraphicImage(); img.setStyleClass("hintIcon"); img.setUrl("/catalog/images/hint.gif"); img.setOnclick("mdeToggleHint(this);"); panel.getChildren().add(img); HtmlPanelGroup hintSection = new HtmlPanelGroup(); hintSection.setStyle("display:none;"); hintSection.setStyleClass("hintSection"); HtmlOutputText outText = new HtmlOutputText(); outText.setStyleClass("hint"); outText.setEscape(false); outText.setValue(sHint); hintSection.getChildren().add(outText); panel.getChildren().add(hintSection); return panel; } else { // inline hint (original component + output text wrapped in a panel) HtmlPanelGroup panel = new HtmlPanelGroup(); panel.getChildren().add(inputComponent); HtmlOutputText outText = new HtmlOutputText(); outText.setStyleClass("hint"); outText.setEscape(false); outText.setValue(" "+sHint); panel.getChildren().add(outText); return panel; } } return inputComponent; } /** * Configures the object based upon a node loaded from a * schema configuration XML. * <br/>The super.configure method should be invoked prior to any * sub-class configuration. * <p/> * The following attributes are configured: * <br/>defaultValue editable hintResourceKey hintMode onchange onclick testResourceKey * @param context the configuration context * @param node the configuration node * @param attributes the attributes of the configuration node */ public void configure(CfgContext context, Node node, NamedNodeMap attributes) { super.configure(context,node,attributes); setDefaultValue(DomUtil.getAttributeValue(attributes,"defaultValue")); setEditable(Val.chkBool(DomUtil.getAttributeValue(attributes,"editable"),true)); setHintResourceKey(DomUtil.getAttributeValue(attributes,"hintResourceKey")); setHintMode(DomUtil.getAttributeValue(attributes,"hintMode")); setTestResourceKey(DomUtil.getAttributeValue(attributes,"testResourceKey")); setOnChange(DomUtil.getAttributeValue(attributes,"onchange")); setOnClick(DomUtil.getAttributeValue(attributes,"onclick")); } /** * Produces a deep clone of the object. * <p/> * The typical approach is to invoke a duplication constructor. * <br/>Example: return new InputText(this); */ public abstract Input duplicate(); /** * Appends property information for the component to a StringBuffer. * <br/>The method is intended to support "FINEST" logging. * <br/>super.echo should be invoked prior appending any local information. * @param sb the StringBuffer to use when appending information */ @Override public void echo(StringBuffer sb) { super.echo(sb); sb.append(" defaultValue=\"").append(getDefaultValue()).append("\""); sb.append(" editable=\"").append(getEditable()).append("\""); if (getHintResourceKey().length() > 0) { sb.append(" hintResourceKey=\"").append(getHintResourceKey()).append("\""); sb.append(" hintMode=\"").append(getHintMode()).append("\""); } if (getTestResourceKey().length() > 0) { sb.append(" testResourceKey=\"").append(getTestResourceKey()).append("\""); } if (getOnChange().length() > 0) { sb.append(" onchange=\"").append(getOnChange()).append("\""); } if (getOnClick().length() > 0) { sb.append(" onclick=\"").append(getOnClick()).append("\""); } } /** * Evaluates the default value for the input component. * <p/> * @param context the UI context * @param currentValue the current value of the component * @return the default or current value */ protected String evaluateDefault(UiContext context, String currentValue) { String sValue = Val.chkStr(currentValue); if ((sValue.length() == 0) && context.getIsCreateDocument()) { sValue = getDefaultValue(); // evaluate a Faces binding, ignore exceptions if ((sValue.length() > 3) && (sValue.indexOf("#{") != -1)) { String sExpr = sValue; sValue = ""; try { FacesContext fc = context.getFacesContext(); ValueBinding vb = fc.getApplication().createValueBinding(sExpr); Object obj = vb.getValue(fc); if (obj != null) { sValue = Val.chkStr(obj.toString()); } } catch (Exception e) { sValue = ""; if (LogUtil.getLogger().isLoggable(Level.FINER)) { String sMsg = "Expression failed to bind:\n"+sExpr+"\n"+e.toString(); LogUtil.getLogger().finer(sMsg); } } } } return sValue; } /** * Finds the associated Faces UIInput component. * <p/> * The Faces UIInput component is located using the supplied editorForm * and the Faces ID for this Input instance, example: <br/> * editorForm.findComponent(getFacesId()); * @param context the UI context * @param editorForm the Faces HtmlForm for the metadata editor * @throws SchemaException if the associated Faces UIInput component cannot be located */ protected UIInput findInputComponent(UiContext context, UIComponent editorForm) throws SchemaException { UIComponent component = editorForm.findComponent(getFacesId()); if ((component != null) && (component instanceof UIInput)) { return (UIInput)component; } else { String sMsg = "The Faces UIInput component cannot be located for: "; throw new SchemaException(sMsg+getFacesId()); } } /** * Formats a value associated with a parameter. * <p/> * Currently, only Date type values are formatted. * @param parameter the associated parameter * @param value the value to format */ protected String formatValue(Parameter parameter, String value) { return parameter.getContent().formatValue(parameter,value); } /** * Returns the String value for a Faces UIInput component. * <p/> * If the associated value is null or not a String, * an empty String is returned. * @param input the Faces input component whose value will be returned */ protected String getInputValue(UIInput input) { Object oValue = input.getValue(); if ((oValue != null) && (oValue instanceof String)) { return (String)oValue; } else { return ""; } } /** * Makes the Faces UI input component for a parameter. * <p/> * It is the responsibility of the sub-class to instantiate the * Faces UIComponent, set the id and bind to a parameter value. * @param context the UI context * @param section the parent section * @param parameter the associated parameter * @return the UI input component */ public abstract UIComponent makeInputComponent(UiContext context, Section section, Parameter parameter); /** * Makes the Faces UI output component for a parameter. * <p/> * The output component is suitable for display on the * metadata details page. * @param context the UI context * @param section the parent section * @param parameter the associated parameter * @return the UI input component */ public UIComponent makeOutputComponent(UiContext context, Section section, Parameter parameter) { MessageBroker msgBroker = context.extractMessageBroker(); String sValue = parameter.getContent().makeDisplayValue(msgBroker,parameter); if (sValue.length() > 0) { HtmlOutputText component = new HtmlOutputText(); component.setId(getFacesId()); component.setValue(sValue); // handle a thumbnail if (parameter.getMeaningType().equalsIgnoreCase(Meaning.MEANINGTYPE_THUMBNAIL_URL)) { HtmlPanelGroup panel = new HtmlPanelGroup(); panel.getChildren().add(component); panel.getChildren().add(makeBR()); HtmlGraphicImage img = new HtmlGraphicImage(); img.setUrl(sValue); img.setStyle("border: 1px solid silver"); panel.getChildren().add(img); return panel; } else { // activate URL links (make sure to escape if turning JSF escaping off) if (sValue.toLowerCase().indexOf("http") != -1) { String exp = "(?<![=\"'\\/>;])http(s)?://([\\w+?\\.\\w+])+([a-zA-Z0-9\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?"; Pattern pattern = Pattern.compile(exp,Pattern.DOTALL | Pattern.UNIX_LINES | Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(Val.escapeXmlForBrowser(sValue)); if (matcher.find()) { String sReplaced = matcher.replaceAll("<a href=\"$0\" target=\"_blank\">$0</a>"); component.setValue(sReplaced); component.setEscape(false); } } // FTP - activate URL links (make sure to escape if turning JSF escaping off) if (sValue.toLowerCase().indexOf("ftp") != -1) { String exp = "(?<![=\"'\\/>;])ftp(s)?://([\\w+?\\.\\w+])+([a-zA-Z0-9\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?"; Pattern pattern = Pattern.compile(exp,Pattern.DOTALL | Pattern.UNIX_LINES | Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(Val.escapeXmlForBrowser(sValue)); if (matcher.find()) { String sReplaced = matcher.replaceAll("<a href=\"$0\" target=\"_blank\">$0</a>"); component.setValue(sReplaced); component.setEscape(false); } } } return component; } return null; } /** * Sets the value for an input component. * <p/> * The value set is based upon the supplied parameter's singleValue. * <p/> * Default values are evaluated if applicable. * Date type values are formatted. * @param context the UI context * @param input the Faces input component * @param parameter the associated parameter */ public void setComponentValue(UiContext context, UIInput input, Parameter parameter) { String sValue = parameter.getContent().getSingleValue().getValue(); sValue = evaluateDefault(context,sValue); sValue = formatValue(parameter,sValue); input.setValue(sValue); } /** * Triggered on the save event from the metadata editor. * <p/> * The intent is to give input objects an opportunity to propagate * local component value(s) back into the parameter's value(s). * <p/> * One example is the propagation of the input HtmlInputText value into * the parameter's singleValue for an InputText component. * <p/> * There is no default behavior. * @param context the UI context * @param editorForm the Faces HtmlForm for the metadata editor * @param parameter the associated parameter * @throws SchemaException if an associated Faces UIComponent cannot be located */ public void unBind(UiContext context, UIComponent editorForm, Parameter parameter) throws SchemaException { // no default behavior } }