001/* Command line program and static methods to manage provenance.
002 * 
003 * Copyright (c) 2015 The Regents of the University of California.
004 * All rights reserved.
005 *
006 * '$Author: crawl $'
007 * '$Date: 2018-02-01 20:19:23 +0000 (Thu, 01 Feb 2018) $' 
008 * '$Revision: 34655 $'
009 * 
010 * Permission is hereby granted, without written agreement and without
011 * license or royalty fees, to use, copy, modify, and distribute this
012 * software and its documentation for any purpose, provided that the above
013 * copyright notice and the following two paragraphs appear in all copies
014 * of this software.
015 *
016 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
017 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
018 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
019 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
020 * SUCH DAMAGE.
021 *
022 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
023 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
024 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
025 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
026 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
027 * ENHANCEMENTS, OR MODIFICATIONS.
028 *
029 */
030package org.kepler.provenance.manager;
031
032import java.io.File;
033import java.nio.file.Files;
034import java.util.Arrays;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.Map;
038
039import org.apache.taverna.robundle.Bundle;
040import org.apache.taverna.robundle.Bundles;
041import org.kepler.Kepler;
042import org.kepler.kar.KAREntry;
043import org.kepler.kar.KARFile;
044import org.kepler.kar.SaveKAR;
045import org.kepler.kar.handlers.ProvKAREntryHandler;
046import org.kepler.loader.util.ParseWorkflow;
047import org.kepler.objectmanager.cache.CacheManager;
048import org.kepler.objectmanager.lsid.KeplerLSID;
049import org.kepler.provenance.ProvenanceRecorder;
050import org.kepler.provenance.QueryException;
051import org.kepler.provenance.Queryable;
052import org.kepler.provenance.Recording;
053import org.kepler.provenance.RecordingException;
054import org.kepler.provenance.prov.ProvUtilities;
055import org.kepler.provenance.sql.SQLRecordingV8;
056import org.kepler.util.WorkflowRun;
057import org.openprovenance.prov.interop.InteropFramework.ProvFormat;
058
059import ptolemy.kernel.ComponentEntity;
060import ptolemy.kernel.util.IllegalActionException;
061import ptolemy.kernel.util.NameDuplicationException;
062import ptolemy.kernel.util.NamedObj;
063import ptolemy.moml.MoMLParser;
064import ptolemy.moml.filter.BackwardCompatibility;
065import ptolemy.util.MessageHandler;
066
067/** A utility class containing static methods and a command line
068 *  interface to manange provenance.
069 * 
070 *  @author Daniel Crawl
071 *  @version $Id: ProvenanceManager.java 34655 2018-02-01 20:19:23Z crawl $
072 */
073public class ProvenanceManager {
074
075    /** Delete a run. */
076    public static void deleteRun(Integer execId) throws Exception {
077        try(Queryable query = _getDefaultQueryable();) {
078            KeplerLSID lsid = query.getExecutionLSIDForExecution(execId);
079            if(lsid == null) {
080                throw new Exception("Execution id " + execId + " not found.");
081            }
082            List<KeplerLSID> lsids = new LinkedList<KeplerLSID>();
083            lsids.add(lsid);
084            deleteRuns(lsids);
085        }
086    }
087    
088    /** Delete runs. */
089    public static void deleteRuns(List<KeplerLSID> execLSIDs) throws Exception {
090        
091        ProvenanceRecorder recorder;
092        Recording recording;
093        try {
094            recorder = new ProvenanceRecorder();
095            recording = recorder.getRecording();
096        } catch (IllegalActionException | NameDuplicationException e) {
097            throw new Exception("Error getting default recording interface.", e);
098        }       
099        
100        // TODO fix for different recording types.
101        if(recording instanceof SQLRecordingV8) {
102            System.out.println("Removing runs: ");
103            for(KeplerLSID lsid: execLSIDs) {
104                    System.out.println(lsid);
105            }
106            ((SQLRecordingV8)recording).deleteExecutions(execLSIDs);
107        } else {
108            System.err.println("Cannot delete from recording type " + recording.getClass());
109        }
110        
111        recorder.disconnect();
112    }
113    
114    /** Export all runs to a KAR file.
115     *  @param outputFileName the name of the KAR file.
116     *  @param delete if true, delete the runs after being exported.
117     */
118    public static void exportProvenance(String outputFileName, boolean delete)
119            throws Exception {        
120        try(Queryable query = _getDefaultQueryable();) {
121            exportProvenance(outputFileName, delete,
122                    query.getExecutionLSIDs());
123        }        
124    }
125
126    /** Export a set of provenance runs to a KAR file. 
127     *  @param outputFileName the name of the KAR file.
128     *  @param delete if true, delete each run exported.
129     *  @param execId the execution id of the run to export.
130     */
131    public static void exportProvenance(String outputFileName, boolean delete, int execId)
132            throws Exception {
133        try(Queryable query = _getDefaultQueryable();) {
134            KeplerLSID lsid = query.getExecutionLSIDForExecution(execId);
135            if(lsid == null) {
136                throw new Exception("Could not find LSID for run " + execId);
137            }
138            List<KeplerLSID> lsids = new LinkedList<KeplerLSID>();
139            lsids.add(lsid);
140            exportProvenance(outputFileName, delete, lsids);
141        }        
142    }
143    
144    /** Export a set of provenance runs to a KAR file. 
145     *  @param outputFileName the name of the KAR file.
146     *  @param delete if true, delete each run exported.
147     *  @param runs the set of KeplerLSID runs to export. 
148     */
149    public static void exportProvenance(String outputFileName, boolean delete,
150            List<KeplerLSID> runs) throws Exception {
151        exportProvenance(outputFileName, delete, runs, "json");
152    }
153    
154    /** Export a set of provenance runs to a KAR file. 
155     *  @param outputFileName the name of the KAR file.
156     *  @param delete if true, delete each run exported.
157     *  @param runs the set of KeplerLSID runs to export. 
158     *  @params format the prov format.
159     */
160    public static void exportProvenance(String outputFileName, boolean delete,
161            List<KeplerLSID> runs, String format) throws Exception {
162
163        TransferComponent component = new TransferComponent(format);
164        component.setName("ProvenanceTransfer");
165        component.addExecutions(runs);
166
167        SaveKAR saveKar = new SaveKAR();
168        saveKar.setFile(new File(outputFileName));
169        saveKar.addSaveInitiator(component);
170        
171        KeplerLSID karLSID = saveKar.saveToDisk(null, null);
172        // check for success
173        if(karLSID == null) {
174            throw new Exception("Error writing KAR.");
175        }
176        
177        if(delete) {
178            deleteRuns(runs);
179        }
180    }  
181
182    /** Export a research object bundle
183     * 
184     */
185    public static void exportROBundle(String outputFileName, KeplerLSID runLSID) throws Exception {
186        
187        File roBundleFile = new File(outputFileName);
188        File workflowFile = File.createTempFile("workflow", ".kar");
189        File provenanceFile = File.createTempFile("provenance", ".kar");
190        try {
191            Bundle bundle = Bundles.createBundle(roBundleFile.toPath());
192     
193            // add the workflow
194            exportWorkflow(workflowFile.getAbsolutePath(), runLSID);            
195            Files.copy(workflowFile.toPath(), bundle.getRoot().resolve("workflow.kar"));
196            
197            // add the prov trace
198            exportProvenance(provenanceFile.getAbsolutePath(), false,
199                Arrays.asList(runLSID), "json");            
200            Files.copy(provenanceFile.toPath(), bundle.getRoot().resolve("provenance.kar"));
201
202            Bundles.setMimeType(bundle, "application/zip");
203
204            bundle.close();
205            
206            
207        } catch(Exception e) {
208            if(roBundleFile.exists() && !roBundleFile.delete()) {
209                System.err.println("WARNING: could not delete " + roBundleFile.getAbsolutePath());
210            }
211            throw e;
212        } finally {
213            if(!workflowFile.delete()) {
214                System.err.println("WARNING: could not delete " + workflowFile.getAbsolutePath());
215            }
216            if(!provenanceFile.delete()) {
217                System.err.println("WARNING: could not delete " + provenanceFile.getAbsolutePath());   
218            }
219        }
220        
221    }
222
223    /** Export a workflow to a file.
224     * @param outputFileName The name of the workflow file.
225     * @param runLSID The run LSID of the workflow to export.
226     * @return The workflow file
227     */
228    public static void exportWorkflow(String outputFileName, KeplerLSID runLSID) throws Exception {
229
230        try(Queryable queryable = _getDefaultQueryable()) {
231            
232            String momlStr = queryable.getMoMLForExecution(runLSID);        
233            
234            SaveKAR saveKAR = new SaveKAR();
235            NamedObj model = ParseWorkflow.parseMoML(momlStr);        
236            saveKAR.addSaveInitiator((ComponentEntity<?>)model);        
237            File karFile = new File(outputFileName);
238            saveKAR.setFile(karFile);
239            
240            KeplerLSID karLSID = saveKAR.saveToDisk(null, null);
241            // check if saved successfully
242            if(karLSID == null) {
243                if(karFile.exists() && !karFile.delete()) {
244                    System.err.println("WARNING: could not delete " +
245                        karFile.getAbsolutePath());
246                }
247                throw new Exception("Could not create KAR.");
248            }            
249        }
250    }
251    
252    /** Import provenance from a KAR file.
253     *  @param inputFileName the name of the KAR file to import from.
254     *  @param force if true, try to import despite missing any module
255     *  dependencies. if false, and there are missing dependencies, throws
256     *  an exception.
257     */
258    public static void importProvenance(String inputFileName, boolean force)
259            throws Exception {
260        
261        File inputFile = new File(inputFileName);
262        if(!inputFile.exists()) {
263            throw new Exception("Import file does not exist: " + inputFileName);
264        }
265        
266        try(KARFile karFile = new KARFile(inputFile);) {
267        
268            if(!force && !karFile.areAllModuleDependenciesSatisfied()) {
269                // TODO
270                throw new Exception("Module dependencies not satisfied and -force not specified.");
271            }
272                    
273            //karFile.cacheKARContents();
274            
275            ProvKAREntryHandler handler = new ProvKAREntryHandler();
276            for (KAREntry entry : karFile.karEntries()) {
277                if (handler.handlesType(entry.getType())) {
278                    System.out.println("Importing " + entry.getName());
279                    handler.open(karFile, entry, null);
280                }
281            }
282        }
283    }
284    
285    /** List the runs. */
286    public static void listRuns() throws Exception {
287
288        // set the ontology index file so we can show tags.
289        Kepler.setOntologyIndexFile();
290        
291        try(Queryable query = _getDefaultQueryable();) {
292            Map<KeplerLSID, WorkflowRun> runs =
293                query.getWorkflowRunsForExecutionLSIDs(query.getExecutionLSIDs());
294            
295            // TODO add:
296            // duration, start/stop time
297            // status: success, error (and error message)            
298            System.out.println("ID, Tag(s), Workflow Name, Execution LSID");        
299            for(WorkflowRun run : runs.values()) {
300                
301                String tags = run.getTagsAsFormattedString();
302                if(tags.trim().isEmpty()) {
303                    tags = "<no tags>";
304                }
305                
306                System.out.println(run.getExecId() + ", " +
307                        tags + ", " +
308                        run.getWorkflowName() + ", " +
309                        run.getExecLSID());
310            }
311        }
312    }
313    
314    /** Program entry point. */
315    public static void main(String[] args) {
316
317        try {
318            if(_parseArgs(args)) {
319
320                if(_operation == Operation.Export ||
321                    _operation == Operation.Import ||
322                    _operation == Operation.Graph) {
323                    System.setProperty("java.awt.headless", "true");
324                    MoMLParser.setMoMLFilters(BackwardCompatibility.allFilters());
325                    MessageHandler.setMessageHandler(new MessageHandler());
326
327                    // set java properties since they may be referenced in workflows
328                    // we will import/export. e.g., module.workflowdir
329                    Kepler.setJavaPropertiesAndCopyModuleDirectories();
330                }
331                
332                switch(_operation) {
333                case List:
334                    listRuns();
335                    break;
336                    
337                case Delete:
338                    deleteRun(_runId);
339                    break;
340                    
341                case Export:
342                case Import:
343                    
344                    // import/export
345                    CacheManager.getInstance().addKAREntryHandler(new ProvKAREntryHandler());
346                
347                    if(_operation == Operation.Import) {
348                        importProvenance(_inputFileName, _forceImport);
349                    } else if(_exportAll) {
350                        exportProvenance(_outputFileName, _deleteAfterExport);
351                    } else {
352                        exportProvenance(_outputFileName, _deleteAfterExport, _runId);
353                    }
354                    break;
355                    
356                case Graph:
357                    ProvUtilities.writeProv(_outputFileName, _type, _runId);
358                    break;
359                }
360            }
361        } catch(Throwable t) {
362            MessageHandler.error("Error managing provenance.", t);
363            System.exit(1);
364        }
365        
366        // TODO shut down the database cleanly so the program will exit
367        // for now call System.exit()
368        System.exit(0);
369    }
370
371    ///////////////////////////////////////////////////////////////////
372    ////                         private methods                   ////
373
374    /** Get the default queryable. */
375    private static Queryable _getDefaultQueryable() throws Exception {
376        
377        try {
378            return ProvenanceRecorder.getDefaultQueryable(null);
379        } catch (QueryException | RecordingException e) {
380            throw new Exception("Error getting default query interface.", e);
381        }
382
383    }
384    
385    /** Parse command line arguments.
386     *  @return True if arguments parsed ok, false if an error
387     *  and execution should stop. 
388     */
389    private static boolean _parseArgs(String[] args) {
390        
391        boolean retval = true;
392        
393        String runIdStr = null;
394        
395        int i = 0;
396        for(i = 0; i < args.length; i++) {
397            String arg = args[i];
398            switch(arg) {
399            case "-all":
400                _exportAll = true;
401                break;
402            case "-delete":
403                _deleteAfterExport = true;
404                break;
405            case "-force":
406                _forceImport = true;
407                break;                
408            case "-h":
409            case "-help":
410            case "--help":
411                _usage();
412                return false;
413            case "-i":
414                if(i + 1 == args.length) {
415                    System.err.println("ERROR: must provide input file name with -i.");
416                    return false;
417                }
418                i++;
419                _inputFileName = args[i];
420                _operation = Operation.Import;
421                break;
422            case "-g":
423                if(i + 1 == args.length) {
424                    System.err.println("ERROR: must provide output file name with -g.");
425                    return false;
426                }
427                i++;
428                _outputFileName = args[i];
429                _operation = Operation.Graph;
430                if(_type == null) {
431                    _type = "jpeg";
432                }
433                break;               
434            case "-l":
435                _operation = Operation.List;
436                break;
437            case "-o":
438                if(i + 1 == args.length) {
439                    System.err.println("ERROR: must provide output file name with -o.");
440                    return false;
441                }
442                i++;
443                _outputFileName = args[i];
444                _operation = Operation.Export;
445                break;
446            case "-rm":
447                if(i + 1 == args.length) {
448                    System.err.println("ERROR: must provide number to delete with -rm.");
449                    return false;
450                }
451                i++;
452                runIdStr = args[i];
453                _operation = Operation.Delete;
454                break;
455            case "-run":
456                if(i + 1 == args.length) {
457                    System.err.println("ERROR: must provide number with -run.");
458                    return false;
459                }
460                i++;
461                runIdStr = args[i];
462                break;
463            case "-t":
464                if(i + 1 == args.length) {
465                    System.err.println("ERROR: must provide type with -t.");
466                    return false;
467                }
468                i++;
469                _type = args[i];
470                break;
471            default:
472                System.err.println("Unknown argument: " + arg);
473                retval = false;
474                break;
475            }
476        }
477                
478        // sanity checks
479        
480        if(_operation == null) {
481            System.err.println("Must specify an operation.");
482            retval = false;
483        } else if(_exportAll && runIdStr != null) {
484            System.err.println("Cannot export all (-all) and specific run (-run n).");
485            retval = false;
486        } else if(_operation == Operation.Export && !_exportAll && runIdStr == null) {
487            System.err.println("Must specify either -all or -run n when exporting.");
488            retval = false;
489        } else if(_deleteAfterExport && _operation != Operation.Export) {
490            System.err.println("Can only use -delete when exporting.");
491            retval = false;
492        } else if(_forceImport && _operation != Operation.Import) {
493            System.err.println("Can only force when importing.");
494            retval = false;
495        } else if(_operation == Operation.Graph && runIdStr == null) {
496            System.err.println("Must specify a specific run when graphing.");
497            retval = false;
498        } 
499        
500        if(_operation == Operation.Graph) {
501            boolean found = false;
502            for(ProvFormat type : ProvFormat.values()) {
503                if(type.toString().equals(_type.toUpperCase())) {
504                    found = true;
505                    break;
506                }
507            }
508            if(!found) {
509                System.err.println("Valid graph output types are: ");
510                for(ProvFormat type : ProvFormat.values()) {
511                    System.err.print(type.toString().toLowerCase() + " " );
512                }
513                System.err.println();
514                retval = false;
515            }
516        }
517                
518        
519        if(runIdStr != null) {
520            try {
521                _runId = Integer.valueOf(runIdStr);
522            } catch(NumberFormatException e) {
523                System.err.println("ERROR: run " + runIdStr + " is not a number.");
524                retval = false;
525            }
526            if(_runId < 0) {
527                System.err.println("ERROR: run " + _runId + " must be at least 0.");
528                retval = false;
529            }
530        }
531        
532        return retval;
533    }
534    
535    /** Show command line usage. */
536    private static void _usage() {
537        
538        System.out.println("USAGE: prov-manager [-l | -o ... | -i ... | -rm n | -g ...] [...]");
539        System.out.println();
540        System.out.println("-h                          show this help.");
541        System.out.println("-i ...                      import from a kar.");
542        System.out.println("-l                          show provenance runs.");
543        System.out.println("-o ...                      export to a kar.");
544        System.out.println("-rm n                       delete workflow run n.");
545        System.out.println();
546        System.out.println("Export provenance to a KAR file:");
547        System.out.println("prov-manager -o output.kar [-all | -run n] [-delete]");
548        System.out.println("-all                        export all workflows runs.");
549        System.out.println("-run n                      export run n.");
550        System.out.println("-delete                     delete workflow runs in database after export.");
551        System.out.println();
552        System.out.println("Import provenance from a KAR file:");
553        System.out.println("prov-manager -i input.kar [-force]");
554        System.out.println("-force                      attempt to import run(s) despite any missing module dependencies.");
555        System.out.println();
556        System.out.println("Generate a PROV provenance graph:");
557        System.out.println("prov-manager -g output -run n [-t jpg|pdf|...]");
558        System.out.println("-run n                      graph run n.");
559        System.out.println("-t                          graph file type.");
560    }
561    
562    ///////////////////////////////////////////////////////////////////
563    ////                         private fields                    ////
564
565    /** If true, delete the runs after exporting. */
566    private static boolean _deleteAfterExport = false;
567
568    /** If true, export all the provenance runs. */
569    private static boolean _exportAll = false;
570
571    /** If true, try to import the runs despite any missing module
572     *  dependencies.
573     */
574    private static boolean _forceImport = false;
575        
576    /** The input file name. */
577    private static String _inputFileName;
578
579    /** The output file name. */
580    private static String _outputFileName;
581       
582    /** The id of the run to delete, export, graph, etc. */
583    private static Integer _runId;
584        
585    /** The types of operations. */
586    private enum Operation { List, Import, Export, Graph, Delete };
587    
588    /** The operation to perform. */
589    private static Operation _operation;
590    
591    /** The graph type. */
592    private static String _type;
593}