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) throws NotFoundException, IncidentServiceException {
121     List<Incident> incidents = getIncidentsOfJob(jobId);
122     List<IncidentTree> childIncidents = new ArrayList<>();
123 
124     try {
125       Job job = getServiceRegistry().getJob(jobId);
126       if (cascade && !"START_WORKFLOW".equals(job.getOperation())) {
127         childIncidents = getChildIncidents(jobId);
128       } else if (cascade && "START_WORKFLOW".equals(job.getOperation())) {
129         for (WorkflowOperationInstance operation : getWorkflowService().getWorkflowById(jobId).getOperations()) {
130           if (operation.getState().equals(OperationState.INSTANTIATED))
131             continue;
132           IncidentTree operationResult = getIncidentsOfJob(operation.getId(), true);
133           if (hasIncidents(Collections.list(operationResult)))
134             childIncidents.add(operationResult);
135         }
136       }
137       return new IncidentTreeImpl(incidents, childIncidents);
138     } catch (NotFoundException ignore) {
139       // Workflow deleted
140       return new IncidentTreeImpl(incidents, childIncidents);
141     } catch (Exception e) {
142       logger.error("Error loading child jobs of: {}", jobId);
143       throw new IncidentServiceException(e);
144     }
145   }
146 
147   private boolean hasIncidents(List<IncidentTree> incidentResults) {
148     for (IncidentTree result : incidentResults) {
149       if (result.getIncidents().size() > 0 || hasIncidents(result.getDescendants()))
150         return true;
151     }
152     return false;
153   }
154 
155   @Override
156   public IncidentL10n getLocalization(long id, Locale locale) throws IncidentServiceException, NotFoundException {
157     final Incident incident = getIncident(id);
158 
159     final List<String> loc = localeToList(locale);
160     // check if cache map is empty
161     // fill cache from
162     final String title = findText(loc, incident.getCode(), FIELD_TITLE).orElse(NO_TITLE);
163     final String description = findText(loc, incident.getCode(), FIELD_DESCRIPTION)
164         .map(text -> replaceVars(text, incident.getDescriptionParameters()))
165         .orElse(NO_DESCRIPTION);
166     return new IncidentL10n() {
167       @Override
168       public String getTitle() {
169         return title;
170       }
171 
172       @Override
173       public String getDescription() {
174         return description;
175       }
176     };
177   }
178 
179   /**
180    * Find a localization text in the database. The keys to look for are made from the locale, the incident code and the
181    * type.
182    *
183    * @param locale
184    *          The locale as a list. See {@link AbstractIncidentService#localeToList(java.util.Locale)}
185    * @param incidentCode
186    *          The incident code. See {@link org.opencastproject.job.api.Incident#getCode()}
187    * @param field
188    *          The field, e.g. "title" or "description"
189    * @return the found text wrapped in an Optional
190    */
191   private Optional<String> findText(List<String> locale, String incidentCode, String field) {
192     final List<String> keys = genDbKeys(locale, incidentCode + "." + field);
193     for (String key : keys) {
194       final Optional<String> text = getText(key);
195       if (text.isPresent()) {
196         return text;
197       }
198     }
199     return Optional.empty();
200   }
201 
202   private final Map<String, String> textCache = new HashMap<>();
203 
204   /** Get a text. */
205   private Optional<String> getText(String key) {
206     synchronized (textCache) {
207       if (textCache.isEmpty()) {
208         textCache.putAll(fetchTextsFromDb());
209       }
210     }
211     return Optional.ofNullable(textCache.get(key));
212   }
213 
214   /** Fetch all localizations from the database. */
215   private Map<String, String> fetchTextsFromDb() {
216     final Map<String, String> locs = new HashMap<String, String>();
217     for (IncidentTextDto a : getDBSession().execTx(IncidentTextDto.findAllQuery)) {
218       locs.put(a.getId(), a.getText());
219     }
220     return locs;
221   }
222 
223   private List<IncidentTree> getChildIncidents(long jobId) throws NotFoundException, ServiceRegistryException,
224           IncidentServiceException {
225     List<Job> childJobs = getServiceRegistry().getChildJobs(jobId);
226     List<IncidentTree> incidentResults = new ArrayList<>();
227     for (Job childJob : childJobs) {
228       if (childJob.getParentJobId() != jobId)
229         continue;
230       List<Incident> incidentsForJob = getIncidentsOfJob(childJob.getId());
231       IncidentTree incidentTree = new IncidentTreeImpl(incidentsForJob, getChildIncidents(childJob.getId()));
232       if (hasIncidents(Collections.list(incidentTree)))
233         incidentResults.add(incidentTree);
234     }
235     return incidentResults;
236   }
237 
238   private List<Incident> getIncidentsOfJob(long jobId) throws NotFoundException, IncidentServiceException {
239     final Job job = findJob(jobId);
240     try {
241       return getDBSession().execTx(IncidentDto.findByJobIdQuery(jobId)).stream()
242           .map(i -> toIncident(job, i))
243           .collect(Collectors.toList());
244     } catch (Exception e) {
245       logger.error("Could not retrieve incidents of job '{}': {}", job.getId(), e.getMessage());
246       throw new IncidentServiceException(e);
247     }
248   }
249 
250   private Job findJob(long jobId) throws NotFoundException, IncidentServiceException {
251     try {
252       return getServiceRegistry().getJob(jobId);
253     } catch (NotFoundException e) {
254       logger.info("Job with Id {} does not exist", jobId);
255       throw e;
256     } catch (ServiceRegistryException e) {
257       logger.error("Could not retrieve job {}: {}", jobId, e.getMessage());
258       throw new IncidentServiceException(e);
259     }
260   }
261 
262   private static Incident toIncident(Job job, IncidentDto dto) {
263     return new IncidentImpl(dto.getId(), job.getId(), job.getJobType(), job.getProcessingHost(), dto.getTimestamp(),
264             dto.getSeverity(), dto.getCode(), dto.getTechnicalInformation(), dto.getParameters());
265   }
266 
267   /**
268    * Create a list of localization database keys from a base key and a locale split into its part, e.g. ["de", "DE"] or
269    * ["en"]. The returned list starts with the most specific key getting more common, e.g.
270    * ["org.opencastproject.composer.1.title.de.DE", "org.opencastproject.composer.1.title.de",
271    * "org.opencastproject.composer.1.title"]
272    */
273   public static List<String> genDbKeys(List<String> locale, String base) {
274     List<String> result = new LinkedList<>();
275     StringBuilder key = new StringBuilder(base);
276     result.add(base);
277     for (String s: locale) {
278       key.append('.').append(s);
279       result.add(0, key.toString());
280     }
281     return result;
282   }
283 
284   /** Convert a locale into a list of strings, [language, country, variant] */
285   public static List<String> localeToList(Locale locale) {
286     return Stream.of(locale.getLanguage(), locale.getCountry(), locale.getVariant())
287         .flatMap(s -> trimToNone(s).stream())
288         .collect(Collectors.toList());
289   }
290 
291   /** Replace variables of the form #{xxx} in a string template. */
292   public static String replaceVars(String template, Map<String, String> params) {
293     String s = template;
294     for (Map.Entry<String, String> e : params.entrySet()) {
295       s = s.replace("#{" + e.getKey() + "}", e.getValue());
296     }
297     return s;
298   }
299 }