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.serviceregistry.impl;
23  
24  import static org.opencastproject.db.Queries.namedQuery;
25  import static org.opencastproject.util.data.Monadics.mlist;
26  import static org.opencastproject.util.data.Option.none;
27  import static org.opencastproject.util.data.Option.option;
28  
29  import org.opencastproject.db.DBSession;
30  import org.opencastproject.job.api.Incident;
31  import org.opencastproject.job.api.IncidentImpl;
32  import org.opencastproject.job.api.IncidentTree;
33  import org.opencastproject.job.api.IncidentTreeImpl;
34  import org.opencastproject.job.api.Job;
35  import org.opencastproject.serviceregistry.api.IncidentL10n;
36  import org.opencastproject.serviceregistry.api.IncidentService;
37  import org.opencastproject.serviceregistry.api.IncidentServiceException;
38  import org.opencastproject.serviceregistry.api.ServiceRegistry;
39  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
40  import org.opencastproject.util.NotFoundException;
41  import org.opencastproject.util.data.Collections;
42  import org.opencastproject.util.data.Function;
43  import org.opencastproject.util.data.Option;
44  import org.opencastproject.util.data.Tuple;
45  import org.opencastproject.util.data.functions.Functions;
46  import org.opencastproject.util.data.functions.Strings;
47  import org.opencastproject.workflow.api.WorkflowOperationInstance;
48  import org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState;
49  import org.opencastproject.workflow.api.WorkflowService;
50  
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  import java.util.ArrayList;
55  import java.util.Date;
56  import java.util.HashMap;
57  import java.util.LinkedList;
58  import java.util.List;
59  import java.util.Locale;
60  import java.util.Map;
61  import java.util.Optional;
62  import java.util.stream.Collectors;
63  
64  public abstract class AbstractIncidentService implements IncidentService {
65    public static final String PERSISTENCE_UNIT_NAME = "org.opencastproject.serviceregistry";
66  
67    public static final String NO_TITLE = "-";
68    public static final String NO_DESCRIPTION = "-";
69    public static final String FIELD_TITLE = "title";
70    public static final String FIELD_DESCRIPTION = "description";
71  
72    /** The logging instance */
73    private static final Logger logger = LoggerFactory.getLogger(AbstractIncidentService.class);
74  
75    protected abstract ServiceRegistry getServiceRegistry();
76  
77    protected abstract WorkflowService getWorkflowService();
78  
79    protected abstract DBSession getDBSession();
80  
81    @Override
82    public Incident storeIncident(Job job, Date timestamp, String code, Incident.Severity severity,
83            Map<String, String> descriptionParameters, List<Tuple<String, String>> details)
84            throws IncidentServiceException, IllegalStateException {
85      try {
86        job = getServiceRegistry().getJob(job.getId());
87  
88        final IncidentDto dto = getDBSession().execTx(namedQuery.persist(
89            IncidentDto.mk(job.getId(), timestamp, code, severity, descriptionParameters, details)
90        ));
91        return toIncident(job, dto);
92      } catch (NotFoundException e) {
93        throw new IllegalStateException("Can't create incident for not-existing job");
94      } catch (Exception e) {
95        logger.error("Could not store job incident: {}", e.getMessage());
96        throw new IncidentServiceException(e);
97      }
98    }
99  
100   @Override
101   public Incident getIncident(long id) throws IncidentServiceException, NotFoundException {
102     Optional<IncidentDto> dto = getDBSession().execTx(namedQuery.findByIdOpt(IncidentDto.class, id));
103     if (dto.isPresent()) {
104       final Job job = findJob(dto.get().getJobId());
105       if (job != null) {
106         return toIncident(job, dto.get());
107       }
108     }
109     throw new NotFoundException();
110   }
111 
112   @Override
113   public List<Incident> getIncidentsOfJob(List<Long> jobIds) throws IncidentServiceException {
114     List<Incident> incidents = new ArrayList<>();
115     for (long jobId : jobIds) {
116       try {
117         incidents.addAll(getIncidentsOfJob(jobId));
118       } catch (NotFoundException ignore) {
119       }
120     }
121     return incidents;
122   }
123 
124   @Override
125   public IncidentTree getIncidentsOfJob(long jobId, boolean cascade) throws NotFoundException, IncidentServiceException {
126     List<Incident> incidents = getIncidentsOfJob(jobId);
127     List<IncidentTree> childIncidents = new ArrayList<>();
128 
129     try {
130       Job job = getServiceRegistry().getJob(jobId);
131       if (cascade && !"START_WORKFLOW".equals(job.getOperation())) {
132         childIncidents = getChildIncidents(jobId);
133       } else if (cascade && "START_WORKFLOW".equals(job.getOperation())) {
134         for (WorkflowOperationInstance operation : getWorkflowService().getWorkflowById(jobId).getOperations()) {
135           if (operation.getState().equals(OperationState.INSTANTIATED))
136             continue;
137           IncidentTree operationResult = getIncidentsOfJob(operation.getId(), true);
138           if (hasIncidents(Collections.list(operationResult)))
139             childIncidents.add(operationResult);
140         }
141       }
142       return new IncidentTreeImpl(incidents, childIncidents);
143     } catch (NotFoundException ignore) {
144       // Workflow deleted
145       return new IncidentTreeImpl(incidents, childIncidents);
146     } catch (Exception e) {
147       logger.error("Error loading child jobs of: {}", jobId);
148       throw new IncidentServiceException(e);
149     }
150   }
151 
152   private boolean hasIncidents(List<IncidentTree> incidentResults) {
153     for (IncidentTree result : incidentResults) {
154       if (result.getIncidents().size() > 0 || hasIncidents(result.getDescendants()))
155         return true;
156     }
157     return false;
158   }
159 
160   @Override
161   public IncidentL10n getLocalization(long id, Locale locale) throws IncidentServiceException, NotFoundException {
162     final Incident incident = getIncident(id);
163 
164     final List<String> loc = localeToList(locale);
165     // check if cache map is empty
166     // fill cache from
167     final String title = findText(loc, incident.getCode(), FIELD_TITLE).getOrElse(NO_TITLE);
168     final String description = findText(loc, incident.getCode(), FIELD_DESCRIPTION).map(
169             replaceVarsF(incident.getDescriptionParameters())).getOrElse(NO_DESCRIPTION);
170     return new IncidentL10n() {
171       @Override
172       public String getTitle() {
173         return title;
174       }
175 
176       @Override
177       public String getDescription() {
178         return description;
179       }
180     };
181   }
182 
183   /**
184    * Find a localization text in the database. The keys to look for are made from the locale, the incident code and the
185    * type.
186    *
187    * @param locale
188    *          The locale as a list. See {@link AbstractIncidentService#localeToList(java.util.Locale)}
189    * @param incidentCode
190    *          The incident code. See {@link org.opencastproject.job.api.Incident#getCode()}
191    * @param field
192    *          The field, e.g. "title" or "description"
193    * @return the found text wrapped in an option
194    */
195   private Option<String> findText(List<String> locale, String incidentCode, String field) {
196     final List<String> keys = genDbKeys(locale, incidentCode + "." + field);
197     for (String key : keys) {
198       final Option<String> text = getText(key);
199       if (text.isSome()) {
200         return text;
201       }
202     }
203     return none();
204   }
205 
206   private final Map<String, String> textCache = new HashMap<>();
207 
208   /** Get a text. */
209   private Option<String> getText(String key) {
210     synchronized (textCache) {
211       if (textCache.isEmpty()) {
212         textCache.putAll(fetchTextsFromDb());
213       }
214     }
215     return option(textCache.get(key));
216   }
217 
218   /** Fetch all localizations from the database. */
219   private Map<String, String> fetchTextsFromDb() {
220     final Map<String, String> locs = new HashMap<String, String>();
221     for (IncidentTextDto a : getDBSession().execTx(IncidentTextDto.findAllQuery)) {
222       locs.put(a.getId(), a.getText());
223     }
224     return locs;
225   }
226 
227   private List<IncidentTree> getChildIncidents(long jobId) throws NotFoundException, ServiceRegistryException,
228           IncidentServiceException {
229     List<Job> childJobs = getServiceRegistry().getChildJobs(jobId);
230     List<IncidentTree> incidentResults = new ArrayList<>();
231     for (Job childJob : childJobs) {
232       if (childJob.getParentJobId() != jobId)
233         continue;
234       List<Incident> incidentsForJob = getIncidentsOfJob(childJob.getId());
235       IncidentTree incidentTree = new IncidentTreeImpl(incidentsForJob, getChildIncidents(childJob.getId()));
236       if (hasIncidents(Collections.list(incidentTree)))
237         incidentResults.add(incidentTree);
238     }
239     return incidentResults;
240   }
241 
242   private List<Incident> getIncidentsOfJob(long jobId) throws NotFoundException, IncidentServiceException {
243     final Job job = findJob(jobId);
244     try {
245       return getDBSession().execTx(IncidentDto.findByJobIdQuery(jobId)).stream()
246           .map(i -> toIncident(job, i))
247           .collect(Collectors.toList());
248     } catch (Exception e) {
249       logger.error("Could not retrieve incidents of job '{}': {}", job.getId(), e.getMessage());
250       throw new IncidentServiceException(e);
251     }
252   }
253 
254   private Job findJob(long jobId) throws NotFoundException, IncidentServiceException {
255     try {
256       return getServiceRegistry().getJob(jobId);
257     } catch (NotFoundException e) {
258       logger.info("Job with Id {} does not exist", jobId);
259       throw e;
260     } catch (ServiceRegistryException e) {
261       logger.error("Could not retrieve job {}: {}", jobId, e.getMessage());
262       throw new IncidentServiceException(e);
263     }
264   }
265 
266   private static Incident toIncident(Job job, IncidentDto dto) {
267     return new IncidentImpl(dto.getId(), job.getId(), job.getJobType(), job.getProcessingHost(), dto.getTimestamp(),
268             dto.getSeverity(), dto.getCode(), dto.getTechnicalInformation(), dto.getParameters());
269   }
270 
271   /**
272    * Create a list of localization database keys from a base key and a locale split into its part, e.g. ["de", "DE"] or
273    * ["en"]. The returned list starts with the most specific key getting more common, e.g.
274    * ["org.opencastproject.composer.1.title.de.DE", "org.opencastproject.composer.1.title.de",
275    * "org.opencastproject.composer.1.title"]
276    */
277   public static List<String> genDbKeys(List<String> locale, String base) {
278     List<String> result = new LinkedList<>();
279     StringBuilder key = new StringBuilder(base);
280     result.add(base);
281     for (String s: locale) {
282       key.append('.').append(s);
283       result.add(0, key.toString());
284     }
285     return result;
286   }
287 
288   /** Convert a locale into a list of strings, [language, country, variant] */
289   public static List<String> localeToList(Locale locale) {
290     return mlist(Strings.trimToNone(locale.getLanguage()), Strings.trimToNone(locale.getCountry()),
291             Strings.trimToNone(locale.getVariant()))
292     // flatten
293             .bind(Functions.<Option<String>> identity()).value();
294   }
295 
296   /** Replace variables of the form #{xxx} in a string template. */
297   public static String replaceVars(String template, Map<String, String> params) {
298     String s = template;
299     for (Map.Entry<String, String> e : params.entrySet()) {
300       s = s.replace("#{" + e.getKey() + "}", e.getValue());
301     }
302     return s;
303   }
304 
305   /**
306    * {@link org.opencastproject.serviceregistry.impl.AbstractIncidentService#replaceVars(String, java.util.Map)} as a
307    * function.
308    */
309   public static Function<String, String> replaceVarsF(final Map<String, String> params) {
310     return new Function<String, String>() {
311       @Override
312       public String apply(String s) {
313         return replaceVars(s, params);
314       }
315     };
316   }
317 }