/* * Constellation - An open source and standard compliant SDI * http://www.constellation-sdi.org * * Copyright 2014 Geomatys. * * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 * * 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.constellation.admin; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.sis.metadata.iso.DefaultIdentifier; import org.apache.sis.metadata.iso.citation.DefaultCitation; import org.apache.sis.metadata.iso.identification.DefaultServiceIdentification; import org.apache.sis.parameter.ParameterBuilder; import org.constellation.admin.exception.ConstellationException; import org.constellation.api.TaskState; import org.constellation.business.IProcessBusiness; import org.constellation.configuration.ConfigurationException; import org.constellation.database.api.jooq.tables.pojos.ChainProcess; import org.constellation.database.api.jooq.tables.pojos.Task; import org.constellation.database.api.jooq.tables.pojos.TaskParameter; import org.constellation.database.api.repository.ChainProcessRepository; import org.constellation.database.api.repository.TaskParameterRepository; import org.constellation.database.api.repository.TaskRepository; import org.constellation.scheduler.QuartzJobListener; import org.constellation.scheduler.QuartzTask; import org.constellation.util.ParamUtilities; import org.geotoolkit.io.DirectoryWatcher; import org.geotoolkit.io.PathChangeListener; import org.geotoolkit.io.PathChangedEvent; import org.geotoolkit.process.Process; import org.geotoolkit.process.ProcessDescriptor; import org.geotoolkit.process.ProcessFinder; import org.geotoolkit.process.ProcessingRegistry; import org.geotoolkit.processing.chain.ChainProcessDescriptor; import org.geotoolkit.processing.chain.model.Chain; import org.geotoolkit.processing.chain.model.ChainMarshallerPool; import org.geotoolkit.processing.quartz.ProcessJobDetail; import org.geotoolkit.utility.parameter.ParametersExt; import org.geotoolkit.xml.parameter.ParameterValueReader; import org.opengis.metadata.Identifier; import org.opengis.metadata.identification.Identification; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.InvalidParameterValueException; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValueGroup; import org.opengis.util.NoSuchIdentifierException; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLStreamException; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.math.BigInteger; import java.nio.file.Path; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; import static org.quartz.impl.matchers.EverythingMatcher.allJobs; /** * * @author Cédric Briançon (Geomatys) * @author Christophe Mourette (Geomatys) * @author Quentin Boileau (Geomatys) * @author Guilhem Legal (Geomatys) */ @Component(ProcessBusiness.BEAN_NAME) @Primary @DependsOn("database-initer") public class ProcessBusiness implements IProcessBusiness { public static final String BEAN_NAME = "processBusiness"; private static final DateFormat TASK_DATE = new SimpleDateFormat("yyyy/MM/dd HH:mm"); private static final Logger LOGGER = LoggerFactory.getLogger("org.constellation.admin"); @Inject private TaskParameterRepository taskParameterRepository; @Inject private TaskRepository taskRepository; @Inject private ChainProcessRepository chainRepository; private Scheduler quartzScheduler; private DirectoryWatcher directoryWatcher; private Map<Integer, Object> scheduledTasks = new HashMap<>(); @PostConstruct public void init(){ //transaction needed for clean tasks in database SpringHelper.executeInTransaction(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { cleanTasksStates(); } }); LOGGER.info("Starting Constellation Scheduler"); /* Quartz scheduler */ Properties properties; try { properties = new Properties(); properties.load(ProcessBusiness.class.getResourceAsStream("/org/constellation/scheduler/tasks-quartz.properties")); } catch (IOException e) { LOGGER.warn("Failed to load quartz properties", e); //use default quartz configuration properties = null; } try { final StdSchedulerFactory schedFact = new StdSchedulerFactory(); if (properties != null) { schedFact.initialize(properties); } quartzScheduler = schedFact.getScheduler(); quartzScheduler.start(); //listen and attach a process on all geotk process tasks quartzScheduler.getListenerManager().addJobListener(new QuartzJobListener(), allJobs()); } catch (SchedulerException ex) { LOGGER.error("Failed to start quartz scheduler\n"+ex.getLocalizedMessage(), ex); return; } LOGGER.info("Constellation Scheduler successfully started"); /* DirectoryWatcher */ LOGGER.info("Starting directory watcher"); try { directoryWatcher = new DirectoryWatcher(true); final PathChangeListener pathListener = new PathChangeListener() { @Override public void pathChanged(PathChangedEvent event) { if (event.kind.equals(ENTRY_MODIFY) || event.kind.equals(ENTRY_CREATE)) { final Path target = event.target; for (Map.Entry<Integer, Object> sTask : scheduledTasks.entrySet()) { if (sTask.getValue() instanceof Path && target.startsWith((Path) sTask.getValue())) { final Integer taskId = sTask.getKey(); final TaskParameter taskParameter = taskParameterRepository.get(taskId); try { executeTaskParameter(taskParameter, null, taskParameter.getOwner()); } catch (ConfigurationException ex) { LOGGER.warn(ex.getMessage(), ex); } } } } } }; directoryWatcher.addPathChangeListener(pathListener); directoryWatcher.start(); } catch (IOException ex) { LOGGER.error("Failed to start directory watcher\n"+ex.getLocalizedMessage(), ex); return; } LOGGER.info("Directory watcher successfully started"); /* Re-programme taskParameters with trigger in scheduler. */ List<? extends TaskParameter> programmedTasks = taskParameterRepository.findProgrammedTasks(); for (TaskParameter taskParameter : programmedTasks) { try { scheduleTaskParameter(taskParameter, taskParameter.getName(), taskParameter.getOwner(), false); } catch (ConstellationException ex) { LOGGER.warn(ex.getMessage(), ex); } } } /** * {@inheritDoc} */ @Override public Map<String, Set<String>> listProcess(){ Map<String, Set<String>> processes = new TreeMap<>(); final Iterator<ProcessingRegistry> ite = ProcessFinder.getProcessFactories(); while(ite.hasNext()){ final ProcessingRegistry factory = ite.next(); final String authorityCode = factory.getIdentification().getCitation() .getIdentifiers().iterator().next().getCode(); Set<String> codes = new TreeSet<>(factory.getNames()); processes.put(authorityCode, codes); } return processes; } /** * {@inheritDoc} */ @Override public List<String> listProcessForFactory(final String authorityCode){ final List<String> names = new ArrayList<>(); final Iterator<ProcessingRegistry> ite = ProcessFinder.getProcessFactories(); while(ite.hasNext()){ final ProcessingRegistry factory = ite.next(); final String currentAuthorityCode = factory.getIdentification().getCitation() .getIdentifiers().iterator().next().getCode(); if (currentAuthorityCode.equals(authorityCode)) { for(String processCode : factory.getNames()){ names.add(processCode); } } } return names; } /** * {@inheritDoc} */ @Override public List<String> listProcessFactory(){ final List<String> names = new ArrayList<>(); final Iterator<ProcessingRegistry> ite = ProcessFinder.getProcessFactories(); while(ite.hasNext()){ final ProcessingRegistry factory = ite.next(); names.add(factory.getIdentification().getCitation() .getIdentifiers().iterator().next().getCode()); } return names; } @Override public TaskParameter getTaskParameterById(Integer id) { return taskParameterRepository.get(id); } @Override @Transactional public TaskParameter addTaskParameter(TaskParameter taskParameter) { return taskParameterRepository.create(taskParameter); } @Override @Transactional public void updateTaskParameter(TaskParameter taskParameter) { taskParameterRepository.update(taskParameter); } @Override @Transactional public void deleteTaskParameter(TaskParameter taskParameter) { taskParameterRepository.delete(taskParameter); } @Override public List<TaskParameter> findTaskParameterByNameAndProcess(String name, String authority, String code) { return (List<TaskParameter>) taskParameterRepository.findAllByNameAndProcess(name, authority, code); } @Override public void registerQuartzListener(JobListener jobListener) throws ConstellationException { try { quartzScheduler.getListenerManager().addJobListener(jobListener, allJobs()); } catch (SchedulerException e) { throw new ConstellationException("Unable to attach listener to quartz scheduler.", e); } } private ProcessDescriptor getDescriptor(final String authority, final String code) { final ProcessDescriptor desc; try { desc = ProcessFinder.getProcessDescriptor(authority, code); } catch (NoSuchIdentifierException ex) { throw new ConstellationException("No Process for id : {" + authority + "}"+code+" has been found"); } catch (InvalidParameterValueException ex) { throw new ConstellationException(ex); } if(desc == null){ throw new ConstellationException("No Process for id : {" + authority + "}"+code+" has been found"); } return desc; } private ParameterValueGroup readTaskParametersFromXML(final TaskParameter taskParameter, final ProcessDescriptor processDesc) { //change the description, always encapsulate in the same namespace and name //jaxb object factory can not reconize changing names without a namespace final ParameterDescriptorGroup idesc = processDesc.getInputDescriptor(); final GeneralParameterDescriptor retypedDesc = new ParameterBuilder().addName("input").setRequired(true) .createGroup(idesc.descriptors().toArray(new GeneralParameterDescriptor[0])); final ParameterValueGroup params; final ParameterValueReader reader = new ParameterValueReader(retypedDesc); try { reader.setInput(taskParameter.getInputs()); params = (ParameterValueGroup) reader.read(); reader.dispose(); } catch (XMLStreamException | IOException ex) { throw new ConstellationException(ex); } return params; } private ParameterValueGroup readTaskParametersFromJSON(final TaskParameter taskParameter, final ProcessDescriptor processDesc) throws ConfigurationException { final ParameterDescriptorGroup idesc = processDesc.getInputDescriptor(); ParameterValueGroup params; try { params = (ParameterValueGroup) ParamUtilities.readParameterJSON(taskParameter.getInputs(), idesc); } catch (IOException e) { throw new ConfigurationException("Fail to parse input parameter as JSON : "+e.getMessage(), e); } return params; } private void registerJobInScheduler(String title, Integer taskParameterId, Integer userId, Trigger trigger, ProcessJobDetail detail) { final QuartzTask quartzTask = new QuartzTask(UUID.randomUUID().toString()); quartzTask.setDetail(detail); quartzTask.setTitle(title); quartzTask.setTrigger(trigger); quartzTask.setTaskParameterId(taskParameterId); quartzTask.setUserId(userId); registerTaskInScheduler(quartzTask); } /** * Read TaskParameter process description and inputs to create a ProcessJobDetail for quartz scheduler. * * @param task TaskParameter * @param createProcess flag that specified if the process is instantiated in ProcessJobDetails or * ProcessJobDetails create it-self a new instance each time is executed. * @return ProcessJobDetails */ private ProcessJobDetail createJobDetailFromTaskParameter(final TaskParameter task, final boolean createProcess) throws ConfigurationException { final ProcessDescriptor processDesc = getDescriptor(task.getProcessAuthority(), task.getProcessCode()); final ParameterValueGroup params = readTaskParametersFromJSON(task, processDesc); if (createProcess) { final ParameterDescriptorGroup originalDesc = processDesc.getInputDescriptor(); final ParameterValueGroup orig = originalDesc.createValue(); ParametersExt.deepCopy(params, orig); final Process process = processDesc.createProcess(orig); return new ProcessJobDetail(process); } else { return new ProcessJobDetail(task.getProcessAuthority(), task.getProcessCode(), params); } } /** * Add the given task in the scheduler. */ private void registerTaskInScheduler(final QuartzTask quartzTask) throws ConstellationException{ //ensure the job detail contain the task in the datamap, this is used in the //job listener to track back the task quartzTask.getDetail().getJobDataMap().put(QuartzJobListener.PROPERTY_TASK, quartzTask); try { quartzScheduler.scheduleJob(quartzTask.getDetail(), quartzTask.getTrigger()); } catch (SchedulerException e) { throw new ConstellationException(e); } LOGGER.info("Scheduler task added : "+quartzTask.getId()+", "+quartzTask.getTitle() +" type : "+quartzTask.getDetail().getFactoryIdentifier()+"."+quartzTask.getDetail().getProcessIdentifier()); } /** * unregister the given task in the scheduler. */ private void unregisterTaskInScheduler(final JobKey key) throws SchedulerException{ quartzScheduler.interrupt(key); final boolean removed = quartzScheduler.deleteJob(key); if(removed){ LOGGER.info("Scheduler task removed : "+key); }else{ LOGGER.warn("Scheduler failed to remove task : "+key); } } /** * Get specific task from task journal (running or finished) * @param uuid task id * @return task object */ @Override public Task getTask(String uuid) { return taskRepository.get(uuid); } @Override @Transactional public Task addTask(Task task) throws ConstellationException { return taskRepository.create(task); } @Override @Transactional public void updateTask(Task task) throws ConstellationException { taskRepository.update(task); } @Override public List<Task> listRunningTasks() { return taskRepository.findRunningTasks(); } @Override public List<Task> listRunningTasks(Integer id, Integer offset, Integer limit) { return taskRepository.findRunningTasks(id, offset, limit); } @Override public List<Task> listTaskHistory(Integer id, Integer offset, Integer limit) { return taskRepository.taskHistory(id, offset, limit); } @Override public List<ProcessDescriptor> getChainDescriptors() throws ConstellationException { final List<ProcessDescriptor> result = new ArrayList<>(); final List<ChainProcess> chains = chainRepository.findAll(); for (ChainProcess cp : chains) { try { final Unmarshaller u = ChainMarshallerPool.getInstance().acquireUnmarshaller(); final Chain chain = (Chain) u.unmarshal(new StringReader(cp.getConfig())); ChainMarshallerPool.getInstance().recycle(u); final ProcessDescriptor desc = new ChainProcessDescriptor(chain, buildIdentification(chain.getName())); result.add(desc); } catch (JAXBException ex) { throw new ConstellationException("Unable to unmarshall chain configuration:" + cp.getId(), ex); } } return result; } public Identification buildIdentification(final String name) { final DefaultServiceIdentification ident = new DefaultServiceIdentification(); final Identifier id = new DefaultIdentifier(name); final DefaultCitation citation = new DefaultCitation(name); citation.setIdentifiers(Collections.singleton(id)); ident.setCitation(citation); return ident; } @Override @Transactional public void createChainProcess(final Chain chain) throws ConstellationException { final String code = chain.getName(); String config = null; try { final Marshaller m = ChainMarshallerPool.getInstance().acquireMarshaller(); final StringWriter sw = new StringWriter(); m.marshal(chain, sw); ChainMarshallerPool.getInstance().recycle(m); config = sw.toString(); } catch (JAXBException ex) { throw new ConstellationException("Unable to marshall chain configuration",ex); } final ChainProcess process = new ChainProcess(); process.setAuth("constellation"); process.setCode(code); process.setConfig(config); chainRepository.create(process); } @Override @Transactional public boolean deleteChainProcess(final String auth, final String code) { final ChainProcess chain = chainRepository.findOne(auth, code); if (chain != null) { chainRepository.delete(chain.getId()); return true; } return false; } @Override public ChainProcess getChainProcess(final String auth, final String code) { return chainRepository.findOne(auth, code); } /** * {@inheritDoc} */ @Override public void runProcess(final String title, final Process process, final Integer taskParameterId, final Integer userId) throws ConstellationException { final TriggerBuilder tb = TriggerBuilder.newTrigger(); final Trigger trigger = tb.startNow().build(); final ProcessJobDetail detail = new ProcessJobDetail(process); registerJobInScheduler(title, taskParameterId, userId, trigger, detail); } /** * {@inheritDoc} */ @Override public void executeTaskParameter (final TaskParameter taskParameter, String title, final Integer userId) throws ConstellationException, ConfigurationException { final TriggerBuilder tb = TriggerBuilder.newTrigger(); final Trigger trigger = tb.startNow().build(); final ProcessJobDetail jobDetail = createJobDetailFromTaskParameter(taskParameter, true); if (title == null) { title = taskParameter.getName()+TASK_DATE.format(new Date()); } registerJobInScheduler(title, taskParameter.getId(), userId, trigger, jobDetail); } /** * {@inheritDoc} */ @Override public void testTaskParameter(TaskParameter taskParameter) throws ConfigurationException { if (taskParameter.getInputs() != null && !taskParameter.getInputs().isEmpty()) { final ProcessDescriptor processDesc = getDescriptor(taskParameter.getProcessAuthority(), taskParameter.getProcessCode()); readTaskParametersFromJSON(taskParameter, processDesc); } else { throw new ConfigurationException("No input for task : " + taskParameter.getName()); } } /** * {@inheritDoc} */ @Override public void scheduleTaskParameter (final TaskParameter task, final String title, final Integer userId, boolean checkEndDate) throws ConstellationException { // Stop previous scheduling first. if (scheduledTasks.containsKey(task.getId())) { try { stopScheduleTaskParameter(task, userId); } catch (ConfigurationException e) { throw new ConstellationException("Unable to re-schedule task.", e); } } String trigger = task.getTrigger(); if (task.getTriggerType() != null && trigger != null && !trigger.isEmpty()) { if ("CRON".equalsIgnoreCase(task.getTriggerType())) { try { String cronExp = null; Date endDate = null; if (trigger.contains("{")) { ObjectMapper jsonMapper = new ObjectMapper(); jsonMapper.configure(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS, true); Map map = jsonMapper.readValue(trigger, Map.class); cronExp = (String) map.get("cron"); long endDateMs = ((BigInteger)map.get("endDate")).longValue(); if (endDateMs > 0) { endDate = new Date(endDateMs); } } else { cronExp = trigger; } if (cronExp == null) { throw new ConstellationException("Invalid cron expression. Can't be empty."); } if (endDate != null && endDate.before(new Date())) { String message = "Task " + task.getName() + " can't be scheduled : end date in the past."; if (checkEndDate) { throw new ConstellationException(message); } else { LOGGER.info(message); return; } } // HACK for Quartz to prevent ParseException : // "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented." // in this case replace the last '*' by '?' if (cronExp.matches("([0-9]\\d{0,1}|\\*) ([0-9]\\d{0,1}|\\*) ([0-9]\\d{0,1}|\\*) \\* ([0-9]\\d{0,1}|\\*) \\*")) { cronExp = cronExp.substring(0, cronExp.length()-1)+ "?"; } final ProcessJobDetail jobDetail = createJobDetailFromTaskParameter(task, false); final JobKey key = jobDetail.getKey(); final TriggerBuilder tb = TriggerBuilder.newTrigger(); final CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule(cronExp); final Trigger cronTrigger; if (endDate != null) { cronTrigger = tb.withSchedule(cronSchedule).forJob(key).endAt(endDate).build(); } else { cronTrigger = tb.withSchedule(cronSchedule).forJob(key).build(); } registerJobInScheduler(task.getName(), task.getId(), userId, cronTrigger, jobDetail); scheduledTasks.put(task.getId(), key); } catch (ParseException | ConfigurationException | IOException e) { throw new ConstellationException(e.getMessage(), e); } } else if ("FOLDER".equalsIgnoreCase(task.getTriggerType())) { final File folder = new File(trigger); final Path path = folder.toPath(); try { if (folder.exists() && folder.isDirectory()) { scheduledTasks.put(task.getId(), path); directoryWatcher.register(path); } else { throw new ConstellationException("Invalid folder trigger : " + trigger); } } catch (IOException e) { // remove task from scheduled list if (scheduledTasks.containsKey(task.getId())) { scheduledTasks.remove(task.getId()); } throw new ConstellationException(e.getMessage(), e); } } } } @Override public void stopScheduleTaskParameter(final TaskParameter task, final Integer userId) throws ConstellationException, ConfigurationException { if (!scheduledTasks.containsKey(task.getId())) { throw new ConstellationException("Task "+task.getName()+" wasn't scheduled."); } final Object obj = scheduledTasks.get(task.getId()); //scheduled task if (obj instanceof JobKey) { try { unregisterTaskInScheduler((JobKey) obj); scheduledTasks.remove(task.getId()); } catch (SchedulerException e) { throw new ConstellationException(e.getMessage(), e); } } else if (obj instanceof Path) { //directory watched task directoryWatcher.unregister((Path) obj); scheduledTasks.remove(task.getId()); } else { throw new ConstellationException("Unable to stop scheduled task " + task.getName()); } } @PreDestroy @Transactional public void stop() { LOGGER.info("=== Stopping Scheduler ==="); try { LOGGER.info("=== Wait for job to stop ==="); quartzScheduler.shutdown(false); quartzScheduler = null; } catch (SchedulerException ex) { LOGGER.error("=== Failed to stop quartz scheduler ===", ex); } LOGGER.info("=== Scheduler successfully stopped ==="); LOGGER.info("=== Stopping directory watcher ==="); try { directoryWatcher.close(); } catch (IOException ex) { LOGGER.error("=== Failed to stop directory watcher ===", ex); } LOGGER.info("=== Directory watcher successfully stopped ==="); cleanTasksStates(); } /** * Clear remaining running tasks before server shutdown or after server startup */ private void cleanTasksStates() { List<Task> runningTasks = taskRepository.findRunningTasks(); if (!runningTasks.isEmpty()) { LOGGER.info("=== Clear remaining running tasks ==="); } long now = System.currentTimeMillis(); String msg = "Stopped by server shutdown"; for (Task runningTask : runningTasks) { if (runningTask.getDateEnd() == null) { runningTask.setDateEnd(now); runningTask.setState(TaskState.CANCELLED.name()); runningTask.setMessage(msg); taskRepository.update(runningTask); } } } }