/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.core.validation; import java.text.MessageFormat; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.SortedSet; import java.util.UUID; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; import org.eclipse.skalli.commons.CollectionUtils; import org.eclipse.skalli.commons.FormatUtils; import org.eclipse.skalli.core.rest.monitor.Monitorable; import org.eclipse.skalli.model.EntityBase; import org.eclipse.skalli.model.Issue; import org.eclipse.skalli.model.Project; import org.eclipse.skalli.model.Severity; import org.eclipse.skalli.model.ValidationException; import org.eclipse.skalli.services.configuration.ConfigurationService; import org.eclipse.skalli.services.configuration.EventConfigUpdate; import org.eclipse.skalli.services.entity.EntityService; import org.eclipse.skalli.services.entity.EntityServices; import org.eclipse.skalli.services.entity.EventEntityUpdate; import org.eclipse.skalli.services.event.EventListener; import org.eclipse.skalli.services.event.EventService; import org.eclipse.skalli.services.issues.Issues; import org.eclipse.skalli.services.issues.IssuesService; import org.eclipse.skalli.services.scheduler.RunnableSchedule; import org.eclipse.skalli.services.scheduler.SchedulerService; import org.eclipse.skalli.services.scheduler.Task; import org.eclipse.skalli.services.validation.Validation; import org.eclipse.skalli.services.validation.ValidationService; import org.osgi.service.component.ComponentConstants; import org.osgi.service.component.ComponentContext; import org.restlet.resource.ServerResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ValidationComponent implements ValidationService, Monitorable { private static final Logger LOG = LoggerFactory.getLogger(ValidationComponent.class); private static final String DEFAULT_USER = ValidationService.class.getName(); private static final Severity DEFAULT_SEVERITY = Severity.INFO; private static final long DEFAULT_QUEUED_INITIAL_DELAY = TimeUnit.SECONDS.toMillis(10); private static final long DEFAULT_QUEUED_PERIOD = TimeUnit.SECONDS.toMillis(10); private IssuesService issuesService; private SchedulerService schedulerService; private ConfigurationService configService; /** The unique identifiers of the schedules registered with the scheduler service */ private final Set<UUID> registeredSchedules = new HashSet<UUID>(); /** The unique identifier of the {@link QueueValidator} task */ private UUID taskIdQueueValidator; /** Entities queued for re-validation subsequent to a persist */ private final PriorityBlockingQueue<QueuedEntity<? extends EntityBase>> queuedEntities = new PriorityBlockingQueue<QueuedEntity<? extends EntityBase>>(); protected void activate(ComponentContext context) { LOG.info(MessageFormat.format("[ValidationService] {0} : activated", (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME))); } protected void deactivate(ComponentContext context) { LOG.info(MessageFormat.format("[ValidationService] {0} : deactivated", (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME))); } protected void bindIssuesService(IssuesService issuesService) { LOG.info(MessageFormat.format("bindIssuesService({0})", issuesService)); //$NON-NLS-1$ this.issuesService = issuesService; } protected void unbindIssuesService(IssuesService issuesService) { LOG.info(MessageFormat.format("unbindIssuesService({0})", issuesService)); //$NON-NLS-1$ this.issuesService = null; } protected void bindSchedulerService(SchedulerService schedulerService) { LOG.info(MessageFormat.format("bindSchedulerService({0})", schedulerService)); //$NON-NLS-1$ this.schedulerService = schedulerService; synchronizeAllTasks(); } protected void unbindSchedulerService(SchedulerService schedulerService) { LOG.info(MessageFormat.format("unbindSchedulerService({0})", schedulerService)); //$NON-NLS-1$ registeredSchedules.clear(); taskIdQueueValidator = null; this.schedulerService = null; } protected void bindConfigurationService(ConfigurationService configService) { LOG.info(MessageFormat.format("bindConfigurationService({0})", configService)); //$NON-NLS-1$ this.configService = configService; synchronizeAllTasks(); } protected void unbindConfigurationService(ConfigurationService configService) { LOG.info(MessageFormat.format("unbindConfigurationService({0})", configService)); //$NON-NLS-1$ this.configService = null; synchronizeAllTasks(); } protected void bindEventService(EventService eventService) { LOG.info(MessageFormat.format("bindEventService({0})", eventService)); //$NON-NLS-1$ eventService.registerListener(EventConfigUpdate.class, new ConfigUpdateListener()); eventService.registerListener(EventEntityUpdate.class, new EntityUpdateListener()); } protected void unbindEventService(EventService eventService) { LOG.info(MessageFormat.format("unbindEventService({0})", eventService)); //$NON-NLS-1$ } @Override public synchronized <T extends EntityBase> void queue(Validation<T> validation) { if (validation.getPriority() < 0) { validateImmediately(validation); return; } Map<Validation<T>, Validation<T>> validations = CollectionUtils.asMap(validation, validation); queueAll(validations); } @Override public synchronized <T extends EntityBase> void queueAll(Class<T> entityClass, Severity minSeverity, String userId) { EntityService<T> entityService = EntityServices.getByEntityClass(entityClass); if (entityService != null) { Map<Validation<T>, Validation<T>> validations = new HashMap<Validation<T>, Validation<T>>(); List<T> enitites = entityService.getAll(); for (T entity : enitites) { Validation<T> validation = new Validation<T>(entityClass, entity.getUuid(), minSeverity, userId); validations.put(validation, validation); } queueAll(validations); } } private <T extends EntityBase> void queueAll(Map<Validation<T>, Validation<T>> newEntries) { // first, remove all entries that already are scheduled from the queue... Iterator<QueuedEntity<?>> oldEntries = queuedEntities.iterator(); while (oldEntries.hasNext()) { Validation<?> oldEntry = oldEntries.next(); Validation<T> newEntry = newEntries.get(oldEntry); if (newEntry != null) { oldEntries.remove(); // relaxing severity is ok (e.g. from FATAL to WARNING), but not vice versa; // otherwise we would not get issues that the previous caller has requested if (oldEntry.getMinSeverity().compareTo(newEntry.getMinSeverity()) > 0) { newEntry.setMinSeverity(oldEntry.getMinSeverity()); LOG.info(MessageFormat.format("{0}: updated severity in queue", newEntry)); } } } // ...then schedule the new entries for (Validation<T> newEntry : newEntries.keySet()) { if (!offerQueueEntry(newEntry)) { // should not happen since we use a queue without bounds, but in case... LOG.warn(MessageFormat.format("Failed to schedule entity {0} for validation", newEntry.getEntityId())); } LOG.info(MessageFormat.format("{0}: queued", newEntry)); } // ...and mark existing issues of the entity as stale markIssuesAsStale(newEntries); } private <T extends EntityBase> void validateImmediately(Validation<T> validation) { if (schedulerService != null) { schedulerService.registerTask(new Task( new ImmediateValidator<T>(new QueuedEntity<T>(validation), DEFAULT_SEVERITY))); } } @Override public synchronized <T extends EntityBase> boolean isQueued(T entity) { Class<?> entityClass = entity.getClass(); UUID entityId = entity.getUuid(); for (Validation<?> queuedEntity : queuedEntities) { if (entityClass.equals(queuedEntity.getEntityClass()) && entityId.equals(queuedEntity.getEntityId())) { return true; } } return false; } @Override public <T extends EntityBase> void validate(Class<T> entityClass, UUID entityId, Severity minSeverity, String userId) { EntityService<T> entityService = EntityServices.getByEntityClass(entityClass); if (entityService != null) { T entity = entityService.getByUUID(entityId); validateAndPersist(entityService, entity, minSeverity, userId); } } @Override public <T extends EntityBase> void validateAll(Class<T> entityClass, Severity minSeverity, String userId) { EntityService<T> entityService = EntityServices.getByEntityClass(entityClass); if (entityService != null) { List<T> entities = entityService.getAll(); for (T entity : entities) { validateAndPersist(entityService, entity, minSeverity, userId); } } } private <T extends EntityBase> void validateAndPersist(Validation<T> entry, Severity defaultSeverity) { EntityService<T> entityService = EntityServices.getByEntityClass(entry.getEntityClass()); if (entityService != null) { T entity = entityService.getByUUID(entry.getEntityId()); if (entity != null) { Severity minSeverity = entry.getMinSeverity(); if (minSeverity == null) { minSeverity = defaultSeverity; } validateAndPersist(entityService, entity, minSeverity, entry.getUserId()); } } } private <T extends EntityBase> void validateAndPersist(EntityService<T> entityService, T entity, Severity minSeverity, String userId) { SortedSet<Issue> issues = null; try { issues = entityService.validate(entity, minSeverity); } catch (RuntimeException e) { LOG.error(MessageFormat.format("Validation of entity {0} failed:\n{1}", entity.getUuid(), e.getMessage()), e); return; } if (issuesService != null) { try { issuesService.persist(entity.getUuid(), issues, userId); SortedSet<Issue> fatalIssues = Issues.getIssues(issues, Severity.FATAL); if (fatalIssues.size() > 0) { LOG.warn(Issue.getMessage( MessageFormat.format("Entity {0} has {1} FATAL issues", entity.getUuid(), fatalIssues.size()), fatalIssues)); } else if (LOG.isInfoEnabled()) { LOG.info(Issue.getMessage( MessageFormat.format("Entity {0}: validated ({1} issues found)", entity.getUuid(), issues.size()), issues)); } } catch (ValidationException e) { // should not happen, but in case... LOG.error(MessageFormat.format("Failed to persist issues for entity {0}:\n{1}", entity.getUuid(), e.getMessage()), e); } } } /** * Sets the "stale" flag on previously persisted issues reported for the entities specified * in the given validation entries. */ private <T extends EntityBase> void markIssuesAsStale(Map<Validation<T>, Validation<T>> validations) { if (issuesService != null) { for (Validation<T> validation : validations.keySet()) { UUID entityId = validation.getEntityId(); Issues issues = issuesService.getByUUID(entityId); if (issues == null) { issues = new Issues(entityId); } issues.setStale(true); try { issuesService.persist(issues, validation.getUserId()); } catch (ValidationException e) { // should not happen, but in case... LOG.warn(MessageFormat.format("Failed to persist validation issues for entity {0}:\n{1}", entityId, e.getMessage())); } } } } // package protected for monitoring and testing purposes Queue<QueuedEntity<? extends EntityBase>> getQueuedEntities() { return queuedEntities; } // package protected for testing purposes QueuedEntity<? extends EntityBase> pollNextQueueEntry() { return queuedEntities.poll(); } // package protected for testing purposes <T extends EntityBase> boolean offerQueueEntry(Validation<T> newEntry) { return queuedEntities.offer(new QueuedEntity<T>(newEntry)); } // package protected for testing purposes UUID getTaskIdQueueValidator() { return taskIdQueueValidator; } // package protected for testing purposes Set<UUID> getRegisteredSchedules() { return registeredSchedules; } /** * Runnable that performs the validation of a single entity * and persists the results. */ final class ImmediateValidator<T extends EntityBase> implements Runnable { private Severity defaultSeverity; QueuedEntity<T> entry; public ImmediateValidator(QueuedEntity<T> entry, Severity defaultSeverity) { this.defaultSeverity = defaultSeverity; this.entry = entry; } @Override public void run() { entry.setStartedAt(System.currentTimeMillis()); LOG.info(MessageFormat.format("{0}: started", entry)); validateAndPersist(entry, defaultSeverity); LOG.info(MessageFormat.format("{0}: done", entry)); } } /** * Runnable that validates entities queued for re-validation and persists the results. */ final class QueueValidator implements Runnable { private Severity defaultSeverity; /** * Creates a validation runnable suitable for periodic re-validation * of entities that have been modified. * * @param minSeverity default minimal severity of issues to report. This severity is * applied if no explicit severity has been specified when the entity was * {@link EntityService#scheduleForValidation(UUID, Severity) scheduled for re-validation}. */ public QueueValidator(Severity defaultSeverity) { this.defaultSeverity = defaultSeverity; } /** * Validates the next entity scheduled for re-validation. * Continues to validate entities from the queue, until the size of the * queue drops below the defined threshold ("bunch validation"). */ @Override public void run() { if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("polling next queued validation at {0}", FormatUtils.formatUTCWithMillis(System.currentTimeMillis()))); } QueuedEntity<? extends EntityBase> entry = pollNextQueueEntry(); if (entry == null) { if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("nothing to do (queuedEntities.size={0})", queuedEntities.size())); } return; } entry.setStartedAt(System.currentTimeMillis()); LOG.info(MessageFormat.format("{0}: started", entry)); validateAndPersist(entry, defaultSeverity); LOG.info(MessageFormat.format("{0}: done", entry)); } } /** * Runnable that queues all entities of a given type for validation. */ final class QueueRunnable implements Runnable { private Severity minSeverity; private String entityClassName; private String userId; /** * Creates a validation runnable suitable for periodic re-validation * of all known entites. * * @param minSeverity minimal severity of issues to report. */ public QueueRunnable(Severity minSeverity, String entityClassName, String userId) { this.minSeverity = minSeverity; this.entityClassName = entityClassName; this.userId = userId; } @Override public void run() { for (EntityService<?> entityService : EntityServices.getAll()) { Class<?> entityClass = entityService.getEntityClass(); if (entityClass.getName().equals(entityClassName) || entityClass.getSimpleName().equals(entityClassName)) { queueAll(entityService.getEntityClass(), minSeverity, userId); break; } } } }; /** * Runnable that queues all known entities for validation. */ final class QueueAllRunnable implements Runnable { private Severity minSeverity; private String userId; /** * Creates a validation runnable suitable for periodic re-validation * of all known entites. * * @param minSeverity minimal severity of issues to report. */ public QueueAllRunnable(Severity minSeverity, String userId) { this.minSeverity = minSeverity; this.userId = userId; } @Override public void run() { for (EntityService<?> entityService : EntityServices.getAll()) { Class<? extends EntityBase> entityClass = entityService.getEntityClass(); if (!Issues.class.isAssignableFrom(entityClass)) { queueAll(entityClass, minSeverity, userId); } } } }; /** * Runnable that validates all known entities of a given entity type * and persists the results. */ final class ValidateRunnable implements Runnable { private Severity minSeverity; private String entityClassName; private String userId; /** * Creates a validation runnable suitable for periodic re-validation * of all known entites. * * @param minSeverity minimal severity of issues to report. */ public ValidateRunnable(Severity minSeverity, String entityClassName, String userId) { this.minSeverity = minSeverity; this.entityClassName = entityClassName; this.userId = userId; } @Override public void run() { for (EntityService<?> entityService : EntityServices.getAll()) { Class<?> entityClass = entityService.getEntityClass(); if (entityClass.getName().equals(entityClassName) || entityClass.getSimpleName().equals(entityClassName)) { validateAll(entityService.getEntityClass(), minSeverity, userId); break; } } } }; /** * Runnable that validates all known entities and persists the results. */ final class ValidateAllRunnable implements Runnable { private Severity minSeverity; private String userId; /** * Creates a validation runnable suitable for periodic re-validation * of all known entites. * * @param minSeverity minimal severity of issues to report. */ public ValidateAllRunnable(Severity minSeverity, String userId) { this.minSeverity = minSeverity; this.userId = userId; } @Override public void run() { for (EntityService<?> entityService : EntityServices.getAll()) { validateAll(entityService.getEntityClass(), minSeverity, userId); } } }; /** * <code>RunnableSchedule</code> wrapper for a validation configuration. */ final class ValidationSchedule extends RunnableSchedule { private ValidationConfig config; public ValidationSchedule(ValidationConfig config) { super(config.getSchedule(), "Validation"); this.config = config; } @Override public void run() { try { setLastStarted(System.currentTimeMillis()); LOG.info("Validating entities..."); Runnable runnable = getRunnableFromConfig(config); if (runnable == null) { LOG.error("No runnable available for schedule " + config); return; } runnable.run(); LOG.info("Validating entities: Finished"); } catch (Exception e) { LOG.error("Validating entities: Failed", e); } finally { setLastCompleted(System.currentTimeMillis()); } } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()); sb.append(" config='"); sb.append(config.toString()); sb.append("'"); return sb.toString(); } } final class ConfigUpdateListener implements EventListener<EventConfigUpdate> { @Override public void onEvent(EventConfigUpdate event) { if (event.getConfigClass().equals(ValidationsConfig.class)) { synchronizeAllTasks(); } } } final class EntityUpdateListener implements EventListener<EventEntityUpdate> { @Override public void onEvent(EventEntityUpdate event) { if (event.getEntityClass().equals(Project.class)) { queue(new Validation<Project>(Project.class, event.getEntityId(), Severity.INFO, event.getUserId(), Validation.IMMEDIATE)); } } } Runnable getRunnableFromConfig(ValidationConfig config) { ValidationAction action = config.getAction(); Severity minSeverity = config.getMinSeverity(); String userId = config.getUserId(); if (StringUtils.isBlank(userId)) { userId = DEFAULT_USER; } String entityType = config.getEntityType(); if (StringUtils.isBlank(entityType)) { switch (action) { case QUEUE: case VALIDATE: LOG.warn(MessageFormat.format( "Ignoring invalid schedule entry ''{0}'': entity type required for action {1}", toString(), action)); return null; default: break; } } switch (action) { case QUEUE: return new QueueRunnable(minSeverity, entityType, userId); case QUEUE_ALL: return new QueueAllRunnable(minSeverity, userId); case VALIDATE: return new ValidateRunnable(minSeverity, entityType, userId); case VALIDATE_ALL: return new ValidateAllRunnable(minSeverity, userId); } return null; } synchronized void startAllTasks() { if (schedulerService != null) { if (taskIdQueueValidator != null || registeredSchedules.size() > 0) { stopAllTasks(); } if (configService != null) { ValidationsConfig validationConfigs = configService.readConfiguration(ValidationsConfig.class); if (validationConfigs != null) { for (ValidationConfig validationConfig : validationConfigs.getValidationConfigs()) { ValidationSchedule schedule = new ValidationSchedule(validationConfig); UUID scheduleId = schedulerService.registerSchedule(schedule); registeredSchedules.add(scheduleId); LOG.info(MessageFormat.format("Custom schedule {0}: registered", schedule)); //$NON-NLS-1$ } } } startDefaultQueueTask(); } } // register default task for the queue validation private void startDefaultQueueTask() { Task task = new Task( new QueueValidator(DEFAULT_SEVERITY), DEFAULT_QUEUED_INITIAL_DELAY, DEFAULT_QUEUED_PERIOD); taskIdQueueValidator = schedulerService.registerTask(task); LOG.info(MessageFormat.format("Default queue task {0}: registered (id={1})", task, taskIdQueueValidator)); //$NON-NLS-1$ } synchronized void stopAllTasks() { if (schedulerService != null) { for (UUID key : registeredSchedules) { schedulerService.unregisterSchedule(key); } if (taskIdQueueValidator != null) { schedulerService.unregisterTask(taskIdQueueValidator); } } registeredSchedules.clear(); taskIdQueueValidator = null; } void synchronizeAllTasks() { stopAllTasks(); startAllTasks(); } // interface Monitorable static final String SERVICE_COMPONENT_NAME = "org.eclipse.skalli.core.validation"; //$NON-NLS-1$ @Override public String getServiceComponentName() { return SERVICE_COMPONENT_NAME; } @Override public Set<String> getResourceNames() { return CollectionUtils.asSet(QueueMonitorResource.RESOURCE_NAME); } @Override public Class<? extends ServerResource> getServerResource(String resourceName) { if (QueueMonitorResource.RESOURCE_NAME.equals(resourceName)) { return QueueMonitorResource.class; } return null; } }