View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.workflow.api;
23  
24  import static org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState.INSTANTIATED;
25  import static org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState.PAUSED;
26  import static org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState.RETRY;
27  import static org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState.RUNNING;
28  
29  import org.opencastproject.mediapackage.MediaPackage;
30  import org.opencastproject.mediapackage.MediaPackageException;
31  import org.opencastproject.mediapackage.MediaPackageParser;
32  import org.opencastproject.security.api.Organization;
33  import org.opencastproject.security.api.User;
34  
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import java.util.ArrayList;
39  import java.util.Collections;
40  import java.util.Date;
41  import java.util.HashMap;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Set;
45  import java.util.TreeMap;
46  
47  import javax.persistence.Access;
48  import javax.persistence.AccessType;
49  import javax.persistence.CascadeType;
50  import javax.persistence.CollectionTable;
51  import javax.persistence.Column;
52  import javax.persistence.ElementCollection;
53  import javax.persistence.Entity;
54  import javax.persistence.FetchType;
55  import javax.persistence.Id;
56  import javax.persistence.Index;
57  import javax.persistence.JoinColumn;
58  import javax.persistence.Lob;
59  import javax.persistence.MapKeyColumn;
60  import javax.persistence.NamedQueries;
61  import javax.persistence.NamedQuery;
62  import javax.persistence.OneToMany;
63  import javax.persistence.OrderColumn;
64  import javax.persistence.Table;
65  import javax.persistence.Temporal;
66  import javax.persistence.TemporalType;
67  import javax.persistence.Transient;
68  import javax.xml.bind.annotation.adapters.XmlAdapter;
69  
70  /**
71   * Entity object for storing workflows in persistence storage. Workflow ID is stored as primary key, DUBLIN_CORE field
72   * is used to store serialized Dublin core and ACCESS_CONTROL field is used to store information about access control
73   * rules.
74   *
75   */
76  @Entity(name = "WorkflowInstance")
77  @Access(AccessType.FIELD)
78  @Table(name = "oc_workflow", indexes = {
79          @Index(name = "IX_oc_workflow_mediapackage_id", columnList = ("mediapackage_id")),
80          @Index(name = "IX_oc_workflow_series_id", columnList = ("series_id")), })
81  @NamedQueries({
82          @NamedQuery(
83                  name = "Workflow.findAll",
84                  query = "select w from WorkflowInstance w "
85                      + "where w.organizationId=:organizationId order by w.dateCreated"
86          ),
87          @NamedQuery(
88                  name = "Workflow.countLatest",
89                  query = "SELECT COUNT(DISTINCT w.mediaPackageId) FROM WorkflowInstance w"
90          ),
91          @NamedQuery(
92                  name = "Workflow.findAllOrganizationIndependent",
93                  query = "select w from WorkflowInstance w"
94          ),
95          @NamedQuery(
96                  name = "Workflow.workflowById",
97                  query = "SELECT w FROM WorkflowInstance as w "
98                      + "where w.workflowId=:workflowId and w.organizationId=:organizationId"
99          ),
100         @NamedQuery(
101             name = "Workflow.workflowByIdOrganizationIndependent",
102             query = "SELECT w FROM WorkflowInstance as w where w.workflowId=:workflowId"
103         ),
104         @NamedQuery(
105                 name = "Workflow.getCount",
106                 query = "select COUNT(w) from WorkflowInstance w where w.organizationId=:organizationId "
107                         + "and (:state is null or w.state = :state) "
108         ),
109         @NamedQuery(
110                 name = "Workflow.toCleanup",
111                 query = "SELECT w FROM WorkflowInstance w where w.state = :state "
112                 + "and w.dateCreated < :dateCreated and w.organizationId = :organizationId"
113         ),
114 
115         // For media packages
116         @NamedQuery(name = "Workflow.byMediaPackage",
117             query = "SELECT w FROM WorkflowInstance w where "
118                 + "w.mediaPackageId = :mediaPackageId and w.organizationId = :organizationId order by w.dateCreated"),
119         @NamedQuery(name = "Workflow.countActiveByMediaPackage",
120             query = "SELECT COUNT(w) FROM WorkflowInstance w where "
121                 + "w.mediaPackageId = :mediaPackageId and w.organizationId = :organizationId and "
122                 + "(w.state = :stateInstantiated or w.state = :statePaused or w.state = :stateRunning "
123                 + "or w.state = :stateFailing)"),
124         @NamedQuery(name = "Workflow.byMediaPackageAndActive",
125             query = "SELECT w FROM WorkflowInstance w where "
126                 + "w.mediaPackageId = :mediaPackageId and w.organizationId = :organizationId and "
127                 + "(w.state = :stateInstantiated or w.state = :statePaused or w.state = :stateRunning "
128                 + "or w.state = :stateFailing) order by w.dateCreated"),
129 
130         // For users
131         @NamedQuery(name = "Workflow.countActiveByUser",
132             query = "SELECT COUNT(w) FROM WorkflowInstance w where "
133                 + "w.creatorName = :userId and w.organizationId = :organizationId and "
134                 + "(w.state = :stateInstantiated or w.state = :statePaused or w.state = :stateRunning "
135                 + "or w.state = :stateFailing)"),
136 })
137 public class WorkflowInstance {
138 
139   /** Workflow ID, primary key */
140   /** The workflow id is the same as the related job id */
141   /** It is set by the workflow service when creating the instance */
142   @Id
143   @Column(name = "id")
144   private long workflowId;
145 
146   @Column(name = "state", length = 128)
147   private WorkflowState state;
148 
149   @Column(name = "template")
150   private String template;
151 
152   @Column(name = "title")
153   private String title;
154 
155   @Column(name = "description")
156   @Lob
157   private String description;
158 
159   @Column(name = "creator_id")
160   private String creatorName;
161 
162   @Column(name = "organization_id")  //NB: This column definition needs to match WorkflowIndexData!
163   private String organizationId;
164 
165   @Column(name = "date_created")
166   @Temporal(TemporalType.TIMESTAMP)
167   private Date dateCreated = null;
168 
169   @Column(name = "date_completed")
170   @Temporal(TemporalType.TIMESTAMP)
171   private Date dateCompleted = null;
172 
173   @Lob
174   @Column(name = "mediapackage", length = 16777215)
175   private String mediaPackage;
176 
177   @Transient
178   private MediaPackage mediaPackageObj;
179 
180   @OneToMany(
181           mappedBy = "instance",
182           cascade = CascadeType.ALL,
183           orphanRemoval = true,
184           fetch = FetchType.LAZY
185   )
186   @OrderColumn(name = "position")
187   protected List<WorkflowOperationInstance> operations;
188 
189   @ElementCollection
190   @CollectionTable(
191           name = "oc_workflow_configuration",
192           joinColumns = @JoinColumn(name = "workflow_id"),
193           indexes = {
194                 @Index(name = "IX_oc_workflow_configuration_workflow_id", columnList = ("workflow_id")),
195           }
196   )
197   @MapKeyColumn(name = "configuration_key")
198   @Lob
199   @Column(name = "configuration_value")
200   protected Map<String, String> configurations;
201 
202   @Column(name = "mediapackage_id", length = 128) //NB: This column definition needs to match WorkflowIndexData!
203   protected String mediaPackageId;
204 
205   @Column(name = "series_id", length = 128)
206   protected String seriesId;
207 
208   public enum WorkflowState {
209     INSTANTIATED, RUNNING, STOPPED, PAUSED, SUCCEEDED, FAILED, FAILING;
210 
211     public boolean isTerminated() {
212       switch (this) {
213         case STOPPED:
214         case SUCCEEDED:
215         case FAILED:
216           return true;
217         default:
218           return false;
219       }
220     }
221     public static class Adapter extends XmlAdapter<String, WorkflowState> {
222 
223       @Override
224       public String marshal(WorkflowState workflowState) {
225         return workflowState == null ? null : workflowState.toString().toLowerCase();
226       }
227 
228       @Override
229       public WorkflowState unmarshal(String val) {
230         return val == null ? null : WorkflowState.valueOf(val.toUpperCase());
231       }
232 
233     }
234   }
235 
236   /** Logging utilities */
237   private static final Logger logger = LoggerFactory.getLogger(WorkflowInstance.class);
238 
239   /**
240    * Default constructor without any import.
241    */
242   public WorkflowInstance() {
243 
244   }
245 
246   /**
247    * Constructs a new workflow instance from the given definition, mediapackage, and optional parent workflow ID and
248    * properties.
249    */
250   public WorkflowInstance(WorkflowDefinition def, MediaPackage mediaPackage, User creator,
251           Organization organization, Map<String, String> configuration) {
252     this.workflowId = -1; // this should be set by the workflow service once the workflow is persisted
253     this.title = def.getTitle();
254     this.template = def.getId();
255     this.description = def.getDescription();
256     this.creatorName = creator != null ? creator.getUsername() : null;
257     this.organizationId = organization != null ? organization.getId() : null;
258     this.state = WorkflowState.INSTANTIATED;
259     this.dateCreated = new Date();
260     this.mediaPackageObj = mediaPackage;
261     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
262     this.mediaPackageId = mediaPackage == null ? null : mediaPackage.getIdentifier().toString();
263     this.seriesId = mediaPackage == null ? null : mediaPackage.getSeries();
264 
265     this.operations = new ArrayList<>();
266     extend(def);
267 
268     this.configurations = new HashMap<>();
269     if (configuration != null) {
270       this.configurations.putAll(configuration);
271     }
272   }
273 
274   public WorkflowInstance(
275           long id,
276           WorkflowState state,
277           String template,
278           String title,
279           String description,
280           String creatorName,
281           String organizationId,
282           Date dateCreated,
283           Date dateCompleted,
284           MediaPackage mediaPackage,
285           List<WorkflowOperationInstance> operations,
286           Map<String, String> configurations,
287           String mediaPackageId,
288           String seriesId) {
289     this.workflowId = id;
290     this.state = state;
291     this.template = template;
292     this.title = title;
293     this.description = description;
294     this.creatorName = creatorName;
295     this.organizationId = organizationId;
296     this.dateCreated = dateCreated;
297     this.dateCompleted = dateCompleted;
298     this.mediaPackageObj = mediaPackage;
299     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
300     this.operations = operations;
301     this.configurations = configurations;
302     this.mediaPackageId = mediaPackageId;
303     this.seriesId = seriesId;
304   }
305 
306   public long getId() {
307     return workflowId;
308   }
309 
310   public void setId(long workflowId) {
311     this.workflowId = workflowId;
312   }
313 
314   public WorkflowState getState() {
315     return state;
316   }
317 
318   public void setState(WorkflowState state) {
319     if (dateCompleted == null && state.isTerminated()) {
320       dateCompleted = new Date();
321     }
322 
323     this.state = state;
324   }
325 
326   public String getTemplate() {
327     return template;
328   }
329 
330   public void setTemplate(String template) {
331     this.template = template;
332   }
333 
334   public String getTitle() {
335     return title;
336   }
337 
338   public void setTitle(String title) {
339     this.title = title;
340   }
341 
342   public String getDescription() {
343     return description;
344   }
345 
346   public void setDescription(String description) {
347     this.description = description;
348   }
349 
350   public String getCreatorName() {
351     return creatorName;
352   }
353 
354   public void setCreatorName(String creatorName) {
355     this.creatorName = creatorName;
356   }
357 
358   public String getOrganizationId() {
359     return organizationId;
360   }
361 
362   public void setOrganizationId(String organizationId) {
363     this.organizationId = organizationId;
364   }
365 
366   public Date getDateCreated() {
367     return dateCreated;
368   }
369 
370   public void setDateCreated(Date dateCreated) {
371     this.dateCreated = dateCreated;
372   }
373 
374   public Date getDateCompleted() {
375     return dateCompleted;
376   }
377 
378   public void setDateCompleted(Date dateCompleted) {
379     this.dateCompleted = dateCompleted;
380   }
381 
382   public MediaPackage getMediaPackage()  {
383     try {
384       if (mediaPackageObj != null) {
385         return mediaPackageObj;
386       }
387       if (mediaPackage != null) {
388         mediaPackageObj = MediaPackageParser.getFromXml(mediaPackage);
389         return mediaPackageObj;
390       }
391     } catch (MediaPackageException e) {
392       logger.error("Error parsing media package in workflow instance", e);
393     }
394     return null;
395   }
396 
397   public void setMediaPackage(MediaPackage mediaPackage) {
398     this.mediaPackageObj = mediaPackage;
399     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
400     this.mediaPackageId = mediaPackage == null ? null : mediaPackage.getIdentifier().toString();
401     this.seriesId = mediaPackage == null ? null : mediaPackage.getSeries();
402   }
403 
404   public boolean isActive() {
405     return !getState().isTerminated();
406   }
407 
408   /**
409    * {@inheritDoc}
410    *
411    * @see org.opencastproject.workflow.api.WorkflowInstance#getOperations()
412    */
413   public List<WorkflowOperationInstance> getOperations() {
414     if (operations == null) {
415       operations = new ArrayList<>();
416     }
417     return operations;
418   }
419 
420   /**
421    * Sets the workflow operations on this workflow instance
422    *
423    * @param workflowOperationInstanceList List of operations to set.
424    */
425   public final void setOperations(List<WorkflowOperationInstance> workflowOperationInstanceList) {
426     for (var workflowOperationInstance : workflowOperationInstanceList) {
427       workflowOperationInstance.setWorkflowInstance(this);
428     }
429     this.operations = workflowOperationInstanceList;
430   }
431 
432   /**
433    * Returns the workflow operation that is currently active or next to be executed.
434    *
435    * @return the current operation
436    */
437   public WorkflowOperationInstance getCurrentOperation() {
438     logger.debug("operations: {}", operations);
439     if (operations == null) {
440       return null;
441     }
442 
443     // Find first operation to work on. This should be the first one in state RUNNING; PAUSED, INSTANTIATED or RETRY.
444     // If one is active right now, it should be RUNNING or PAUSED.
445     // If none is active right now, it should be INSTANTIATED or RETRY as this should be the next one being run.
446     var currentStates = List.of(RUNNING, PAUSED, RETRY, INSTANTIATED);
447     for (var operation : operations) {
448       if (currentStates.contains(operation.getState())) {
449         logger.debug("current operation: {}", operation);
450         return operation;
451       }
452     }
453     return null;
454   }
455 
456   public Map<String, String> getConfigurations() {
457     if (configurations == null) {
458       return Collections.emptyMap();
459     }
460     return configurations;
461   }
462 
463   /**
464    * {@inheritDoc}
465    *
466    * @see org.opencastproject.workflow.api.Configurable#getConfiguration(java.lang.String)
467    */
468   public String getConfiguration(String key) {
469     if (key == null || configurations == null) {
470       return null;
471     }
472     return configurations.get(key);
473   }
474 
475   /**
476    * {@inheritDoc}
477    *
478    * @see org.opencastproject.workflow.api.Configurable#getConfigurationKeys()
479    */
480   public Set<String> getConfigurationKeys() {
481     if (configurations == null) {
482       return Collections.emptySet();
483     }
484     return configurations.keySet();
485   }
486 
487   /**
488    * {@inheritDoc}
489    *
490    * @see org.opencastproject.workflow.api.Configurable#removeConfiguration(java.lang.String)
491    */
492   public void removeConfiguration(String key) {
493     if (key == null || configurations == null) {
494       return;
495     }
496     configurations.remove(key);
497   }
498 
499   /**
500    * {@inheritDoc}
501    *
502    * @see org.opencastproject.workflow.api.Configurable#setConfiguration(java.lang.String, java.lang.String)
503    */
504   public void setConfiguration(String key, String value) {
505     if (key == null) {
506       return;
507     }
508     if (configurations == null) {
509       configurations = new TreeMap<>();
510     }
511 
512     // Adjust already existing values
513     configurations.put(key, value);
514   }
515 
516   @Override
517   public int hashCode() {
518     return Long.valueOf(workflowId).hashCode();
519   }
520 
521   @Override
522   public boolean equals(Object obj) {
523     if (obj instanceof WorkflowInstance) {
524       WorkflowInstance other = (WorkflowInstance) obj;
525       return workflowId == other.getId();
526     }
527     return false;
528   }
529 
530   @Override
531   public String toString() {
532     return "Workflow {" + workflowId + "}";
533   }
534 
535 
536   public void extend(WorkflowDefinition workflowDefinition) {
537     for (var operation : workflowDefinition.getOperations()) {
538       var operationInstance = new WorkflowOperationInstance(operation);
539       operationInstance.setWorkflowInstance(this);
540       operations.add(operationInstance);
541     }
542     setTemplate(workflowDefinition.getId());
543   }
544 
545   public void insert(WorkflowDefinition workflowDefinition, WorkflowOperationInstance after) {
546     var index = operations.indexOf(after) + 1;
547     for (var operation : workflowDefinition.getOperations()) {
548       var operationInstance = new WorkflowOperationInstance(operation);
549       operationInstance.setWorkflowInstance(this);
550       operations.add(index, operationInstance);
551       index++;
552     }
553   }
554 }