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 is
72   * 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 where w.organizationId=:organizationId order by w.dateCreated"
85          ),
86          @NamedQuery(
87                  name = "Workflow.countLatest",
88                  query = "SELECT COUNT(DISTINCT w.mediaPackageId) FROM WorkflowInstance w"
89          ),
90          @NamedQuery(
91                  name = "Workflow.findAllOrganizationIndependent",
92                  query = "select w from WorkflowInstance w"
93          ),
94          @NamedQuery(
95                  name = "Workflow.workflowById",
96                  query = "SELECT w FROM WorkflowInstance as w where w.workflowId=:workflowId and w.organizationId=:organizationId"
97          ),
98          @NamedQuery(
99              name = "Workflow.workflowByIdOrganizationIndependent",
100             query = "SELECT w FROM WorkflowInstance as w where w.workflowId=:workflowId"
101         ),
102         @NamedQuery(
103                 name = "Workflow.getCount",
104                 query = "select COUNT(w) from WorkflowInstance w where w.organizationId=:organizationId "
105                         + "and (:state is null or w.state = :state) "
106         ),
107         @NamedQuery(
108                 name = "Workflow.toCleanup",
109                 query = "SELECT w FROM WorkflowInstance w where w.state = :state "
110                 + "and w.dateCreated < :dateCreated and w.organizationId = :organizationId"
111         ),
112 
113         // For media packages
114         @NamedQuery(name = "Workflow.byMediaPackage", query = "SELECT w FROM WorkflowInstance w where "
115                 + "w.mediaPackageId = :mediaPackageId and w.organizationId = :organizationId order by w.dateCreated"),
116         @NamedQuery(name = "Workflow.countActiveByMediaPackage", query = "SELECT COUNT(w) FROM WorkflowInstance w where "
117                 + "w.mediaPackageId = :mediaPackageId and w.organizationId = :organizationId and "
118                 + "(w.state = :stateInstantiated or w.state = :statePaused or w.state = :stateRunning "
119                 + "or w.state = :stateFailing)"),
120         @NamedQuery(name = "Workflow.byMediaPackageAndActive", query = "SELECT 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) order by w.dateCreated"),
124 
125         // For users
126         @NamedQuery(name = "Workflow.countActiveByUser", query = "SELECT COUNT(w) FROM WorkflowInstance w where "
127                 + "w.creatorName = :userId and w.organizationId = :organizationId and "
128                 + "(w.state = :stateInstantiated or w.state = :statePaused or w.state = :stateRunning "
129                 + "or w.state = :stateFailing)"),
130 })
131 public class WorkflowInstance {
132 
133   /** Workflow ID, primary key */
134   /** The workflow id is the same as the related job id */
135   /** It is set by the workflow service when creating the instance */
136   @Id
137   @Column(name = "id")
138   private long workflowId;
139 
140   @Column(name = "state", length = 128)
141   private WorkflowState state;
142 
143   @Column(name = "template")
144   private String template;
145 
146   @Column(name = "title")
147   private String title;
148 
149   @Column(name = "description")
150   @Lob
151   private String description;
152 
153   @Column(name = "creator_id")
154   private String creatorName;
155 
156   @Column(name = "organization_id")  //NB: This column definition needs to match WorkflowIndexData!
157   private String organizationId;
158 
159   @Column(name = "date_created")
160   @Temporal(TemporalType.TIMESTAMP)
161   private Date dateCreated = null;
162 
163   @Column(name = "date_completed")
164   @Temporal(TemporalType.TIMESTAMP)
165   private Date dateCompleted = null;
166 
167   @Lob
168   @Column(name = "mediapackage", length = 16777215)
169   private String mediaPackage;
170 
171   @Transient
172   private MediaPackage mediaPackageObj;
173 
174   @OneToMany(
175           mappedBy = "instance",
176           cascade = CascadeType.ALL,
177           orphanRemoval = true,
178           fetch = FetchType.LAZY
179   )
180   @OrderColumn(name = "position")
181   protected List<WorkflowOperationInstance> operations;
182 
183   @ElementCollection
184   @CollectionTable(
185           name = "oc_workflow_configuration",
186           joinColumns = @JoinColumn(name = "workflow_id"),
187           indexes = {
188                 @Index(name = "IX_oc_workflow_configuration_workflow_id", columnList = ("workflow_id")),
189           }
190   )
191   @MapKeyColumn(name = "configuration_key")
192   @Lob
193   @Column(name = "configuration_value")
194   protected Map<String, String> configurations;
195 
196   @Column(name = "mediapackage_id", length = 128) //NB: This column definition needs to match WorkflowIndexData!
197   protected String mediaPackageId;
198 
199   @Column(name = "series_id", length = 128)
200   protected String seriesId;
201 
202   public enum WorkflowState {
203     INSTANTIATED, RUNNING, STOPPED, PAUSED, SUCCEEDED, FAILED, FAILING;
204 
205     public boolean isTerminated() {
206       switch (this) {
207         case STOPPED:
208         case SUCCEEDED:
209         case FAILED:
210           return true;
211         default:
212           return false;
213       }
214     }
215     public static class Adapter extends XmlAdapter<String, WorkflowState> {
216 
217       @Override
218       public String marshal(WorkflowState workflowState) {
219         return workflowState == null ? null : workflowState.toString().toLowerCase();
220       }
221 
222       @Override
223       public WorkflowState unmarshal(String val) {
224         return val == null ? null : WorkflowState.valueOf(val.toUpperCase());
225       }
226 
227     }
228   }
229 
230   /** Logging utilities */
231   private static final Logger logger = LoggerFactory.getLogger(WorkflowInstance.class);
232 
233   /**
234    * Default constructor without any import.
235    */
236   public WorkflowInstance() {
237 
238   }
239 
240   /**
241    * Constructs a new workflow instance from the given definition, mediapackage, and optional parent workflow ID and
242    * properties.
243    */
244   public WorkflowInstance(WorkflowDefinition def, MediaPackage mediaPackage, User creator,
245           Organization organization, Map<String, String> configuration) {
246     this.workflowId = -1; // this should be set by the workflow service once the workflow is persisted
247     this.title = def.getTitle();
248     this.template = def.getId();
249     this.description = def.getDescription();
250     this.creatorName = creator != null ? creator.getUsername() : null;
251     this.organizationId = organization != null ? organization.getId() : null;
252     this.state = WorkflowState.INSTANTIATED;
253     this.dateCreated = new Date();
254     this.mediaPackageObj = mediaPackage;
255     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
256     this.mediaPackageId = mediaPackage == null ? null : mediaPackage.getIdentifier().toString();
257     this.seriesId = mediaPackage == null ? null : mediaPackage.getSeries();
258 
259     this.operations = new ArrayList<>();
260     extend(def);
261 
262     this.configurations = new HashMap<>();
263     if (configuration != null) {
264       this.configurations.putAll(configuration);
265     }
266   }
267 
268   public WorkflowInstance(
269           long id,
270           WorkflowState state,
271           String template,
272           String title,
273           String description,
274           String creatorName,
275           String organizationId,
276           Date dateCreated,
277           Date dateCompleted,
278           MediaPackage mediaPackage,
279           List<WorkflowOperationInstance> operations,
280           Map<String, String> configurations,
281           String mediaPackageId,
282           String seriesId) {
283     this.workflowId = id;
284     this.state = state;
285     this.template = template;
286     this.title = title;
287     this.description = description;
288     this.creatorName = creatorName;
289     this.organizationId = organizationId;
290     this.dateCreated = dateCreated;
291     this.dateCompleted = dateCompleted;
292     this.mediaPackageObj = mediaPackage;
293     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
294     this.operations = operations;
295     this.configurations = configurations;
296     this.mediaPackageId = mediaPackageId;
297     this.seriesId = seriesId;
298   }
299 
300   public long getId() {
301     return workflowId;
302   }
303 
304   public void setId(long workflowId) {
305     this.workflowId = workflowId;
306   }
307 
308   public WorkflowState getState() {
309     return state;
310   }
311 
312   public void setState(WorkflowState state) {
313     if (dateCompleted == null && state.isTerminated()) {
314       dateCompleted = new Date();
315     }
316 
317     this.state = state;
318   }
319 
320   public String getTemplate() {
321     return template;
322   }
323 
324   public void setTemplate(String template) {
325     this.template = template;
326   }
327 
328   public String getTitle() {
329     return title;
330   }
331 
332   public void setTitle(String title) {
333     this.title = title;
334   }
335 
336   public String getDescription() {
337     return description;
338   }
339 
340   public void setDescription(String description) {
341     this.description = description;
342   }
343 
344   public String getCreatorName() {
345     return creatorName;
346   }
347 
348   public void setCreatorName(String creatorName) {
349     this.creatorName = creatorName;
350   }
351 
352   public String getOrganizationId() {
353     return organizationId;
354   }
355 
356   public void setOrganizationId(String organizationId) {
357     this.organizationId = organizationId;
358   }
359 
360   public Date getDateCreated() {
361     return dateCreated;
362   }
363 
364   public void setDateCreated(Date dateCreated) {
365     this.dateCreated = dateCreated;
366   }
367 
368   public Date getDateCompleted() {
369     return dateCompleted;
370   }
371 
372   public void setDateCompleted(Date dateCompleted) {
373     this.dateCompleted = dateCompleted;
374   }
375 
376   public MediaPackage getMediaPackage()  {
377     try {
378       if (mediaPackageObj != null) {
379         return mediaPackageObj;
380       }
381       if (mediaPackage != null) {
382         mediaPackageObj = MediaPackageParser.getFromXml(mediaPackage);
383         return mediaPackageObj;
384       }
385     } catch (MediaPackageException e) {
386       logger.error("Error parsing media package in workflow instance", e);
387     }
388     return null;
389   }
390 
391   public void setMediaPackage(MediaPackage mediaPackage) {
392     this.mediaPackageObj = mediaPackage;
393     this.mediaPackage = mediaPackage == null ? null : MediaPackageParser.getAsXml(mediaPackage);
394     this.mediaPackageId = mediaPackage == null ? null : mediaPackage.getIdentifier().toString();
395     this.seriesId = mediaPackage == null ? null : mediaPackage.getSeries();
396   }
397 
398   public boolean isActive() {
399     return !getState().isTerminated();
400   }
401 
402   /**
403    * {@inheritDoc}
404    *
405    * @see org.opencastproject.workflow.api.WorkflowInstance#getOperations()
406    */
407   public List<WorkflowOperationInstance> getOperations() {
408     if (operations == null) {
409       operations = new ArrayList<>();
410     }
411     return operations;
412   }
413 
414   /**
415    * Sets the workflow operations on this workflow instance
416    *
417    * @param workflowOperationInstanceList List of operations to set.
418    */
419   public final void setOperations(List<WorkflowOperationInstance> workflowOperationInstanceList) {
420     for (var workflowOperationInstance : workflowOperationInstanceList) {
421       workflowOperationInstance.setWorkflowInstance(this);
422     }
423     this.operations = workflowOperationInstanceList;
424   }
425 
426   /**
427    * Returns the workflow operation that is currently active or next to be executed.
428    *
429    * @return the current operation
430    */
431   public WorkflowOperationInstance getCurrentOperation() {
432     logger.debug("operations: {}", operations);
433     if (operations == null) {
434       return null;
435     }
436 
437     // Find first operation to work on. This should be the first one in state RUNNING; PAUSED, INSTANTIATED or RETRY.
438     // If one is active right now, it should be RUNNING or PAUSED.
439     // If none is active right now, it should be INSTANTIATED or RETRY as this should be the next one being run.
440     var currentStates = List.of(RUNNING, PAUSED, RETRY, INSTANTIATED);
441     for (var operation : operations) {
442       if (currentStates.contains(operation.getState())) {
443         logger.debug("current operation: {}", operation);
444         return operation;
445       }
446     }
447     return null;
448   }
449 
450   public Map<String, String> getConfigurations() {
451     if (configurations == null) {
452       return Collections.emptyMap();
453     }
454     return configurations;
455   }
456 
457   /**
458    * {@inheritDoc}
459    *
460    * @see org.opencastproject.workflow.api.Configurable#getConfiguration(java.lang.String)
461    */
462   public String getConfiguration(String key) {
463     if (key == null || configurations == null)
464       return null;
465     return configurations.get(key);
466   }
467 
468   /**
469    * {@inheritDoc}
470    *
471    * @see org.opencastproject.workflow.api.Configurable#getConfigurationKeys()
472    */
473   public Set<String> getConfigurationKeys() {
474     if (configurations == null) {
475       return Collections.emptySet();
476     }
477     return configurations.keySet();
478   }
479 
480   /**
481    * {@inheritDoc}
482    *
483    * @see org.opencastproject.workflow.api.Configurable#removeConfiguration(java.lang.String)
484    */
485   public void removeConfiguration(String key) {
486     if (key == null || configurations == null)
487       return;
488     configurations.remove(key);
489   }
490 
491   /**
492    * {@inheritDoc}
493    *
494    * @see org.opencastproject.workflow.api.Configurable#setConfiguration(java.lang.String, java.lang.String)
495    */
496   public void setConfiguration(String key, String value) {
497     if (key == null)
498       return;
499     if (configurations == null)
500       configurations = new TreeMap<>();
501 
502     // Adjust already existing values
503     configurations.put(key, value);
504   }
505 
506   @Override
507   public int hashCode() {
508     return Long.valueOf(workflowId).hashCode();
509   }
510 
511   @Override
512   public boolean equals(Object obj) {
513     if (obj instanceof WorkflowInstance) {
514       WorkflowInstance other = (WorkflowInstance) obj;
515       return workflowId == other.getId();
516     }
517     return false;
518   }
519 
520   @Override
521   public String toString() {
522     return "Workflow {" + workflowId + "}";
523   }
524 
525 
526   public void extend(WorkflowDefinition workflowDefinition) {
527     for (var operation : workflowDefinition.getOperations()) {
528       var operationInstance = new WorkflowOperationInstance(operation);
529       operationInstance.setWorkflowInstance(this);
530       operations.add(operationInstance);
531     }
532     setTemplate(workflowDefinition.getId());
533   }
534 
535   public void insert(WorkflowDefinition workflowDefinition, WorkflowOperationInstance after) {
536     var index = operations.indexOf(after) + 1;
537     for (var operation : workflowDefinition.getOperations()) {
538       var operationInstance = new WorkflowOperationInstance(operation);
539       operationInstance.setWorkflowInstance(this);
540       operations.add(index, operationInstance);
541       index++;
542     }
543   }
544 }