/*******************************************************************************
* 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;
}
}