/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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 org.apache.jmeter.testbeans.gui; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyEditor; import java.beans.PropertyEditorSupport; import java.util.Arrays; import javax.swing.JOptionPane; import org.apache.jmeter.gui.GuiPackage; import org.apache.jmeter.util.JMeterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is an implementation of a full-fledged property editor, providing both * object-text transformation and an editor GUI (a custom editor component), * from two simpler property editors providing only one of these functions * each, namely: * <dl> * <dt>typeEditor * <dt> * <dd>Provides suitable object-to-string and string-to-object transformation * for the property's type. That is: it's a simple editor that only need to * support the set/getAsText and set/getValue methods.</dd> * <dt>guiEditor</dt> * <dd>Provides a suitable GUI for the property, but works on [possibly null] * String values. That is: it supportsCustomEditor, but get/setAsText and * get/setValue are identical.</dd> * </dl> * <p> * The resulting editor provides optional support for null values (you can * choose whether <strong>null</strong> is to be a valid property value). It also * provides optional support for JMeter 'expressions' (you can choose whether * they make valid property values). * */ class WrapperEditor extends PropertyEditorSupport implements PropertyChangeListener { private static final Logger log = LoggerFactory.getLogger(WrapperEditor.class); /** The type's property editor. */ private final PropertyEditor typeEditor; /** The gui property editor */ private final PropertyEditor guiEditor; /** Whether to allow <b>null</b> as a property value. */ private final boolean acceptsNull; /** Whether to allow JMeter 'expressions' as property values. */ private final boolean acceptsExpressions; /** Whether to allow any constant values different from the provided tags. */ private final boolean acceptsOther; /** Default value to be used to (re-)initialise the field */ private final Object defaultValue; /** * Keep track of the last valid value in the editor, so that we can revert * to it if the user enters an invalid value. */ private String lastValidValue = null; /** * Constructor for use when a PropertyEditor is delegating to us. */ WrapperEditor( Object source, PropertyEditor typeEditor, PropertyEditor guiEditor, boolean acceptsNull, boolean acceptsExpressions, boolean acceptsOther, Object defaultValue) { super(); if (source != null) { super.setSource(source); } this.typeEditor = typeEditor; this.guiEditor = guiEditor; this.acceptsNull = acceptsNull; this.acceptsExpressions = acceptsExpressions; this.acceptsOther = acceptsOther; this.defaultValue = defaultValue; initialize(); } /** * Constructor for use for regular instantiation and by subclasses. */ WrapperEditor( PropertyEditor typeEditor, PropertyEditor guiEditor, boolean acceptsNull, boolean acceptsExpressions, boolean acceptsOther, Object defaultValue) { this(null, typeEditor, guiEditor, acceptsNull, acceptsExpressions, acceptsOther, defaultValue); } final void resetValue() { setValue(defaultValue); lastValidValue = getAsText(); } private void initialize() { resetValue(); if (guiEditor instanceof ComboStringEditor) { String[] tags = guiEditor.getTags(); // Provide an initial edit value if necessary -- this is a heuristic // that tries to provide the most convenient initial edit value: String v; if (!acceptsOther) { v = "${}"; //$NON-NLS-1$ } else if (isValidValue("")) { //$NON-NLS-1$ v = ""; //$NON-NLS-1$ } else if (acceptsExpressions) { v = "${}"; //$NON-NLS-1$ } else if (tags != null && tags.length > 0) { v = tags[0]; } else { v = getAsText(); } ((ComboStringEditor) guiEditor).setInitialEditValue(v); } guiEditor.addPropertyChangeListener(this); } @Override public boolean supportsCustomEditor() { return true; } @Override public Component getCustomEditor() { return guiEditor.getCustomEditor(); } @Override public String[] getTags() { return guiEditor.getTags(); } /** * Determine whether a string is one of the known tags. * * @param text the value to be checked * @return true if text equals one of the getTags() */ private boolean isATag(String text) { String[] tags = getTags(); if (tags == null) { return false; } for (String tag : tags) { if (tag.equals(text)) { return true; } } return false; } /** * Determine whether a string is a valid value for the property. * * @param text * the value to be checked * @return true iif text is a valid value */ private boolean isValidValue(String text) { if (text == null) { return acceptsNull; } if (acceptsExpressions && isExpression(text)) { return true; } // Not an expression (isn't or can't be), not null. // The known tags are assumed to be valid: if (isATag(text)) { return true; } // Was not a tag, so if we can't accept other values... if (!acceptsOther) { return false; } // Delegate the final check to the typeEditor: try { typeEditor.setAsText(text); } catch (IllegalArgumentException e1) { return false; // setAsText failed: not valid } // setAsText succeeded: valid return true; } /** * This method is used to do some low-cost defensive programming: it is * called when a condition that the program logic should prevent from * happening occurs. I hope this will help early detection of logical bugs * in property value handling. * * @throws Error * always throws an error. */ private void shouldNeverHappen(String msg) throws Error { throw new Error(msg); // Programming error: bail out. } /** * Same as shouldNeverHappen(), but provide a source exception. * * @param e * the exception that helped identify the problem * @throws Error * always throws one. */ private void shouldNeverHappen(Exception e) throws Error { throw new Error(e.toString()); // Programming error: bail out. } /** * Check if a string is a valid JMeter 'expression'. * <p> * The current implementation is very basic: it just accepts any string * containing "${" as a valid expression. * TODO: improve, but keep returning true for "${}". */ private boolean isExpression(String text) { return text.contains("${");//$NON-NLS-1$ } /** * Same as isExpression(String). * * @param text * @return true if text is a String and isExpression(text). */ private boolean isExpression(Object text) { return text instanceof String && isExpression((String) text); } /** * @see java.beans.PropertyEditor#getValue() * @see org.apache.jmeter.testelement.property.JMeterProperty */ @Override public Object getValue() { String text = (String) guiEditor.getValue(); Object value; if (text == null) { if (!acceptsNull) { shouldNeverHappen("Text is null but null is not allowed"); } value = null; } else { if (acceptsExpressions && isExpression(text)) { value = text; } else { // not an expression (isn't or can't be), not null. // a check, just in case: if (!acceptsOther && !isATag(text)) { shouldNeverHappen("Text is not a tag but other entries are not allowed"); } try { // Bug 44314 Number field does not seem to accept "" try { typeEditor.setAsText(text); } catch (NumberFormatException e) { if (text.length() == 0){ text = "0";//$NON-NLS-1$ typeEditor.setAsText(text); } else { shouldNeverHappen(e); } } } catch (IllegalArgumentException e) { shouldNeverHappen(e); } value = typeEditor.getValue(); } } if (log.isDebugEnabled()) { if (value == null) { log.debug("->NULL:null"); } else { log.debug("->{}:{}", value.getClass().getName(), value); } } return value; } @Override public final void setValue(Object value) { /// final because called from ctor String text; if (log.isDebugEnabled()) { if (value == null) { log.debug("<-NULL:null"); } else { log.debug("<-{}:{}", value.getClass().getName(), value); } } if (value == null) { if (!acceptsNull) { throw new IllegalArgumentException("Null is not allowed"); } text = null; } else if (acceptsExpressions && isExpression(value)) { text = (String) value; } else { // Not an expression (isn't or can't be), not null. typeEditor.setValue(value); // may throw IllegalArgumentExc. text = fixGetAsTextBug(typeEditor.getAsText()); if (!acceptsOther && !isATag(text)) { throw new IllegalArgumentException("Value not allowed: '" + text + "' is not in " + Arrays.toString(getTags())); } } guiEditor.setValue(text); } /* * Fix bug in JVMs that return true/false rather than True/False * from the type editor getAsText() method */ private String fixGetAsTextBug(String asText) { if (asText == null){ return null; } if (asText.equals("true")){ log.debug("true=>True");// so we can detect it return "True"; } if (asText.equals("false")){ log.debug("false=>False");// so we can detect it return "False"; } return asText; } @Override public String getAsText() { String text = fixGetAsTextBug(guiEditor.getAsText()); if (text == null) { if (!acceptsNull) { shouldNeverHappen("Text is null, but null is not allowed"); } } else if (!acceptsExpressions || !isExpression(text)) { // not an expression (can't be or isn't), not null. try { typeEditor.setAsText(text); // ensure value is propagated to editor } catch (IllegalArgumentException e) { shouldNeverHappen(e); } text = fixGetAsTextBug(typeEditor.getAsText()); // a check, just in case: if (!acceptsOther && !isATag(text)) { shouldNeverHappen("Text is not a tag, but other values are not allowed"); } } log.debug("->\"{}\"", text); return text; } @Override public void setAsText(String text) throws IllegalArgumentException { if (log.isDebugEnabled()) { if (text == null) { log.debug("<-null"); } else { log.debug("<-\"{}\"", text); } } String value; if (text == null) { if (!acceptsNull) { throw new IllegalArgumentException("Null parameter not allowed"); } value = null; } else { if (acceptsExpressions && isExpression(text)) { value = text; } else { // Some editors do tiny transformations (e.g. "true" to // "True",...): typeEditor.setAsText(text); // may throw IllegalArgumentException value = typeEditor.getAsText(); if (!acceptsOther && !isATag(text)) { throw new IllegalArgumentException("Value not allowed: "+text); } } } guiEditor.setValue(value); } @Override public void propertyChange(PropertyChangeEvent event) { String text = fixGetAsTextBug(guiEditor.getAsText()); if (isValidValue(text)) { lastValidValue = text; firePropertyChange(); } else { if (GuiPackage.getInstance() == null){ log.warn("Invalid value: {} {}", text, typeEditor); } else { JOptionPane.showMessageDialog(guiEditor.getCustomEditor().getParent(), JMeterUtils.getResString("property_editor.value_is_invalid_message"),//$NON-NLS-1$ JMeterUtils.getResString("property_editor.value_is_invalid_title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE); } // Revert to the previous value: guiEditor.setAsText(lastValidValue); } } public void addChangeListener(PropertyChangeListener listener) { guiEditor.addPropertyChangeListener(listener); } }