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