/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.serviceregistry.impl; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.option; import org.opencastproject.fun.juc.Mutables; import org.opencastproject.job.api.Incident; import org.opencastproject.job.api.IncidentImpl; import org.opencastproject.job.api.IncidentTree; import org.opencastproject.job.api.IncidentTreeImpl; import org.opencastproject.job.api.Job; import org.opencastproject.serviceregistry.api.IncidentL10n; import org.opencastproject.serviceregistry.api.IncidentService; import org.opencastproject.serviceregistry.api.IncidentServiceException; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.data.Collections; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Function2; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; import org.opencastproject.util.data.functions.Functions; import org.opencastproject.util.data.functions.Strings; import org.opencastproject.util.persistence.PersistenceEnv; import org.opencastproject.util.persistence.Queries; import org.opencastproject.workflow.api.WorkflowOperationInstance; import org.opencastproject.workflow.api.WorkflowOperationInstance.OperationState; import org.opencastproject.workflow.api.WorkflowService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public abstract class AbstractIncidentService implements IncidentService { public static final String PERSISTENCE_UNIT_NAME = "org.opencastproject.serviceregistry"; public static final String NO_TITLE = "-"; public static final String NO_DESCRIPTION = "-"; public static final String FIELD_TITLE = "title"; public static final String FIELD_DESCRIPTION = "description"; /** The logging instance */ private static final Logger logger = LoggerFactory.getLogger(AbstractIncidentService.class); protected abstract ServiceRegistry getServiceRegistry(); protected abstract WorkflowService getWorkflowService(); protected abstract PersistenceEnv getPenv(); @Override public Incident storeIncident(Job job, Date timestamp, String code, Incident.Severity severity, Map<String, String> descriptionParameters, List<Tuple<String, String>> details) throws IncidentServiceException, IllegalStateException { try { job = getServiceRegistry().getJob(job.getId()); final IncidentDto dto = getPenv().tx( Queries.persist(IncidentDto.mk(job.getId(), timestamp, code, severity, descriptionParameters, details))); return toIncident(job, dto); } catch (NotFoundException e) { throw new IllegalStateException("Can't create incident for not-existing job"); } catch (Exception e) { logger.error("Could not store job incident: {}", e.getMessage()); throw new IncidentServiceException(e); } } @Override public Incident getIncident(long id) throws IncidentServiceException, NotFoundException { for (IncidentDto dto : getPenv().tx(Queries.find(IncidentDto.class, id))) { final Job job = findJob(dto.getJobId()); if (job != null) { return toIncident(job, dto); } } throw new NotFoundException(); } @Override public List<Incident> getIncidentsOfJob(List<Long> jobIds) throws IncidentServiceException { List<Incident> incidents = new ArrayList<Incident>(); for (long jobId : jobIds) { try { incidents.addAll(getIncidentsOfJob(jobId)); } catch (NotFoundException ignore) { } } return incidents; } @Override public IncidentTree getIncidentsOfJob(long jobId, boolean cascade) throws NotFoundException, IncidentServiceException { List<Incident> incidents = getIncidentsOfJob(jobId); List<IncidentTree> childIncidents = new ArrayList<IncidentTree>(); try { Job job = getServiceRegistry().getJob(jobId); if (cascade && !"START_WORKFLOW".equals(job.getOperation())) { childIncidents = getChildIncidents(jobId); } else if (cascade && "START_WORKFLOW".equals(job.getOperation())) { for (WorkflowOperationInstance operation : getWorkflowService().getWorkflowById(jobId).getOperations()) { if (operation.getState().equals(OperationState.INSTANTIATED)) continue; IncidentTree operationResult = getIncidentsOfJob(operation.getId(), true); if (hasIncidents(Collections.list(operationResult))) childIncidents.add(operationResult); } } return new IncidentTreeImpl(incidents, childIncidents); } catch (NotFoundException ignore) { // Workflow deleted return new IncidentTreeImpl(incidents, childIncidents); } catch (Exception e) { logger.error("Error loading child jobs of {}: {}", jobId); throw new IncidentServiceException(e); } } private boolean hasIncidents(List<IncidentTree> incidentResults) { for (IncidentTree result : incidentResults) { if (result.getIncidents().size() > 0 || hasIncidents(result.getDescendants())) return true; } return false; } @Override public IncidentL10n getLocalization(long id, Locale locale) throws IncidentServiceException, NotFoundException { final Incident incident = getIncident(id); final List<String> loc = localeToList(locale); // check if cache map is empty // fill cache from final String title = findText(loc, incident.getCode(), FIELD_TITLE).getOrElse(NO_TITLE); final String description = findText(loc, incident.getCode(), FIELD_DESCRIPTION).map( replaceVarsF(incident.getDescriptionParameters())).getOrElse(NO_DESCRIPTION); return new IncidentL10n() { @Override public String getTitle() { return title; } @Override public String getDescription() { return description; } }; } /** * Find a localization text in the database. The keys to look for are made from the locale, the incident code and the * type. * * @param locale * The locale as a list. See {@link AbstractIncidentService#localeToList(java.util.Locale)} * @param incidentCode * The incident code. See {@link org.opencastproject.job.api.Incident#getCode()} * @param field * The field, e.g. "title" or "description" * @return the found text wrapped in an option */ private Option<String> findText(List<String> locale, String incidentCode, String field) { final List<String> keys = genDbKeys(locale, incidentCode + "." + field); for (String key : keys) { final Option<String> text = getText(key); if (text.isSome()) { return text; } } return none(); } private final Map<String, String> textCache = new HashMap<String, String>(); /** Get a text. */ private Option<String> getText(String key) { synchronized (textCache) { if (textCache.isEmpty()) { textCache.putAll(fetchTextsFromDb()); } } return option(textCache.get(key)); } /** Fetch all localizations from the database. */ private Map<String, String> fetchTextsFromDb() { final Map<String, String> locs = new HashMap<String, String>(); for (IncidentTextDto a : getPenv().tx(IncidentTextDto.findAll)) { locs.put(a.getId(), a.getText()); } return locs; } private List<IncidentTree> getChildIncidents(long jobId) throws NotFoundException, ServiceRegistryException, IncidentServiceException { List<Job> childJobs = getServiceRegistry().getChildJobs(jobId); List<IncidentTree> incidentResults = new ArrayList<IncidentTree>(); for (Job childJob : childJobs) { if (childJob.getParentJobId() != jobId) continue; List<Incident> incidentsForJob = getIncidentsOfJob(childJob.getId()); IncidentTree incidentTree = new IncidentTreeImpl(incidentsForJob, getChildIncidents(childJob.getId())); if (hasIncidents(Collections.list(incidentTree))) incidentResults.add(incidentTree); } return incidentResults; } private List<Incident> getIncidentsOfJob(long jobId) throws NotFoundException, IncidentServiceException { final Job job = findJob(jobId); try { return mlist(getPenv().tx(IncidentDto.findByJobId(jobId))).map(toIncident(job)).value(); } catch (Exception e) { logger.error("Could not retrieve incidents of job '{}': {}", job.getId(), e.getMessage()); throw new IncidentServiceException(e); } } private Job findJob(long jobId) throws NotFoundException, IncidentServiceException { try { return getServiceRegistry().getJob(jobId); } catch (NotFoundException e) { logger.info("Job with Id {} does not exist", jobId); throw e; } catch (ServiceRegistryException e) { logger.error("Could not retrieve job {}: {}", jobId, e.getMessage()); throw new IncidentServiceException(e); } } private static Incident toIncident(Job job, IncidentDto dto) { return new IncidentImpl(dto.getId(), job.getId(), job.getJobType(), job.getProcessingHost(), dto.getTimestamp(), dto.getSeverity(), dto.getCode(), dto.getTechnicalInformation(), dto.getParameters()); } private static Function<IncidentDto, Incident> toIncident(final Job job) { return new Function<IncidentDto, Incident>() { @Override public Incident apply(IncidentDto dto) { return toIncident(job, dto); } }; } /** * Create a list of localization database keys from a base key and a locale split into its part, e.g. ["de", "DE"] or * ["en"]. The returned list starts with the most specific key getting more common, e.g. * ["org.opencastproject.composer.1.title.de.DE", "org.opencastproject.composer.1.title.de", * "org.opencastproject.composer.1.title"] */ public static List<String> genDbKeys(List<String> locale, String base) { final List<String> keys = mlist(locale).foldl(Mutables.list(base), new Function2<List<String>, String, List<String>>() { @Override public List<String> apply(List<String> sum, String s) { sum.add(sum.get(sum.size() - 1) + "." + s); return sum; } }); return mlist(keys).reverse().value(); } /** Convert a locale into a list of strings, [language, country, variant] */ public static List<String> localeToList(Locale locale) { return mlist(Strings.trimToNone(locale.getLanguage()), Strings.trimToNone(locale.getCountry()), Strings.trimToNone(locale.getVariant())) // flatten .bind(Functions.<Option<String>> identity()).value(); } /** Replace variables of the form #{xxx} in a string template. */ public static String replaceVars(String template, Map<String, String> params) { String s = template; for (Map.Entry<String, String> e : params.entrySet()) { s = s.replace("#{" + e.getKey() + "}", e.getValue()); } return s; } /** * {@link org.opencastproject.serviceregistry.impl.AbstractIncidentService#replaceVars(String, java.util.Map)} as a * function. */ public static Function<String, String> replaceVarsF(final Map<String, String> params) { return new Function<String, String>() { @Override public String apply(String s) { return replaceVars(s, params); } }; } }