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}