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.awt.GridBagConstraints;
021: import java.awt.GridBagLayout;
022: import java.awt.Insets;
023: import java.beans.BeanInfo;
024: import java.beans.PropertyDescriptor;
025: import java.beans.PropertyEditor;
026: import java.beans.PropertyEditorManager;
027: import java.io.Serializable;
028: import java.text.MessageFormat;
029: import java.util.Arrays;
030: import java.util.Comparator;
031: import java.util.Map;
032: import java.util.MissingResourceException;
033: import java.util.ResourceBundle;
034:
035: import javax.swing.BorderFactory;
036: import javax.swing.Box;
037: import javax.swing.JLabel;
038: import javax.swing.JPanel;
039: import javax.swing.JScrollPane;
040:
041: import org.apache.jmeter.util.JMeterUtils;
042: import org.apache.jorphan.logging.LoggingManager;
043: import org.apache.log.Logger;
044:
045: /**
046: * The GenericTestBeanCustomizer is designed to provide developers with a
047: * mechanism to quickly implement GUIs for new components.
048: * <p>
049: * It allows editing each of the public exposed properties of the edited type 'a
050: * la JavaBeans': as far as the types of those properties have an associated
051: * editor, there's no GUI development required.
052: * <p>
053: * This class understands the following PropertyDescriptor attributes:
054: * <dl>
055: * <dt>group: String</dt>
056: * <dd>Group under which the property should be shown in the GUI. The string is
057: * also used as a group title (but see comment on resourceBundle below). The
058: * default group is "".</dd>
059: * <dt>order: Integer</dt>
060: * <dd>Order in which the property will be shown in its group. A smaller
061: * integer means higher up in the GUI. The default order is 0. Properties of
062: * equal order are sorted alphabetically.</dd>
063: * <dt>tags: String[]</dt>
064: * <dd>List of values to be offered for the property in addition to those
065: * offered by its property editor.</dd>
066: * <dt>notUndefined: Boolean</dt>
067: * <dd>If true, the property should not be left undefined. A <b>default</b>
068: * attribute must be provided if this is set.</dd>
069: * <dd>notExpression: Boolean</dd>
070: * <dd>If true, the property content should always be constant: JMeter
071: * 'expressions' (strings using ${var}, etc...) can't be used.</dt>
072: * <dd>notOther: Boolean</dd>
073: * <dd>If true, the property content must always be one of the tags values or
074: * null.</dt>
075: * <dt>default: Object</dt>
076: * <dd>Initial value for the property's GUI. Must be provided and be non-null
077: * if <b>notUndefined</b> is set. Must be one of the provided tags (or null) if
078: * <b>notOther</b> is set.
079: * </dl>
080: * <p>
081: * The following BeanDescriptor attributes are also understood:
082: * <dl>
083: * <dt>group.<i>group</i>.order: Integer</dt>
084: * <dd>where <b><i>group</i></b> is a group name used in a <b>group</b>
085: * attribute in one or more PropertyDescriptors. Defines the order in which the
086: * group will be shown in the GUI. A smaller integer means higher up in the GUI.
087: * The default order is 0. Groups of equal order are sorted alphabetically.</dd>
088: * <dt>resourceBundle: ResourceBundle</dt>
089: * <dd>A resource bundle to be used for GUI localization: group display names
090: * will be obtained from property "<b><i>group</i>.displayName</b>" if
091: * available (where <b><i>group</i></b> is the group name).
092: * </dl>
093: *
094: * @author <a href="mailto:jsalvata@apache.org">Jordi Salvat i Alabart</a>
095: */
096: public class GenericTestBeanCustomizer extends JPanel implements
097: SharedCustomizer {
098: private static final Logger log = LoggingManager
099: .getLoggerForClass();
100:
101: public static final String GROUP = "group"; //$NON-NLS-1$
102:
103: public static final String ORDER = "order"; //$NON-NLS-1$
104:
105: public static final String TAGS = "tags"; //$NON-NLS-1$
106:
107: public static final String NOT_UNDEFINED = "notUndefined"; //$NON-NLS-1$
108:
109: public static final String NOT_EXPRESSION = "notExpression"; //$NON-NLS-1$
110:
111: public static final String NOT_OTHER = "notOther"; //$NON-NLS-1$
112:
113: public static final String DEFAULT = "default"; //$NON-NLS-1$
114:
115: public static final String RESOURCE_BUNDLE = "resourceBundle"; //$NON-NLS-1$
116:
117: public static final String ORDER(String group) {
118: return "group." + group + ".order";
119: }
120:
121: public static final String DEFAULT_GROUP = "";
122:
123: private int scrollerCount = 0;
124:
125: /**
126: * BeanInfo object for the class of the objects being edited.
127: */
128: private transient BeanInfo beanInfo;
129:
130: /**
131: * Property descriptors from the beanInfo.
132: */
133: private transient PropertyDescriptor[] descriptors;
134:
135: /**
136: * Property editors -- or null if the property can't be edited. Unused if
137: * customizerClass==null.
138: */
139: private transient PropertyEditor[] editors;
140:
141: /**
142: * Message format for property field labels:
143: */
144: private MessageFormat propertyFieldLabelMessage;
145:
146: /**
147: * Message format for property tooltips:
148: */
149: private MessageFormat propertyToolTipMessage;
150:
151: /**
152: * The Map we're currently customizing. Set by setObject().
153: */
154: private Map propertyMap;
155:
156: public GenericTestBeanCustomizer() {
157: log.warn("Constructor only intended for use in testing"); // $NON-NLS-1$
158: }
159:
160: /**
161: * Create a customizer for a given test bean type.
162: *
163: * @param testBeanClass
164: * a subclass of TestBean
165: * @see org.apache.jmeter.testbeans.TestBean
166: */
167: GenericTestBeanCustomizer(BeanInfo beanInfo) {
168: super ();
169:
170: this .beanInfo = beanInfo;
171:
172: // Get and sort the property descriptors:
173: descriptors = beanInfo.getPropertyDescriptors();
174: Arrays.sort(descriptors, new PropertyComparator());
175:
176: // Obtain the propertyEditors:
177: editors = new PropertyEditor[descriptors.length];
178: for (int i = 0; i < descriptors.length; i++) {
179: String name = descriptors[i].getName();
180:
181: // Don't get editors for hidden or non-read-write properties:
182: if (descriptors[i].isHidden()
183: || (descriptors[i].isExpert() && !JMeterUtils
184: .isExpertMode())
185: || descriptors[i].getReadMethod() == null
186: || descriptors[i].getWriteMethod() == null) {
187: log.debug("No editor for property " + name);
188: editors[i] = null;
189: continue;
190: }
191:
192: PropertyEditor propertyEditor;
193: Class editorClass = descriptors[i].getPropertyEditorClass();
194:
195: if (log.isDebugEnabled()) {
196: log.debug("Property " + name + " has editor class "
197: + editorClass);
198: }
199:
200: if (editorClass != null) {
201: try {
202: propertyEditor = (PropertyEditor) editorClass
203: .newInstance();
204: } catch (InstantiationException e) {
205: log.error("Can't create property editor.", e);
206: throw new Error(e.toString());
207: } catch (IllegalAccessException e) {
208: log.error("Can't create property editor.", e);
209: throw new Error(e.toString());
210: }
211: } else {
212: Class c = descriptors[i].getPropertyType();
213: propertyEditor = PropertyEditorManager.findEditor(c);
214: }
215:
216: if (log.isDebugEnabled()) {
217: log.debug("Property " + name + " has property editor "
218: + propertyEditor);
219: }
220:
221: if (propertyEditor == null) {
222: log.debug("No editor for property " + name);
223: editors[i] = null;
224: continue;
225: }
226:
227: if (!propertyEditor.supportsCustomEditor()) {
228: propertyEditor = createWrapperEditor(propertyEditor,
229: descriptors[i]);
230:
231: if (log.isDebugEnabled()) {
232: log.debug("Editor for property " + name
233: + " is wrapped in " + propertyEditor);
234: }
235: }
236: if (propertyEditor.getCustomEditor() instanceof JScrollPane) {
237: scrollerCount++;
238: }
239:
240: editors[i] = propertyEditor;
241:
242: // Initialize the editor with the provided default value or null:
243: setEditorValue(i, descriptors[i].getValue(DEFAULT));
244:
245: }
246:
247: // Obtain message formats:
248: propertyFieldLabelMessage = new MessageFormat(JMeterUtils
249: .getResString("property_as_field_label")); //$NON-NLS-1$
250: propertyToolTipMessage = new MessageFormat(JMeterUtils
251: .getResString("property_tool_tip")); //$NON-NLS-1$
252:
253: // Initialize the GUI:
254: init();
255: }
256:
257: /**
258: * Find the default typeEditor and a suitable guiEditor for the given
259: * property descriptor, and combine them in a WrapperEditor.
260: *
261: * @param typeEditor
262: * @param descriptor
263: * @return
264: */
265: private WrapperEditor createWrapperEditor(
266: PropertyEditor typeEditor, PropertyDescriptor descriptor) {
267: String[] editorTags = typeEditor.getTags();
268: String[] additionalTags = (String[]) descriptor.getValue(TAGS);
269: String[] tags = null;
270: if (editorTags == null)
271: tags = additionalTags;
272: else if (additionalTags == null)
273: tags = editorTags;
274: else {
275: tags = new String[editorTags.length + additionalTags.length];
276: int j = 0;
277: for (int i = 0; i < editorTags.length; i++)
278: tags[j++] = editorTags[i];
279: for (int i = 0; i < additionalTags.length; i++)
280: tags[j++] = additionalTags[i];
281: }
282:
283: boolean notNull = Boolean.TRUE.equals(descriptor
284: .getValue(NOT_UNDEFINED));
285: boolean notExpression = Boolean.TRUE.equals(descriptor
286: .getValue(NOT_EXPRESSION));
287: boolean notOther = Boolean.TRUE.equals(descriptor
288: .getValue(NOT_OTHER));
289:
290: PropertyEditor guiEditor;
291: if (notNull && tags == null) {
292: guiEditor = new FieldStringEditor();
293: } else {
294: ComboStringEditor e = new ComboStringEditor();
295: e.setNoUndefined(notNull);
296: e.setNoEdit(notExpression && notOther);
297: e.setTags(tags);
298:
299: guiEditor = e;
300: }
301:
302: WrapperEditor wrapper = new WrapperEditor(typeEditor,
303: guiEditor, !notNull, // acceptsNull
304: !notExpression, // acceptsExpressions
305: !notOther, // acceptsOther
306: descriptor.getValue(DEFAULT));
307:
308: return wrapper;
309: }
310:
311: /**
312: * Set the value of the i-th property, properly reporting a possible
313: * failure.
314: *
315: * @param i
316: * the index of the property in the descriptors and editors
317: * arrays
318: * @param value
319: * the value to be stored in the editor
320: *
321: * @throws IllegalArgumentException
322: * if the editor refuses the value
323: */
324: private void setEditorValue(int i, Object value)
325: throws IllegalArgumentException {
326: editors[i].setValue(value);
327: }
328:
329: /*
330: * (non-Javadoc)
331: *
332: * @see org.apache.jmeter.gui.JMeterGUIComponent#configure(org.apache.jmeter.testelement.TestElement)
333: */
334: public void setObject(Object map) {
335: propertyMap = (Map) map;
336:
337: if (propertyMap.size() == 0) {
338: // Uninitialized -- set it to the defaults:
339: for (int i = 0; i < editors.length; i++) {
340: Object value = descriptors[i].getValue(DEFAULT);
341: String name = descriptors[i].getName();
342: if (value != null) {
343: propertyMap.put(name, value);
344: log.debug("Set " + name + "= " + value);
345: }
346: firePropertyChange(name, null, value);
347: }
348: }
349:
350: // Now set the editors to the element's values:
351: for (int i = 0; i < editors.length; i++) {
352: if (editors[i] == null)
353: continue;
354: try {
355: setEditorValue(i, propertyMap.get(descriptors[i]
356: .getName()));
357: } catch (IllegalArgumentException e) {
358: // I guess this can happen as a result of a bad
359: // file read? In this case, it would be better to replace the
360: // incorrect value with anything valid, e.g. the default value
361: // for the property.
362: // But for the time being, I just prefer to be aware of any
363: // problems occuring here, most likely programming errors,
364: // so I'll bail out.
365: // (MS Note) Can't bail out - newly create elements have blank
366: // values and must get the defaults.
367: // Also, when loading previous versions of jmeter test scripts,
368: // some values
369: // may not be right, and should get default values - MS
370: // TODO: review this and possibly change to:
371: setEditorValue(i, descriptors[i].getValue(DEFAULT));
372: }
373: }
374: }
375:
376: // /**
377: // * Find the index of the property of the given name.
378: // *
379: // * @param name
380: // * the name of the property
381: // * @return the index of that property in the descriptors array, or -1 if
382: // * there's no property of this name.
383: // */
384: // private int descriptorIndex(String name) // NOTUSED
385: // {
386: // for (int i = 0; i < descriptors.length; i++) {
387: // if (descriptors[i].getName().equals(name)) {
388: // return i;
389: // }
390: // }
391: // return -1;
392: // }
393:
394: /**
395: * Initialize the GUI.
396: */
397: private void init() {
398: setLayout(new GridBagLayout());
399:
400: GridBagConstraints cl = new GridBagConstraints(); // for labels
401: cl.gridx = 0;
402: cl.anchor = GridBagConstraints.EAST;
403: cl.insets = new Insets(0, 1, 0, 1);
404:
405: GridBagConstraints ce = new GridBagConstraints(); // for editors
406: ce.fill = GridBagConstraints.BOTH;
407: ce.gridx = 1;
408: ce.weightx = 1.0;
409: ce.insets = new Insets(0, 1, 0, 1);
410:
411: GridBagConstraints cp = new GridBagConstraints(); // for panels
412: cp.fill = GridBagConstraints.BOTH;
413: cp.gridx = 1;
414: cp.gridy = GridBagConstraints.RELATIVE;
415: cp.gridwidth = 2;
416: cp.weightx = 1.0;
417:
418: JPanel currentPanel = this ;
419: String currentGroup = DEFAULT_GROUP;
420: int y = 0;
421:
422: for (int i = 0; i < editors.length; i++) {
423: if (editors[i] == null)
424: continue;
425:
426: if (log.isDebugEnabled()) {
427: log
428: .debug("Laying property "
429: + descriptors[i].getName());
430: }
431:
432: String g = group(descriptors[i]);
433: if (!currentGroup.equals(g)) {
434: if (currentPanel != this ) {
435: add(currentPanel, cp);
436: }
437: currentGroup = g;
438: currentPanel = new JPanel(new GridBagLayout());
439: currentPanel.setBorder(BorderFactory
440: .createTitledBorder(BorderFactory
441: .createEtchedBorder(),
442: groupDisplayName(g)));
443: cp.weighty = 0.0;
444: y = 0;
445: }
446:
447: Component customEditor = editors[i].getCustomEditor();
448:
449: boolean multiLineEditor = false;
450: if (customEditor.getPreferredSize().height > 50
451: || customEditor instanceof JScrollPane) {
452: // TODO: the above works in the current situation, but it's
453: // just a hack. How to get each editor to report whether it
454: // wants to grow bigger? Whether the property label should
455: // be at the left or at the top of the editor? ...?
456: multiLineEditor = true;
457: }
458:
459: JLabel label = createLabel(descriptors[i]);
460: label.setLabelFor(customEditor);
461:
462: cl.gridy = y;
463: cl.gridwidth = multiLineEditor ? 2 : 1;
464: cl.anchor = multiLineEditor ? GridBagConstraints.CENTER
465: : GridBagConstraints.EAST;
466: currentPanel.add(label, cl);
467:
468: ce.gridx = multiLineEditor ? 0 : 1;
469: ce.gridy = multiLineEditor ? ++y : y;
470: ce.gridwidth = multiLineEditor ? 2 : 1;
471: ce.weighty = multiLineEditor ? 1.0 : 0.0;
472:
473: cp.weighty += ce.weighty;
474:
475: currentPanel.add(customEditor, ce);
476:
477: y++;
478: }
479: if (currentPanel != this ) {
480: add(currentPanel, cp);
481: }
482:
483: // Add a 0-sized invisible component that will take all the vertical
484: // space that nobody wants:
485: cp.weighty = 0.0001;
486: add(Box.createHorizontalStrut(0), cp);
487: }
488:
489: private JLabel createLabel(PropertyDescriptor desc) {
490: String text = desc.getDisplayName();
491: if (!"".equals(text)) {
492: text = propertyFieldLabelMessage.format(new Object[] { desc
493: .getDisplayName() });
494: }
495: // if the displayName is the empty string, leave it like that.
496: JLabel label = new JLabel(text);
497: label.setHorizontalAlignment(JLabel.TRAILING);
498: text = propertyToolTipMessage.format(new Object[] {
499: desc.getName(), desc.getShortDescription() });
500: label.setToolTipText(text);
501:
502: return label;
503: }
504:
505: /**
506: * Obtain a property descriptor's group.
507: *
508: * @param descriptor
509: * @return the group String.
510: */
511: private String group(PropertyDescriptor d) {
512: String group = (String) d.getValue(GROUP);
513: if (group == null)
514: group = DEFAULT_GROUP;
515: return group;
516: }
517:
518: /**
519: * Obtain a group's display name
520: */
521: private String groupDisplayName(String group) {
522: try {
523: ResourceBundle b = (ResourceBundle) beanInfo
524: .getBeanDescriptor().getValue(RESOURCE_BUNDLE);
525: if (b == null)
526: return group;
527: else
528: return b.getString(group + ".displayName");
529: } catch (MissingResourceException e) {
530: return group;
531: }
532: }
533:
534: /**
535: * Comparator used to sort properties for presentation in the GUI.
536: */
537: private class PropertyComparator implements Comparator,
538: Serializable {
539: public int compare(Object o1, Object o2) {
540: return compare((PropertyDescriptor) o1,
541: (PropertyDescriptor) o2);
542: }
543:
544: private int compare(PropertyDescriptor d1, PropertyDescriptor d2) {
545: int result;
546:
547: String g1 = group(d1), g2 = group(d2);
548: Integer go1 = groupOrder(g1), go2 = groupOrder(g2);
549:
550: result = go1.compareTo(go2);
551: if (result != 0)
552: return result;
553:
554: result = g1.compareTo(g2);
555: if (result != 0)
556: return result;
557:
558: Integer po1 = propertyOrder(d1), po2 = propertyOrder(d2);
559: result = po1.compareTo(po2);
560: if (result != 0)
561: return result;
562:
563: return d1.getName().compareTo(d2.getName());
564: }
565:
566: /**
567: * Obtain a group's order.
568: *
569: * @param group
570: * group name
571: * @return the group's order (zero by default)
572: */
573: private Integer groupOrder(String group) {
574: Integer order = (Integer) beanInfo.getBeanDescriptor()
575: .getValue(ORDER(group));
576: if (order == null)
577: order = new Integer(0);
578: return order;
579: }
580:
581: /**
582: * Obtain a property's order.
583: *
584: * @param d
585: * @return the property's order attribute (zero by default)
586: */
587: private Integer propertyOrder(PropertyDescriptor d) {
588: Integer order = (Integer) d.getValue(ORDER);
589: if (order == null)
590: order = new Integer(0);
591: return order;
592: }
593: }
594:
595: /**
596: * Save values from the GUI fields into the property map
597: */
598: void saveGuiFields() {
599: for (int i = 0; i < editors.length; i++) {
600: PropertyEditor propertyEditor = editors[i]; // might be null (e.g. in testing)
601: if (propertyEditor != null) {
602: Object value = propertyEditor.getValue();
603: String name = descriptors[i].getName();
604: if (value == null) {
605: propertyMap.remove(name);
606: if (log.isDebugEnabled()) {
607: log.debug("Unset " + name);
608: }
609: } else {
610: propertyMap.put(name, value);
611: if (log.isDebugEnabled()) {
612: log.debug("Set " + name + "= " + value);
613: }
614: }
615: }
616: }
617: }
618:
619: void clearGuiFields() {
620: for (int i = 0; i < editors.length; i++) {
621: PropertyEditor propertyEditor = editors[i]; // might be null (e.g. in testing)
622: if (propertyEditor != null) {
623: try {
624: if (propertyEditor instanceof WrapperEditor) {
625: WrapperEditor we = (WrapperEditor) propertyEditor;
626: String tags[] = we.getTags();
627: if (tags != null && tags.length > 0) {
628: we.setAsText(tags[0]);
629: } else {
630: we.setValue("");
631: }
632: } else if (propertyEditor instanceof ComboStringEditor) {
633: ComboStringEditor cse = (ComboStringEditor) propertyEditor;
634: cse.setAsText(cse.getInitialEditValue());
635: } else {
636: propertyEditor.setAsText("");
637: }
638: } catch (IllegalArgumentException ex) {
639: log.error("Failed to set field "
640: + descriptors[i].getName(), ex);
641: }
642: }
643: }
644: }
645:
646: }
|