001/*
002 * Copyright (c) 2015 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2017-08-24 05:20:42 +0000 (Thu, 24 Aug 2017) $' 
007 * '$Revision: 34621 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029package org.kepler.loader.util;
030
031import java.io.File;
032import java.io.FileOutputStream;
033import java.io.OutputStream;
034import java.util.HashSet;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.Set;
038
039import javax.swing.SwingUtilities;
040
041import org.apache.commons.io.FilenameUtils;
042import org.kepler.gui.KeplerIconLoader;
043import org.kepler.objectmanager.library.LibraryManager;
044
045import ptolemy.actor.CompositeActor;
046import ptolemy.actor.gui.Configuration;
047import ptolemy.actor.gui.ConfigurationApplication;
048import ptolemy.actor.gui.Tableau;
049import ptolemy.kernel.ComponentEntity;
050import ptolemy.kernel.util.IllegalActionException;
051import ptolemy.moml.MoMLParser;
052import ptolemy.vergil.basic.BasicGraphFrame;
053
054/** A class to automate taking screenshots of Kepler workflows.
055 * 
056 *  @author Daniel Crawl
057 *  @version $Id: Screenshot.java 34621 2017-08-24 05:20:42Z crawl $
058 */
059public class Screenshot {
060
061    /** Create a new Screenshot for a workflow with the specified output. */
062    private Screenshot(File workflow, File output) {
063        _workflowFile = workflow;
064        _outputFile = output;
065    }
066
067    /** Create screenshots of workflows. There are three possible ways to specify
068     *  the name and location of the output images:
069     *  <ul>
070     *  <li>If outputDir is not null, then the images are all written to this directory
071     *  with the same name as the workflow.</li>
072     *  <li>If outputs is not null, then the outputs are written to the files specified
073     *  in outputs. There must be a file name in outputs for each workflow.</li>
074     *  <li>If both outputDir and outputs is null, the outputs are written to the
075     *  same directory as the workflow, with the same name as the workflow.</li>
076     *  </ul>
077     *  @param workflows the set of workflow filenames of which to create screenshots
078     *  @param type the screenshot image type. If a set of output filenames are
079     *  specified, this parameter must be null since the output filename extensions
080     *  specify the image type.
081     *  @param outputs A set of output filenames. If this is null, then the output
082     *  directory and type must be specified. If this is not null, it must contain an
083     *  output image name for each workflow.
084     *  @param outputDir The output directory in which to write the screenshot images.
085     *  If this is null, then the outputs must specify the output file names. Otherwise,
086     *  outputs must be null.
087     *  @param force If true, always create screenshot images. Otherwise, the screenshot
088     *  images are only created if the output image does not exist or was last modified
089     *  before the workflow file was last modified.
090     *  @return A list of files containing the screenshots.
091     */
092    public synchronized static List<File> makeScreenshot(List<String> workflows, String type,
093            List<String> outputs, String outputDir, boolean force) throws Exception {
094        
095        // make sure each workflow file exists
096        for(String workflow : workflows) {
097            File workflowFile = new File(workflow);
098            if(!workflowFile.exists()) {
099                throw new IllegalActionException("Workflow " + workflow + " does not exists.");
100            }
101        }
102
103        // if output specified, make sure is the same number as workflows
104        if(outputs != null && workflows.size() != outputs.size()) {
105            throw new IllegalActionException("The number of outputs does not match the number of workflows.");
106        }
107        
108        // error if output and directory both specified
109        if(outputs != null && outputDir != null) {
110            throw new IllegalActionException("Either output name or output directory can be specified but not both.");
111        }
112
113        // create directory if specified and does not exist
114        if(outputDir != null) {
115            File outputDirFile = new File(outputDir);
116            if(!outputDirFile.isDirectory()) {
117                if(!outputDirFile.mkdirs()) {
118                    throw new IllegalActionException("Could not create output directory " + outputDir);
119                }
120            }
121        }
122        
123        // error if type and output is specified
124        if(type != null && outputs != null) {
125            throw new IllegalActionException("Either output type or output name can be specified, but not both.\n"
126                    + "The extension in the output name is used as the type.");
127        }
128        
129        if(type == null && outputDir != null) {
130            throw new IllegalActionException("Must specified type if output directory is specified.");
131        }
132        
133        if(type == null && outputs == null) {
134            throw new IllegalActionException("Must specify either output type or output name.");
135        }
136                
137        // set this property to prevent Configuration._removeEntity()
138        // from calling System.exit() when the workflow is closed.
139        // this property is also set to true in 
140        // ConfigurationApplication.closeModelWithoutSavingOrExiting(),
141        // but does not appear to work for kepler.
142        System.setProperty("ptolemy.ptII.doNotExit", "true");
143
144        // Set the icon loader to load Kepler files
145        MoMLParser.setIconLoader(new KeplerIconLoader());
146
147        Set<Screenshot> screenshots = new HashSet<Screenshot>();
148        
149        String[] workflowArray = workflows.toArray(new String[workflows.size()]);
150        String[] outputArray = null;
151        if(outputs != null) {
152            outputArray = outputs.toArray(new String[outputs.size()]);
153        }
154        
155        String typeExtension = null;
156        if(type != null) {
157            typeExtension = type.trim().toLowerCase();
158        }
159        
160        
161        List<File> outputFiles = new LinkedList<File>();
162        
163        for(int i = 0; i < workflowArray.length; i++) {
164        
165            String workflowStr = workflowArray[i];
166            File workflowFile = new File(workflowStr);
167            File output;
168            
169            // set output
170            if(outputArray != null) {
171                output = new File(outputArray[i]);
172            } else if(outputDir != null) {
173                output = new File(outputDir, 
174                        FilenameUtils.getBaseName(workflowStr) + "." + typeExtension);
175            } else {
176                output = new File(workflowFile.getParentFile(), 
177                        FilenameUtils.getBaseName(workflowStr) + "." + typeExtension);
178            }
179            
180            outputFiles.add(output);
181            
182            // if not force, check if output exists
183            if(!force && output.exists() && workflowFile.lastModified() <= output.lastModified()) {
184                System.out.println("Skipping screen shot for " + workflowArray[i] + " since image " +
185                        output + " is newer");
186                continue;
187            }
188            
189            Screenshot screenshot = new Screenshot(workflowFile, output);
190            screenshots.add(screenshot);
191        }
192        
193        if(!screenshots.isEmpty()) {            
194            
195            // open the initial frame
196            if(_initialFrame == null) {
197                _initialFrame = _openKeplerGraphFrame(null, false);
198            }   
199
200            // make each screenshot
201            for(Screenshot screenshot : screenshots) {
202                screenshot._takeScreenshot();
203            }
204            
205            // close the initial frame
206            if(_initialFrame != null && _closeAllWhenDone) {
207                closeAll();
208            }
209
210        }
211        
212        return outputFiles;
213    }
214    
215    /** Set if should close all tableaux and shutdown after last screenshot. */
216    public synchronized static void closeAllWhenDone(boolean close) {
217        _closeAllWhenDone = close;
218    }
219    
220    /** Close all tableaux, which will shutdown Kepler. */
221    public synchronized static void closeAll() throws IllegalActionException {
222        if(_initialFrame != null) {
223            _initialFrame.dispose();
224            _initialFrame = null;
225        }
226        Configuration.closeAllTableaux();
227    }
228    
229    /** Take the screen shot of an actor or workflow. */
230    private boolean _takeScreenshot() {
231        
232        final boolean success[] = new boolean[1];
233        success[0] = true;
234        
235        if(_entity == null) {
236            try {
237                _entity = (ComponentEntity<?>) ParseWorkflow.parseWorkflow(_workflowFile);
238            } catch (Exception e) {
239                System.out.println("Error parsing " + _workflowFile + ":" + e.getMessage());
240                e.printStackTrace();
241                return false;                
242            }
243            if(_entity == null) {
244                System.out.println("Could not parse " + _workflowFile);
245                return false;
246            }
247        }
248        
249        BasicGraphFrame frametmp;
250        try {
251            frametmp = _openKeplerGraphFrame(_entity, !_isWorkflow);
252        } catch (Exception e) {
253            System.out.println("Could not open frame for component " +
254                    _entity.getDisplayName() + ": " + e.getMessage());
255            return false;
256        }
257        
258        final BasicGraphFrame frame = frametmp;
259        
260        Runnable runnable = new Runnable() {
261            @Override
262            public void run() {
263                try(OutputStream outputStream = new FileOutputStream(_outputFile);) {                   
264                    frame.zoomFit();
265                    frame.getJGraph().exportImage(outputStream,
266                            FilenameUtils.getExtension(_outputFile.getAbsolutePath()));
267                    frame.close();
268                } catch(Exception e) {
269                    System.out.println("Error writing image: " + e.getMessage());
270                    success[0] = false;
271                }
272            }
273        };
274        
275        try {
276            SwingUtilities.invokeAndWait(runnable);
277        } catch (Exception e) {
278            e.printStackTrace();
279            success[0] = false;
280        }
281        
282        return success[0];
283    }
284    
285    /** Display an entity in a BasicGraphFrame. 
286     *  @param entity the entity to display in the BasicGraphFrame.  
287     *  @param placeInEmptyComposite if true, put the entity in an empty composite
288     *  actor and display the contents of this composite actor in the BasicGraphFrame.
289     */
290    private static BasicGraphFrame _openKeplerGraphFrame(final ComponentEntity<?> entity,
291            final boolean placeInEmptyComposite) throws Exception {
292        
293        // create an empty library since creating a KeplerGraphFrame initializes 
294        // the ComponentLibraryTab, which in turns requires a library.
295        LibraryManager.getInstance().buildEmptyLibrary();
296
297        final BasicGraphFrame[] frame = new BasicGraphFrame[1];
298        
299        Runnable runnable = new Runnable() {
300            @Override
301            public void run() {
302                
303                CompositeActor container = null;
304                if(entity == null) {
305                    container = new CompositeActor();
306                } else if(placeInEmptyComposite) {
307                    container = new CompositeActor(entity.workspace());
308                } else {
309                    container = (CompositeActor) entity;
310                } 
311
312                Configuration configuration;
313                
314                try {
315                    
316                    if(entity != null && placeInEmptyComposite) {
317                        entity.setContainer(container);
318                    }
319
320                    configuration = ConfigurationApplication
321                            .readConfiguration(ConfigurationApplication
322                                    .specToURL("ptolemy/configs/kepler/configuration.xml"));
323                    Tableau tableau = configuration.openModel(container);
324                    frame[0] = (BasicGraphFrame)tableau.getFrame();
325                } catch (Exception e) {
326                    e.printStackTrace();
327                }
328            }
329        };
330
331        if(SwingUtilities.isEventDispatchThread()) {
332            runnable.run();
333        } else {
334            SwingUtilities.invokeAndWait(runnable);
335        }
336        
337        try {
338            Thread.sleep(1000);
339        } catch (Throwable ex) {
340            // Ignore
341        }
342        
343        return frame[0];
344    }
345      
346    /** The workflow file. */
347    private File _workflowFile;
348    
349    /** The screenshot image file. */
350    private File _outputFile;
351    
352    /** If true, the entity is a workflow. */
353    private boolean _isWorkflow = true;
354    
355    /** The entity to take a screenshot of. */
356    private ComponentEntity<?> _entity;
357    
358    /** The frame used to initialize the entity library. */
359    private static BasicGraphFrame _initialFrame;
360    
361    /** If true, close all tableaux and shutdown after last screenshot. */
362    private static boolean _closeAllWhenDone = false;
363}