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