001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.jmeter.testbeans.gui;
018:
019: import java.awt.Component;
020: import java.beans.PropertyChangeEvent;
021: import java.beans.PropertyChangeListener;
022: import java.beans.PropertyEditor;
023: import java.beans.PropertyEditorSupport;
024:
025: import javax.swing.JOptionPane;
026:
027: import org.apache.jmeter.util.JMeterUtils;
028: import org.apache.jorphan.logging.LoggingManager;
029: import org.apache.log.Logger;
030:
031: /**
032: * This is an implementation of a full-fledged property editor, providing both
033: * object-text transformation and an editor GUI (a custom editor component),
034: * from two simpler property editors providing only one of these functionalities
035: * each, namely:
036: * <dl>
037: * <dt>typeEditor
038: * <dt>
039: * <dd>Provides suitable object-to-string and string-to-object transformation
040: * for the property's type. That is: it's a simple editor that only need to
041: * support the set/getAsText and set/getValue methods.</dd>
042: * <dt>guiEditor</dt>
043: * <dd>Provides a suitable GUI for the property, but works on [possibly null]
044: * String values. That is: it supportsCustomEditor, but get/setAsText and
045: * get/setValue are indentical.</dd>
046: * </dl>
047: * <p>
048: * The resulting editor provides optional support for null values (you can
049: * choose whether <b>null</b> is to be a valid property value). It also
050: * provides optional support for JMeter 'expressions' (you can choose whether
051: * they make valid property values).
052: *
053: */
054: class WrapperEditor extends PropertyEditorSupport implements
055: PropertyChangeListener {
056: protected static Logger log = LoggingManager.getLoggerForClass();
057:
058: /**
059: * The type's property editor.
060: */
061: PropertyEditor typeEditor;
062:
063: /**
064: * The gui property editor
065: */
066: PropertyEditor guiEditor;
067:
068: /**
069: * Whether to allow <b>null</b> as a property value.
070: */
071: boolean acceptsNull;
072:
073: /**
074: * Whether to allow JMeter 'expressions' as property values.
075: */
076: boolean acceptsExpressions;
077:
078: /**
079: * Whether to allow any constant values different from the provided tags.
080: */
081: boolean acceptsOther;
082:
083: /**
084: * Keep track of the last valid value in the editor, so that we can revert
085: * to it if the user enters an invalid value.
086: */
087: private String lastValidValue = null;
088:
089: /**
090: * Constructor for use when a PropertyEditor is delegating to us.
091: */
092: WrapperEditor(Object source, PropertyEditor typeEditor,
093: PropertyEditor guiEditor, boolean acceptsNull,
094: boolean acceptsExpressions, boolean acceptsOther,
095: Object defaultValue) {
096: super (source);
097: initialize(typeEditor, guiEditor, acceptsNull,
098: acceptsExpressions, acceptsOther, defaultValue);
099: }
100:
101: /**
102: * Constructor for use for regular instantiation and by subclasses.
103: */
104: WrapperEditor(PropertyEditor typeEditor, PropertyEditor guiEditor,
105: boolean acceptsNull, boolean acceptsExpressions,
106: boolean acceptsOther, Object defaultValue) {
107: super ();
108: initialize(typeEditor, guiEditor, acceptsNull,
109: acceptsExpressions, acceptsOther, defaultValue);
110: }
111:
112: private void initialize(PropertyEditor _typeEditor,
113: PropertyEditor _guiEditor, boolean _acceptsNull,
114: boolean _acceptsExpressions, boolean _acceptsOther,
115: Object defaultValue) {
116: this .typeEditor = _typeEditor;
117: this .guiEditor = _guiEditor;
118: this .acceptsNull = _acceptsNull;
119: this .acceptsExpressions = _acceptsExpressions;
120: this .acceptsOther = _acceptsOther;
121:
122: setValue(defaultValue);
123: lastValidValue = getAsText();
124:
125: if (_guiEditor instanceof ComboStringEditor) {
126: String[] tags = ((ComboStringEditor) _guiEditor).getTags();
127:
128: // Provide an initial edit value if necessary -- this is an
129: // heuristic that tries to provide the most convenient
130: // initial edit value:
131:
132: String v;
133: if (!_acceptsOther)
134: v = "${}";
135: else if (isValidValue(""))
136: v = "";
137: else if (_acceptsExpressions)
138: v = "${}";
139: else if (tags != null && tags.length > 0)
140: v = tags[0];
141: else
142: v = getAsText();
143:
144: ((ComboStringEditor) _guiEditor).setInitialEditValue(v);
145: }
146:
147: _guiEditor.addPropertyChangeListener(this );
148: }
149:
150: public boolean supportsCustomEditor() {
151: return true;
152: }
153:
154: public Component getCustomEditor() {
155: return guiEditor.getCustomEditor();
156: }
157:
158: public String[] getTags() {
159: return guiEditor.getTags();
160: }
161:
162: /**
163: * Determine wheter a string is one of the known tags.
164: *
165: * @param text
166: * @return true iif text equals one of the getTags()
167: */
168: private boolean isATag(String text) {
169: String[] tags = getTags();
170: if (tags == null)
171: return false;
172: for (int i = 0; i < tags.length; i++) {
173: if (tags[i].equals(text))
174: return true;
175: }
176: return false;
177: }
178:
179: /**
180: * Determine whether a string is a valid value for the property.
181: *
182: * @param text
183: * the value to be checked
184: * @return true iif text is a valid value
185: */
186: private boolean isValidValue(String text) {
187: if (text == null)
188: return acceptsNull;
189:
190: if (acceptsExpressions && isExpression(text))
191: return true;
192:
193: // Not an expression (isn't or can't be), not null.
194:
195: // The known tags are assumed to be valid:
196: if (isATag(text))
197: return true;
198:
199: // Was not a tag, so if we can't accept other values...
200: if (!acceptsOther)
201: return false;
202:
203: // Delegate the final check to the typeEditor:
204: try {
205: typeEditor.setAsText(text);
206: } catch (IllegalArgumentException e1) {
207: // setAsText failed: not valid
208: return false;
209: }
210: // setAsText succeeded: valid
211: return true;
212: }
213:
214: /**
215: * This method is used to do some low-cost defensive programming: it is
216: * called when a condition that the program logic should prevent from
217: * happening occurs. I hope this will help early detection of logical bugs
218: * in property value handling.
219: *
220: * @throws Error
221: * always throws an error.
222: */
223: private final void shouldNeverHappen() throws Error {
224: throw new Error(); // Programming error: bail out.
225: }
226:
227: /**
228: * Same as shouldNeverHappen(), but provide a source exception.
229: *
230: * @param e
231: * the exception that helped identify the problem
232: * @throws Error
233: * always throws one.
234: */
235: private final void shouldNeverHappen(Exception e) throws Error {
236: throw new Error(e.toString()); // Programming error: bail out.
237: }
238:
239: /**
240: * Check if a string is a valid JMeter 'expression'.
241: * <p>
242: * The current implementation is very basic: it just accepts any string
243: * containing "${" as a valid expression. TODO: improve, but keep returning
244: * true for "${}".
245: */
246: private final boolean isExpression(String text) {
247: return text.indexOf("${") != -1;
248: }
249:
250: /**
251: * Same as isExpression(String).
252: *
253: * @param text
254: * @return true iif text is a String and isExpression(text).
255: */
256: private final boolean isExpression(Object text) {
257: return text instanceof String && isExpression((String) text);
258: }
259:
260: /**
261: * @see java.beans.PropertyEditor#getValue()
262: * @see org.apache.jmeter.testelement.property.JMeterProperty
263: */
264: public Object getValue() {
265: String text = (String) guiEditor.getValue();
266:
267: Object value;
268:
269: if (text == null) {
270: if (!acceptsNull)
271: shouldNeverHappen();
272: value = null;
273: } else {
274: if (acceptsExpressions && isExpression(text)) {
275: value = text;
276: } else {
277: // not an expression (isn't or can't be), not null.
278:
279: // a check, just in case:
280: if (!acceptsOther && !isATag(text))
281: shouldNeverHappen();
282:
283: try {
284: typeEditor.setAsText(text);
285: } catch (IllegalArgumentException e) {
286: shouldNeverHappen(e);
287: }
288: value = typeEditor.getValue();
289: }
290: }
291:
292: if (log.isDebugEnabled()) {
293: log.debug("->"
294: + (value != null ? value.getClass().getName()
295: : "NULL") + ":" + value);
296: }
297: return value;
298: }
299:
300: public void setValue(Object value) {
301: String text;
302:
303: if (log.isDebugEnabled()) {
304: log.debug("<-"
305: + (value != null ? value.getClass().getName()
306: : "NULL") + ":" + value);
307: }
308:
309: if (value == null) {
310: if (!acceptsNull)
311: throw new IllegalArgumentException(
312: "Null is not allowed");
313: text = null;
314: } else if (acceptsExpressions && isExpression(value)) {
315: text = (String) value;
316: } else {
317: // Not an expression (isn't or can't be), not null.
318: typeEditor.setValue(value); // may throw IllegalArgumentExc.
319: text = typeEditor.getAsText();
320:
321: if (!acceptsOther && !isATag(text))
322: throw new IllegalArgumentException(
323: "Value not allowed: " + text);
324: }
325:
326: guiEditor.setValue(text);
327: }
328:
329: public String getAsText() {
330: String text = guiEditor.getAsText();
331:
332: if (text == null) {
333: if (!acceptsNull)
334: shouldNeverHappen();
335: } else if (!acceptsExpressions || !isExpression(text)) {
336: // not an expression (can't be or isn't), not null.
337: try {
338: typeEditor.setAsText(text);
339: } catch (IllegalArgumentException e) {
340: shouldNeverHappen(e);
341: }
342: text = typeEditor.getAsText();
343:
344: // a check, just in case:
345: if (!acceptsOther && !isATag(text))
346: shouldNeverHappen();
347: }
348:
349: if (log.isDebugEnabled()) {
350: log.debug("->\"" + text + "\"");
351: }
352: return text;
353: }
354:
355: public void setAsText(String text) throws IllegalArgumentException {
356: if (log.isDebugEnabled()) {
357: log.debug(text == null ? "<-null" : "<-\"" + text + "\"");
358: }
359:
360: String value;
361:
362: if (text == null) {
363: if (!acceptsNull)
364: throw new IllegalArgumentException(
365: "Null parameter not allowed");
366: value = null;
367: } else {
368: if (acceptsExpressions && isExpression(text)) {
369: value = text;
370: } else {
371: // Some editors do tiny transformations (e.g. "true" to
372: // "True",...):
373: typeEditor.setAsText(text); // may throw IllegalArgumentException
374: value = typeEditor.getAsText();
375:
376: if (!acceptsOther && !isATag(text))
377: throw new IllegalArgumentException(
378: "Value not allowed: " + text);
379: }
380: }
381:
382: guiEditor.setValue(value);
383: }
384:
385: public void propertyChange(PropertyChangeEvent event) {
386: String text = guiEditor.getAsText();
387: if (isValidValue(text)) {
388: lastValidValue = text;
389: firePropertyChange();
390: } else {
391: // TODO: how to bring the editor back in view & focus?
392: JOptionPane
393: .showMessageDialog(
394: guiEditor.getCustomEditor().getParent(),
395: JMeterUtils
396: .getResString("property_editor.value_is_invalid_message"),
397: JMeterUtils
398: .getResString("property_editor.value_is_invalid_title"),
399: JOptionPane.WARNING_MESSAGE);
400:
401: // Revert to the previous value:
402: guiEditor.setAsText(lastValidValue);
403: }
404: }
405: }
|