SchemaFeaturesEditor.java
001 /*
002  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
003  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  *
005  *  This file is part of GATE (see http://gate.ac.uk/), and is free
006  *  software, licenced under the GNU Library General Public License,
007  *  Version 2, June 1991 (in the distribution as file licence.html,
008  *  and also available at http://gate.ac.uk/gate/licence.html).
009  *
010  *  SchemaFeaturesEditor.java
011  *
012  *  Valentin Tablan, Sep 11, 2007
013  *
014  *  $Id: SchemaFeaturesEditor.java 17874 2014-04-18 11:19:47Z markagreenwood $
015  */
016 package gate.gui.annedit;
017 
018 import gate.FeatureMap;
019 import gate.creole.AnnotationSchema;
020 import gate.creole.FeatureSchema;
021 import gate.swing.JChoice;
022 
023 import java.awt.Color;
024 import java.awt.Component;
025 import java.awt.GridBagConstraints;
026 import java.awt.GridBagLayout;
027 import java.awt.Insets;
028 import java.awt.event.ActionEvent;
029 import java.awt.event.ActionListener;
030 import java.util.Arrays;
031 import java.util.HashSet;
032 import java.util.LinkedHashMap;
033 import java.util.Map;
034 import java.util.Set;
035 
036 import javax.swing.BorderFactory;
037 import javax.swing.Box;
038 import javax.swing.BoxLayout;
039 import javax.swing.JCheckBox;
040 import javax.swing.JComponent;
041 import javax.swing.JLabel;
042 import javax.swing.JPanel;
043 import javax.swing.border.Border;
044 import javax.swing.event.DocumentEvent;
045 import javax.swing.event.DocumentListener;
046 
047 /**
048  * A GUI component for editing a feature map based on a feature schema object.
049  */
050 @SuppressWarnings("serial")
051 public class SchemaFeaturesEditor extends JPanel{
052 
053   protected static enum FeatureType{
054     /**
055      * Type for features that have a range of possible values 
056      */
057     nominal, 
058     
059     /**
060      * Type for boolean features.
061      */
062     bool, 
063     
064     /**
065      * Type for free text features.
066      */
067     text};
068 
069   protected class FeatureEditor{
070     
071     /**
072      * Constructor for nominal features
073      @param featureName
074      @param values
075      @param defaultValue
076      */
077     public FeatureEditor(String featureName, String[] values, 
078             String defaultValue){
079       this.featureName = featureName;
080       this.type = FeatureType.nominal;
081       this.values = values;
082       this.defaultValue = defaultValue;
083       buildGui();
084     }
085     
086     /**
087      * Constructor for boolean features
088      @param featureName
089      @param defaultValue
090      */
091     public FeatureEditor(String featureName, Boolean defaultValue){
092       this.featureName = featureName;
093       this.type = FeatureType.bool;
094       if (defaultValue != null )
095           this.defaultValue = defaultValue.booleanValue() ? BOOLEAN_TRUE : BOOLEAN_FALSE;
096       else
097           this.defaultValue = null;
098       this.values = new String[]{BOOLEAN_FALSE, BOOLEAN_TRUE};
099       buildGui();
100     }
101     
102     /**
103      * Constructor for plain text features
104      @param featureName
105      @param defaultValue
106      */
107     public FeatureEditor(String featureName, String defaultValue){
108       this.featureName = featureName;
109       this.type = FeatureType.text;
110       this.defaultValue = defaultValue;
111       this.values = null;
112       buildGui();
113     }
114     
115     /**
116      * Builds the GUI according to the internally stored values.
117      */
118     protected void buildGui(){
119       //prepare the action listener
120       sharedActionListener = new ActionListener(){
121         @Override
122         public void actionPerformed(ActionEvent e) {          
123           Object newValue = null;
124           if(e.getSource() == checkbox){
125             newValue = new Boolean(checkbox.isSelected());
126           }else if(e.getSource() == textField){
127             newValue = textField.getText();
128           }else if(e.getSource() == jchoice){
129             newValue = jchoice.getSelectedItem();
130             if(newValue != null && type == FeatureType.bool){
131               //convert eh new value to Boolean
132               newValue = new Boolean(BOOLEAN_TRUE == newValue);
133             }
134           }else if(e.getSource() == SchemaFeaturesEditor.this){
135             //synthetic event
136             newValue = getValue();
137           }
138           
139           if(featureMap != null && e.getSource() != SchemaFeaturesEditor.this){
140             if(newValue != null){
141               if(newValue != featureMap.get(featureName)){ 
142                 featureMap.put(featureName, newValue);
143               }
144             }else{
145               featureMap.remove(featureName);
146             }
147           }
148           
149           
150           //if the change makes this feature map non schema-compliant,
151           //highlight this feature editor
152           if(required && newValue == null){
153             if(getGui().getBorder() != highlightBorder){ 
154               getGui().setBorder(highlightBorder);
155             }
156           }else{
157             if(getGui().getBorder() != defaultBorder){
158               getGui().setBorder(defaultBorder);
159             }
160           }
161         }
162       };
163       
164       //build the empty shell
165       gui = new JPanel();
166       gui.setAlignmentX(Component.LEFT_ALIGNMENT);
167       gui.setLayout(new BoxLayout(gui, BoxLayout.Y_AXIS));
168       switch(type) {
169         case nominal:
170           //use JChoice
171           jchoice = new JChoice<String>(values);
172           jchoice.setDefaultButtonMargin(new Insets(0202));
173           jchoice.setMaximumFastChoices(20);
174           jchoice.setMaximumWidth(300);
175           jchoice.setSelectedItem(value);
176           jchoice.addActionListener(sharedActionListener);
177           gui.add(jchoice);
178           break;
179         case bool:
180           //new implementation -> use JChoice instead of JCheckBox in order
181           //to allow "unset" value (i.e. null)
182           jchoice = new JChoice<String>(values);
183           jchoice.setDefaultButtonMargin(new Insets(0202));
184           jchoice.setMaximumFastChoices(20);
185           jchoice.setMaximumWidth(300);
186           if (BOOLEAN_TRUE.equals(value))
187             jchoice.setSelectedItem(BOOLEAN_TRUE);
188           else if (BOOLEAN_FALSE.equals(value))
189             jchoice.setSelectedItem(BOOLEAN_FALSE);
190           else
191             jchoice.setSelectedItem(null);
192           jchoice.addActionListener(sharedActionListener);
193           gui.add(jchoice);
194           break;
195           
196 //        case bool:
197 //          gui.setLayout(new BoxLayout(gui, BoxLayout.LINE_AXIS));
198 //          checkbox = new JCheckBox();
199 //          checkbox.addActionListener(sharedActionListener);
200 //          if(defaultValue != null){ 
201 //            checkbox.setSelected(Boolean.parseBoolean(defaultValue));
202 //          }
203 //          gui.add(checkbox);
204 //          break;
205         case text:
206           gui.setLayout(new BoxLayout(gui, BoxLayout.LINE_AXIS));
207           textField = new JNullableTextField();
208           textField.setColumns(20);
209           if(value != null){
210             textField.setText(value);
211           }else if(defaultValue != null){
212             textField.setText(defaultValue);
213           }
214           textField.addDocumentListener(new DocumentListener(){
215             @Override
216             public void changedUpdate(DocumentEvent e) {
217               sharedActionListener.actionPerformed(
218                       new ActionEvent(textField, ActionEvent.ACTION_PERFORMED, 
219                               null));
220             }
221             @Override
222             public void insertUpdate(DocumentEvent e) {
223               sharedActionListener.actionPerformed(
224                       new ActionEvent(textField, ActionEvent.ACTION_PERFORMED, 
225                               null));
226             }
227             @Override
228             public void removeUpdate(DocumentEvent e) {
229               sharedActionListener.actionPerformed(
230                       new ActionEvent(textField, ActionEvent.ACTION_PERFORMED, 
231                               null));
232             }
233           });
234           gui.add(textField);          
235           break;
236       }
237       
238       defaultBorder = BorderFactory.createEmptyBorder(2222);
239       highlightBorder = BorderFactory.createLineBorder(Color.RED, 2);
240       gui.setBorder(defaultBorder);
241     }
242     
243     protected JNullableTextField textField;
244     protected JCheckBox checkbox;
245     protected JChoice<String> jchoice;
246     
247     protected Border defaultBorder;
248     
249     protected Border highlightBorder;
250     
251     
252     /**
253      * The type of the feature.
254      */
255     protected FeatureType type;
256     
257     /**
258      * The name of the feature
259      */
260     protected String featureName;
261     
262     /**
263      
264      * The GUI used for editing the feature.
265      */
266     protected JComponent gui;
267     
268     /**
269      * Permitted values for nominal features. 
270      */
271     protected String[] values;
272     
273     /**
274      * Is this feature required
275      */
276     protected boolean required;
277     
278     /**
279      * The action listener that acts upon UI actions on nay of the widgets.
280      */
281     protected ActionListener sharedActionListener;
282     
283     /**
284      * Default value as string.
285      */
286     protected String defaultValue;
287     
288     /**
289      * The value of the feature
290      */
291     protected String value;
292     
293     /**
294      @return the type
295      */
296     public FeatureType getType() {
297       return type;
298     }
299     /**
300      @param type the type to set
301      */
302     public void setType(FeatureType type) {
303       this.type = type;
304     }
305     /**
306      @return the values
307      */
308     public String[] getValues() {
309       return values;
310     }
311     /**
312      @param values the values to set
313      */
314     public void setValues(String[] values) {
315       this.values = values;
316     }
317     /**
318      @return the defaultValue
319      */
320     public String getDefaultValue() {
321       return defaultValue;
322     }
323     
324     /**
325      @param defaultValue the defaultValue to set
326      */
327     public void setDefaultValue(String defaultValue) {
328       this.defaultValue = defaultValue;
329     }
330     
331     /**
332      * Sets the value for this feature
333      @param value
334      */
335     /**
336      @param value
337      */
338     public void setValue(String value) {
339       // cache the actually provided value: if the value is null, we need to 
340       // know, as the text editor would return "" when asked rather than null
341       this.value = value;
342       switch(type){
343         case nominal:
344           jchoice.setSelectedItem(value);
345           break;
346         case bool:
347           if (BOOLEAN_TRUE.equals(value))
348             jchoice.setSelectedItem(BOOLEAN_TRUE);
349           else if (BOOLEAN_FALSE.equals(value))
350             jchoice.setSelectedItem(BOOLEAN_FALSE);
351           else
352             jchoice.setSelectedItem(null);
353           break;          
354 //        case bool:
355 //          checkbox.setSelected(value != null && Boolean.parseBoolean(value));
356 //          break;
357         case text:
358           textField.setText(value);
359           break;
360       }
361       //call the action listener to update the border
362       sharedActionListener.actionPerformed(
363               new ActionEvent(SchemaFeaturesEditor.this, 
364               ActionEvent.ACTION_PERFORMED, ""));
365     }
366 
367     public Object getValue(){
368       switch(type){
369         case nominal:
370           return jchoice.getSelectedItem();
371         case bool:
372           Object choiceValue = jchoice.getSelectedItem();        
373           return choiceValue == null null 
374             new Boolean(choiceValue == BOOLEAN_TRUE);
375 //        case bool:
376 //          return new Boolean(checkbox.isSelected());
377         case text:
378           return textField.getText();
379         default:
380           return null;
381       }
382     }
383     /**
384      @return the featureName
385      */
386     public String getFeatureName() {
387       return featureName;
388     }
389     /**
390      @param featureName the featureName to set
391      */
392     public void setFeatureName(String featureName) {
393       this.featureName = featureName;
394     }
395     
396     /**
397      @return the gui
398      */
399     public JComponent getGui() {
400       if(gui == nullbuildGui();
401       return gui;
402     }
403 
404     /**
405      @return the required
406      */
407     public boolean isRequired() {
408       return required;
409     }
410 
411     /**
412      @param required the required to set
413      */
414     public void setRequired(boolean required) {
415       this.required = required;
416     }
417 
418   }
419   
420   public SchemaFeaturesEditor(AnnotationSchema schema){
421     this.schema = schema;
422     featureSchemas = new LinkedHashMap<String, FeatureSchema>();
423     if(schema != null && schema.getFeatureSchemaSet() != null){
424       for(FeatureSchema aFSchema : schema.getFeatureSchemaSet()){
425         featureSchemas.put(aFSchema.getFeatureName(), aFSchema);
426       }
427     }
428     initGui();
429   }
430     
431   protected void initGui(){
432     setLayout(new GridBagLayout());   
433     GridBagConstraints constraints = new GridBagConstraints();
434     constraints.anchor = GridBagConstraints.WEST;
435     constraints.fill = GridBagConstraints.BOTH;
436     constraints.insets = new Insets(2,2,2,2);
437     constraints.weightx = 0;
438     constraints.weighty = 0;
439     int gridy = 0;
440     constraints.gridx = GridBagConstraints.RELATIVE;
441 
442     
443     //build the feature editors
444     featureEditors = new LinkedHashMap<String, FeatureEditor>();
445     Set<FeatureSchema> fsSet = schema.getFeatureSchemaSet();
446     if(fsSet != null){
447       for(FeatureSchema aFeatureSchema : fsSet){
448         String aFeatureName = aFeatureSchema.getFeatureName();
449         String defaultValue = aFeatureSchema.getFeatureValue();
450         if(defaultValue != null && defaultValue.length() == 0
451           defaultValue = null;
452         String[] valuesArray = null;
453         Set <Object>values = aFeatureSchema.getPermittedValues();
454         if(values != null && values.size() 0){
455           valuesArray = new String[values.size()];
456           int i = 0;
457           for(Object aValue : values){
458             valuesArray[i++= aValue.toString();
459           }
460           Arrays.sort(valuesArray);
461         }
462         //build the right editor for the current feature
463         FeatureEditor anEditor;
464         if(valuesArray != null && valuesArray.length > 0){
465           //we have a set of allowed values -> nominal feature
466           anEditor = new FeatureEditor(aFeatureName, valuesArray, 
467                   defaultValue);
468         }else{
469           //we don't have any permitted set of values specified
470           if(aFeatureSchema.getFeatureValueClass().equals(Boolean.class)){
471             //boolean value
472             Boolean tValue = null;
473             if (BOOLEAN_FALSE.equals(defaultValue))
474               tValue = false;
475             else if (BOOLEAN_TRUE.equals(defaultValue))
476               tValue = true;
477                          
478             anEditor = new FeatureEditor(aFeatureName, tValue);
479           }else{
480             //plain text value
481             anEditor = new FeatureEditor(aFeatureName, defaultValue);
482           }
483         }
484         anEditor.setRequired(aFeatureSchema.isRequired());
485         featureEditors.put(aFeatureName, anEditor);
486       }
487     }
488     //add the feature editors in the alphabetical order
489     for(String featureName : featureEditors.keySet()){
490       FeatureEditor featureEditor = featureEditors.get(featureName);
491       constraints.gridy = gridy++;
492       JLabel nameLabel = new JLabel(
493               "<html>" + featureName + 
494               (featureEditor.isRequired() "<b><font color='red'>*</font></b>: " ": "+
495               "</html>");
496       add(nameLabel, constraints);
497       constraints.weightx = 1;
498       add(featureEditor.getGui(), constraints);
499       constraints.weightx = 0;
500 //      //add a horizontal spacer
501 //      constraints.weightx = 1;
502 //      add(Box.createHorizontalGlue(), constraints);
503 //      constraints.weightx = 0;
504     }
505     //add a vertical spacer
506     constraints.weighty = 1;
507     constraints.gridy = gridy++;
508     constraints.gridx = GridBagConstraints.LINE_START;
509     add(Box.createVerticalGlue(), constraints);
510   }
511   
512   /**
513    * Method called to initiate editing of a new feature map.
514    @param featureMap
515    */
516   public void editFeatureMap(FeatureMap featureMap){
517     this.featureMap = featureMap;
518     featureMapUpdated();
519   }
520   
521   /* (non-Javadoc)
522    * @see gate.event.FeatureMapListener#featureMapUpdated()
523    */
524   public void featureMapUpdated() {
525     //the underlying F-map was changed
526     // 1) validate that known features are schema-compliant
527     if(featureMap != null){
528       for(Object aFeatureName : new HashSet<Object>(featureMap.keySet())){
529         //first check if the feature is allowed
530         if(featureSchemas.keySet().contains(aFeatureName)){
531           FeatureSchema fSchema = featureSchemas.get(aFeatureName);
532           Object aFeatureValue = featureMap.get(aFeatureName);
533           //check if the value is permitted
534           Class<?> featureValueClass = fSchema.getFeatureValueClass()
535           if(featureValueClass.equals(Boolean.class||
536              featureValueClass.equals(Integer.class||
537              featureValueClass.equals(Short.class||
538              featureValueClass.equals(Byte.class||
539              featureValueClass.equals(Float.class||
540              featureValueClass.equals(Double.class)){
541             //just check the right type
542             if(!featureValueClass.isAssignableFrom(aFeatureValue.getClass())){
543               //invalid value type
544               featureMap.remove(aFeatureName);
545             }
546           }else if(featureValueClass.equals(String.class)){
547             if(fSchema.getPermittedValues() != null &&
548                     !fSchema.getPermittedValues().contains(aFeatureValue)){
549                    //invalid value
550                    featureMap.remove(aFeatureName);
551                  }
552           }
553         }else{
554           //feature not permitted -> ignore
555 //          featureMap.remove(aFeatureName);
556         }
557       }
558     }
559     // 2) then update all the displays
560     for(String featureName : featureEditors.keySet()){
561 //      FeatureSchema fSchema = featureSchemas.get(featureName);
562       FeatureEditor aFeatureEditor = featureEditors.get(featureName);
563       Object featureValue = featureMap == null 
564               null : featureMap.get(featureName);
565       if(featureValue == null){
566         //we don't have a value from the featureMap
567         //use the default
568         featureValue = aFeatureEditor.getDefaultValue();
569         //if we still don't have a value, use the last used value
570 //        if(featureValue == null ||
571 //           ( featureValue instanceof String && 
572 //             ((String)featureValue).length() == 0 
573 //           ) ){
574 //          featureValue = aFeatureEditor.getValue();
575 //        }
576         if(featureValue != null && featureMap != null){
577           //we managed to find a relevant value -> save it in the feature map
578           featureMap.put(featureName, featureValue);
579         }
580       }else{
581         
582         //Some values need converting to String
583         FeatureSchema fSchema = featureSchemas.get(featureName);
584         Class<?> featureValueClass = fSchema.getFeatureValueClass();
585         if(featureValueClass.equals(Boolean.class)){
586             featureValue = ((Boolean)featureValue).booleanValue() ?
587                     BOOLEAN_TRUE : BOOLEAN_FALSE;
588         }else if(featureValueClass.equals(String.class)){
589           //already a String - nothing to do
590         }else{
591           //some other type
592           featureValue = featureValue.toString();
593         }
594       }
595       aFeatureEditor.setValue((String)featureValue);
596     }
597   }
598   
599   
600   /**
601    * Label for the <tt>true</tt> boolean value.
602    */
603   private static final String BOOLEAN_TRUE = "True";
604 
605   /**
606    * Label for the <tt>false</tt> boolean value.
607    */
608   private static final String BOOLEAN_FALSE = "False";
609 
610   
611   /**
612    * The feature schema for this editor
613    */
614   protected AnnotationSchema schema;
615   
616   /**
617    * Stored the individual feature schemas, indexed by name. 
618    */
619   protected Map<String, FeatureSchema> featureSchemas;
620   
621   /**
622    * The feature map currently being edited.
623    */
624   protected FeatureMap featureMap;
625   
626 
627   /**
628    * A Map storing the editor for each feature.
629    */
630   protected Map<String, FeatureEditor> featureEditors;
631 }