DocumentExportMenu.java
001 /*
002  *  Copyright (c) 1995-2014, 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  *  Mark A. Greenwood 11/07/2014
011  *
012  */
013 
014 package gate.gui;
015 
016 import gate.Corpus;
017 import gate.CorpusExporter;
018 import gate.Document;
019 import gate.DocumentExporter;
020 import gate.Factory;
021 import gate.FeatureMap;
022 import gate.Gate;
023 import gate.Resource;
024 import gate.corpora.export.GateXMLExporter;
025 import gate.creole.Parameter;
026 import gate.creole.ParameterException;
027 import gate.creole.ResourceData;
028 import gate.event.CreoleEvent;
029 import gate.event.CreoleListener;
030 import gate.swing.XJFileChooser;
031 import gate.swing.XJMenu;
032 import gate.util.Err;
033 import gate.util.Files;
034 import gate.util.InvalidOffsetException;
035 
036 import java.awt.Dimension;
037 import java.awt.event.ActionEvent;
038 import java.awt.event.KeyEvent;
039 import java.io.File;
040 import java.io.IOException;
041 import java.net.URISyntaxException;
042 import java.net.URL;
043 import java.util.HashSet;
044 import java.util.IdentityHashMap;
045 import java.util.Iterator;
046 import java.util.List;
047 import java.util.Set;
048 
049 import javax.swing.AbstractAction;
050 import javax.swing.Action;
051 import javax.swing.ActionMap;
052 import javax.swing.Box;
053 import javax.swing.BoxLayout;
054 import javax.swing.InputMap;
055 import javax.swing.JButton;
056 import javax.swing.JComponent;
057 import javax.swing.JDialog;
058 import javax.swing.JFileChooser;
059 import javax.swing.JLabel;
060 import javax.swing.JMenuItem;
061 import javax.swing.JOptionPane;
062 import javax.swing.JPanel;
063 import javax.swing.JScrollPane;
064 import javax.swing.JTable;
065 import javax.swing.JTextField;
066 import javax.swing.KeyStroke;
067 import javax.swing.SwingUtilities;
068 import javax.swing.table.TableCellEditor;
069 
070 import org.apache.log4j.Logger;
071 
072 /**
073  * A menu which updates as plugins are (un)loaded to allow the export of
074  * documents and corpora to any of the supported output formats.
075  */
076 @SuppressWarnings("serial")
077 public class DocumentExportMenu extends XJMenu implements CreoleListener {
078 
079   private static final Logger log = Logger.getLogger(DocumentExportMenu.class);
080 
081   static DocumentExportDialog dialog = new DocumentExportDialog();
082 
083   protected IdentityHashMap<Resource, JMenuItem> itemByResource =
084           new IdentityHashMap<Resource, JMenuItem>();
085 
086   private Handle handle;
087 
088   public DocumentExportMenu(NameBearerHandle handle) {
089     super("Save as...""", handle.sListenerProxy);
090     if(!(handle.getTarget() instanceof Document)
091             && !(handle.getTarget() instanceof Corpus))
092       throw new IllegalArgumentException(
093               "We only deal with documents and corpora");
094     this.handle = handle;
095     init();
096   }
097 
098   private void init() {
099 
100     DocumentExporter gateXMLExporter =
101             (DocumentExporter)Gate.getCreoleRegister()
102                     .get(GateXMLExporter.class.getCanonicalName())
103                     .getInstantiations().iterator().next();
104     addExporter(gateXMLExporter);
105 
106     Set<String> toolTypes = Gate.getCreoleRegister().getToolTypes();
107     for(String type : toolTypes) {
108       List<Resource> instances =
109               Gate.getCreoleRegister().get(type).getInstantiations();
110       for(Resource res : instances) {
111         if(res instanceof DocumentExporter) {
112           addExporter((DocumentExporter)res);
113         }
114       }
115     }
116     Gate.getCreoleRegister().addCreoleListener(this);
117   }
118 
119   private File getSelectedFile(List<List<Parameter>> params,
120           DocumentExporter de, FeatureMap options) {
121     File selectedFile = null;
122 
123     Document document =
124             (handle.getTarget() instanceof Document ? (Document)handle
125                     .getTarget() null);
126     // are we looking for a file or a directory?
127     boolean singleFile = (document != null|| (de instanceof CorpusExporter);
128 
129     if(document != null && document.getSourceUrl() != null) {
130       String fileName = "";
131       try {
132         fileName = document.getSourceUrl().toURI().getPath().trim();
133       catch(URISyntaxException e) {
134         fileName = document.getSourceUrl().getPath().trim();
135       }
136       if(fileName.equals(""|| fileName.equals("/")) {
137         if(document.getNamedAnnotationSets().containsKey("Original markups")
138                 && !document.getAnnotations("Original markups").get("title")
139                         .isEmpty()) {
140           // use the title annotation if any
141           try {
142             fileName =
143                     document.getContent()
144                             .getContent(
145                                     document.getAnnotations("Original markups")
146                                             .get("title").firstNode()
147                                             .getOffset(),
148                                     document.getAnnotations("Original markups")
149                                             .get("title").lastNode()
150                                             .getOffset()).toString();
151           catch(InvalidOffsetException e) {
152             e.printStackTrace();
153           }
154         else {
155           fileName = document.getSourceUrl().toString();
156         }
157         // cleans the file name
158         fileName = fileName.replaceAll("/""_");
159       else {
160         // replaces the extension with the default
161         fileName =
162                 fileName.replaceAll("\\.[a-zA-Z]{1,4}$",
163                         "." + de.getDefaultExtension());
164       }
165       // cleans the file name
166       fileName = fileName.replaceAll("[^/a-zA-Z0-9._-]""_");
167       fileName = fileName.replaceAll("__+""_");
168       // adds the default extension if not present
169       if(!fileName.endsWith("." + de.getDefaultExtension())) {
170         fileName += "." + de.getDefaultExtension();
171       }
172 
173       selectedFile = new File(fileName);
174     }
175 
176     if(params == null || params.isEmpty()) {
177       XJFileChooser fileChooser = MainFrame.getFileChooser();
178       fileChooser.resetChoosableFileFilters();
179       fileChooser.setFileFilter(de.getFileFilter());
180       fileChooser.setMultiSelectionEnabled(false);
181       fileChooser.setDialogTitle("Save as " + de.getFileType());
182       fileChooser.setFileSelectionMode(singleFile
183               ? JFileChooser.FILES_ONLY
184               : JFileChooser.DIRECTORIES_ONLY);
185 
186       if(selectedFile != null) {
187         fileChooser.ensureFileIsVisible(selectedFile);
188         fileChooser.setSelectedFile(selectedFile);
189       }
190 
191       if(fileChooser.showSaveDialog(MainFrame.getInstance()) != JFileChooser.APPROVE_OPTION)
192         return null;
193       selectedFile = fileChooser.getSelectedFile();
194     else {
195       if(!dialog.show(de, params, singleFile, selectedFile != null
196               ? selectedFile.getAbsolutePath()
197               "")) return null;
198 
199       options.putAll(dialog.getSelectedParameters());
200       selectedFile = new File(dialog.getSelectedFileName());
201     }
202 
203     return selectedFile;
204   }
205 
206   private void addExporter(final DocumentExporter de) {
207 
208     if(itemByResource.containsKey(de)) return;
209 
210     final ResourceData rd =
211             Gate.getCreoleRegister().get(de.getClass().getCanonicalName());
212 
213     if(DocumentExportMenu.this.getItemCount() == 1) {
214       DocumentExportMenu.this.addSeparator();
215     }
216 
217     JMenuItem item =
218             DocumentExportMenu.this.add(new AbstractAction(de.getFileType()
219                     " (." + de.getDefaultExtension() ")", MainFrame
220                     .getIcon(rd.getIcon())) {
221 
222               @Override
223               public void actionPerformed(ActionEvent ae) {
224 
225                 List<List<Parameter>> params =
226                         rd.getParameterList().getRuntimeParameters();
227 
228                 final FeatureMap options = Factory.newFeatureMap();
229 
230                 final File selectedFile = getSelectedFile(params, de, options);
231 
232                 if(selectedFile == nullreturn;
233 
234                 Runnable runnable = new Runnable() {
235                   public void run() {
236 
237                     if(handle.getTarget() instanceof Document) {
238 
239                       long start = System.currentTimeMillis();
240                       listener.statusChanged("Saving as " + de.getFileType()
241                               " to " + selectedFile.toString() "...");
242                       try {
243                         de.export((Document)handle.getTarget(), selectedFile,
244                                 options);
245                       catch(IOException e) {
246                         e.printStackTrace();
247                       }
248 
249                       long time = System.currentTimeMillis() - start;
250                       listener.statusChanged("Finished saving as "
251                               + de.getFileType() " into " " the file: "
252                               + selectedFile.toString() " in "
253                               ((double)time1000 "s");
254                     else // corpus
255                       if(de instanceof CorpusExporter) {
256 
257                         long start = System.currentTimeMillis();
258                         listener.statusChanged("Saving as " + de.getFileType()
259                                 " to " + selectedFile.toString() "...");
260                         try {
261                           ((CorpusExporter)de).export((Corpus)handle.getTarget(), selectedFile,
262                                   options);
263                         catch(IOException e) {
264                           e.printStackTrace();
265                         }
266 
267                         long time = System.currentTimeMillis() - start;
268                         listener.statusChanged("Finished saving as "
269                                 + de.getFileType() " into " " the file: "
270                                 + selectedFile.toString() " in "
271                                 ((double)time1000 "s");
272                       else // not a CorpusExporter
273                         try {
274                           File dir = selectedFile;
275                           // create the top directory if needed
276                           if(!dir.exists()) {
277                             if(!dir.mkdirs()) {
278                               JOptionPane.showMessageDialog(
279                                       MainFrame.getInstance(),
280                                       "Could not create top directory!""GATE",
281                                       JOptionPane.ERROR_MESSAGE);
282                               return;
283                             }
284                           }
285   
286                           MainFrame.lockGUI("Saving...");
287   
288                           Corpus corpus = (Corpus)handle.getTarget();
289   
290                           // iterate through all the docs and save
291                           // each of
292                           // them
293                           Iterator<Document> docIter = corpus.iterator();
294                           boolean overwriteAll = false;
295                           int docCnt = corpus.size();
296                           int currentDocIndex = 0;
297                           Set<String> usedFileNames = new HashSet<String>();
298                           while(docIter.hasNext()) {
299                             boolean docWasLoaded =
300                                     corpus.isDocumentLoaded(currentDocIndex);
301                             Document currentDoc = docIter.next();
302   
303                             URL sourceURL = currentDoc.getSourceUrl();
304                             String fileName = null;
305                             if(sourceURL != null) {
306                               fileName = sourceURL.getPath();
307                               fileName = Files.getLastPathComponent(fileName);
308                             }
309                             if(fileName == null || fileName.length() == 0) {
310                               fileName = currentDoc.getName();
311                             }
312                             // makes sure that the filename does not
313                             // contain
314                             // any
315                             // forbidden character
316                             fileName =
317                                     fileName.replaceAll("[\\/:\\*\\?\"<>|]""_");
318                             if(fileName.toLowerCase().endsWith(
319                                     "." + de.getDefaultExtension())) {
320                               fileName =
321                                       fileName.substring(0,
322                                               fileName.length()
323                                                       - de.getDefaultExtension()
324                                                               .length()
325                                                       1);
326                             }
327                             if(usedFileNames.contains(fileName)) {
328                               // name clash -> add unique ID
329                               String fileNameBase = fileName;
330                               int uniqId = 0;
331                               fileName = fileNameBase + "-" + uniqId++;
332                               while(usedFileNames.contains(fileName)) {
333                                 fileName = fileNameBase + "-" + uniqId++;
334                               }
335                             }
336                             usedFileNames.add(fileName);
337                             if(!fileName.toLowerCase().endsWith(
338                                     "." + de.getDefaultExtension()))
339                               fileName += "." + de.getDefaultExtension();
340                             File docFile = null;
341                             boolean nameOK = false;
342                             do {
343                               docFile = new File(dir, fileName);
344                               if(docFile.exists() && !overwriteAll) {
345                                 // ask the user if we can overwrite
346                                 // the file
347                                 Object[] opts =
348                                         new Object[] {"Yes""All""No",
349                                             "Cancel"};
350                                 MainFrame.unlockGUI();
351                                 int answer =
352                                         JOptionPane.showOptionDialog(
353                                                 MainFrame.getInstance()"File "
354                                                         + docFile.getName()
355                                                         " already exists!\n"
356                                                         "Overwrite?""GATE",
357                                                 JOptionPane.DEFAULT_OPTION,
358                                                 JOptionPane.WARNING_MESSAGE,
359                                                 null, opts, opts[2]);
360                                 MainFrame.lockGUI("Saving...");
361                                 switch(answer) {
362                                   case 0{
363                                     nameOK = true;
364                                     break;
365                                   }
366                                   case 1{
367                                     nameOK = true;
368                                     overwriteAll = true;
369                                     break;
370                                   }
371                                   case 2{
372                                     // user said NO, allow them to
373                                     // provide
374                                     // an
375                                     // alternative name;
376                                     MainFrame.unlockGUI();
377                                     fileName =
378                                             (String)JOptionPane.showInputDialog(
379                                                     MainFrame.getInstance(),
380                                                     "Please provide an alternative file name",
381                                                     "GATE",
382                                                     JOptionPane.QUESTION_MESSAGE,
383                                                     null, null, fileName);
384                                     if(fileName == null) {
385                                       handle.processFinished();
386                                       return;
387                                     }
388                                     MainFrame.lockGUI("Saving");
389                                     break;
390                                   }
391                                   case 3{
392                                     // user gave up; return
393                                     handle.processFinished();
394                                     return;
395                                   }
396                                 }
397   
398                               else {
399                                 nameOK = true;
400                               }
401                             while(!nameOK);
402                             // save the file
403                             try {
404                               // do the actual exporting
405                               de.export(currentDoc, docFile, options);
406                             catch(Exception ioe) {
407                               MainFrame.unlockGUI();
408                               JOptionPane.showMessageDialog(
409                                       MainFrame.getInstance(),
410                                       "Could not create write file:"
411                                               + ioe.toString()"GATE",
412                                       JOptionPane.ERROR_MESSAGE);
413                               ioe.printStackTrace(Err.getPrintWriter());
414                               return;
415                             }
416   
417                             listener.statusChanged(currentDoc.getName()
418                                     " saved");
419                             // close the doc if it wasn't already
420                             // loaded
421                             if(!docWasLoaded) {
422                               corpus.unloadDocument(currentDoc);
423                               Factory.deleteResource(currentDoc);
424                             }
425   
426                             handle.progressChanged(100 * currentDocIndex++
427                                     / docCnt);
428                           }// while(docIter.hasNext())
429                           listener.statusChanged("Corpus Saved");
430                           handle.processFinished();
431                         finally {
432                           MainFrame.unlockGUI();
433                         }
434                       }
435                     }
436                   }
437                 };
438 
439                 Thread thread =
440                         new Thread(Thread.currentThread().getThreadGroup(),
441                                 runnable, "Document Exporter Thread");
442                 thread.setPriority(Thread.MIN_PRIORITY);
443                 thread.start();
444               }
445 
446             });
447 
448     itemByResource.put(de, item);
449 
450   }
451 
452   /**
453    * If the resource just loaded is a tool (according to the creole
454    * register) then see if it publishes any actions and if so, add them
455    * to the menu in the appropriate places.
456    */
457   @Override
458   public void resourceLoaded(CreoleEvent e) {
459     final Resource res = e.getResource();
460 
461     if(res instanceof DocumentExporter) {
462       Runnable runnable = new Runnable() {
463         @Override
464         public void run() {
465           addExporter((DocumentExporter)res);
466         }
467       };
468       
469       if(SwingUtilities.isEventDispatchThread()) {
470         runnable.run();
471       else {
472         try {
473           SwingUtilities.invokeAndWait(runnable);
474         catch(Exception ex) {
475           log.warn("Exception registering document exporter", ex);
476         }
477       }
478     }
479   }
480 
481   @Override
482   public void resourceUnloaded(CreoleEvent e) {
483     final Resource res = e.getResource();
484 
485     if(res instanceof DocumentExporter) {
486       SwingUtilities.invokeLater(new Runnable() {
487         
488         @Override
489         public void run() {
490           // TODO Auto-generated method stub
491           JMenuItem item = itemByResource.get(res);
492 
493           if(item != null) {
494             remove(item);
495             itemByResource.remove(res);
496           }
497           
498           if(getItemCount() == 2) {
499             remove(1);
500           }
501         }
502       });     
503     }    
504   }
505 
506   // remaining CreoleListener methods not used
507   @Override
508   public void datastoreClosed(CreoleEvent e) {
509   }
510 
511   @Override
512   public void datastoreCreated(CreoleEvent e) {
513   }
514 
515   @Override
516   public void datastoreOpened(CreoleEvent e) {
517   }
518 
519   @Override
520   public void resourceRenamed(Resource resource, String oldName, String newName) {
521   }
522 
523   private static class DocumentExportDialog extends JDialog {
524 
525     private DocumentExporter de;
526 
527     private JButton okBtn, fileBtn, cancelBtn;
528 
529     private JTextField txtFileName;
530 
531     private ResourceParametersEditor parametersEditor;
532 
533     private boolean singleFile, userCanceled;
534     
535     private FeatureMap parameters;
536 
537     public DocumentExportDialog() {
538       super(MainFrame.getInstance()"Save As..."true);
539       MainFrame.getGuiRoots().add(this);
540       initGuiComponents();
541       initListeners();
542     }
543 
544     protected void initGuiComponents() {
545       this.getContentPane().setLayout(
546               new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS));
547 
548       // name field
549       Box nameBox = Box.createHorizontalBox();
550       nameBox.add(Box.createHorizontalStrut(5));
551       nameBox.add(new JLabel("Save To:"));
552       nameBox.add(Box.createHorizontalStrut(5));
553       txtFileName = new JTextField(30);
554       txtFileName.setMaximumSize(new Dimension(Integer.MAX_VALUE, txtFileName
555               .getPreferredSize().height));
556       txtFileName.setRequestFocusEnabled(true);
557       txtFileName.setVerifyInputWhenFocusTarget(false);
558       nameBox.add(txtFileName);
559       // nameField.setToolTipText("Enter a name for the resource");
560 
561       nameBox.add(Box.createHorizontalStrut(5));
562       nameBox.add(fileBtn = new JButton(MainFrame.getIcon("OpenFile")));
563       nameBox.add(Box.createHorizontalGlue());
564       this.getContentPane().add(nameBox);
565       this.getContentPane().add(Box.createVerticalStrut(5));
566 
567       // parameters table
568       parametersEditor = new ResourceParametersEditor();
569       this.getContentPane().add(new JScrollPane(parametersEditor));
570       this.getContentPane().add(Box.createVerticalStrut(5));
571       this.getContentPane().add(Box.createVerticalGlue());
572       // buttons box
573       JPanel buttonsBox = new JPanel();
574       buttonsBox.setLayout(new BoxLayout(buttonsBox, BoxLayout.X_AXIS));
575       buttonsBox.add(Box.createHorizontalStrut(10));
576       buttonsBox.add(okBtn = new JButton("OK"));
577       buttonsBox.add(Box.createHorizontalStrut(10));
578       buttonsBox.add(cancelBtn = new JButton("Cancel"));
579       buttonsBox.add(Box.createHorizontalStrut(10));
580       this.getContentPane().add(buttonsBox);
581       this.getContentPane().add(Box.createVerticalStrut(5));
582       setSize(400300);
583 
584       getRootPane().setDefaultButton(okBtn);
585     }
586 
587     protected void initListeners() {
588       Action fileAction = new AbstractAction() {
589 
590         @Override
591         public void actionPerformed(ActionEvent ae) {
592           XJFileChooser fileChooser = MainFrame.getFileChooser();
593           fileChooser.resetChoosableFileFilters();
594           fileChooser.setFileFilter(de.getFileFilter());
595           fileChooser.setMultiSelectionEnabled(false);
596           fileChooser.setDialogTitle("Save as " + de.getFileType());
597           fileChooser.setFileSelectionMode(singleFile
598                   ? JFileChooser.FILES_ONLY
599                   : JFileChooser.DIRECTORIES_ONLY);
600 
601           try {
602             File f = new File(txtFileName.getText());
603             fileChooser.ensureFileIsVisible(f);
604             fileChooser.setSelectedFile(f);
605           catch(Exception e) {
606             // swallow and ignore
607           }
608 
609           if(fileChooser.showSaveDialog(DocumentExportDialog.this!= JFileChooser.APPROVE_OPTION)
610             return;
611 
612           File selectedFile = fileChooser.getSelectedFile();
613           if(selectedFile == nullreturn;
614 
615           txtFileName.setText(selectedFile.getAbsolutePath());
616         }
617       };
618 
619       Action applyAction = new AbstractAction() {
620         @Override
621         public void actionPerformed(ActionEvent e) {
622           userCanceled = false;
623           TableCellEditor cellEditor = parametersEditor.getCellEditor();
624           if(cellEditor != null) {
625             cellEditor.stopCellEditing();
626           }
627           setVisible(false);
628         }
629       };
630       Action cancelAction = new AbstractAction() {
631         @Override
632         public void actionPerformed(ActionEvent e) {
633           userCanceled = true;
634           setVisible(false);
635         }
636       };
637 
638       fileBtn.addActionListener(fileAction);
639       okBtn.addActionListener(applyAction);
640       cancelBtn.addActionListener(cancelAction);
641 
642       // disable Enter key in the table so this key will confirm the
643       // dialog
644       InputMap im =
645               parametersEditor
646                       .getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
647       KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
648       im.put(enter, "none");
649 
650       // define keystrokes action bindings at the level of the main
651       // window
652       InputMap inputMap =
653               ((JComponent)this.getContentPane())
654                       .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
655       ActionMap actionMap = ((JComponent)this.getContentPane()).getActionMap();
656       inputMap.put(KeyStroke.getKeyStroke("ENTER")"Apply");
657       actionMap.put("Apply", applyAction);
658       inputMap.put(KeyStroke.getKeyStroke("ESCAPE")"Cancel");
659       actionMap.put("Cancel", cancelAction);
660     }
661 
662     public synchronized boolean show(DocumentExporter de,
663             List<List<Parameter>> params, boolean singleFile, String filePath) {
664 
665       
666       
667       this.singleFile = singleFile;
668       this.de = de;
669       this.parameters = null;
670 
671       setTitle("Save as " + de.getFileType());
672 
673       txtFileName.setText(filePath);
674       parametersEditor.init(null, params);
675       pack();
676       txtFileName.requestFocusInWindow();
677       userCanceled = true;
678       setModal(true);
679       setLocationRelativeTo(getOwner());
680       super.setVisible(true);
681       dispose();
682       if(userCanceled)
683         return false;
684       
685       //update the feature map to convert values to objects of the correct type.
686       
687       parameters = parametersEditor.getParameterValues();
688       
689       for (List<Parameter> disjunction : params) {
690         for (Parameter param : disjunction) {
691           if (!param.getTypeName().equals("java.lang.String"&& parameters.containsKey(param.getName())) {
692             Object value = parameters.get(param.getName());
693             if (value instanceof String) {
694               try {
695                 parameters.put(param.getName(), param.calculateValueFromString((String)value));
696               }
697               catch (ParameterException pe) {
698                 pe.printStackTrace();
699                 parameters = null;
700                 return false;
701               }
702             }
703           }
704         }
705       }
706       
707       return true;
708     }
709 
710     @Override
711     public void dispose() {
712       de = null;
713     }
714 
715     /**
716      * Returns the selected params for the resource or null if none was
717      * selected or the user pressed cancel
718      */
719     public FeatureMap getSelectedParameters() {
720       return parameters;
721     }
722 
723     /**
724      * Return the String entered into the resource name field of the
725      * dialog.
726      
727      @param rData
728      */
729     public String getSelectedFileName() {
730       return txtFileName.getText();
731     }
732   }
733 }