com.evolveum.midpoint.model.impl.trigger.TriggerScannerTaskHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.midpoint.model.impl.trigger.TriggerScannerTaskHandler.java

Source

/*
 * Copyright (c) 2010-2013 Evolveum
 *
 * 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 com.evolveum.midpoint.model.impl.trigger;

import com.evolveum.midpoint.model.api.ModelPublicConstants;
import com.evolveum.midpoint.model.impl.util.AbstractScannerResultHandler;
import com.evolveum.midpoint.model.impl.util.AbstractScannerTaskHandler;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismContainerDefinition;
import com.evolveum.midpoint.prism.PrismContainerValue;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.delta.ChangeType;
import com.evolveum.midpoint.prism.delta.ContainerDelta;
import com.evolveum.midpoint.prism.delta.ItemDelta;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.query.ObjectFilter;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.result.OperationConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.task.api.TaskRunResult;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.TriggerType;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.XMLGregorianCalendar;
import java.util.*;

import static com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType.F_TRIGGER;
import static com.evolveum.midpoint.xml.ns._public.common.common_3.TriggerType.F_TIMESTAMP;

/**
 *
 * @author Radovan Semancik
 *
 */
@Component
public class TriggerScannerTaskHandler
        extends AbstractScannerTaskHandler<ObjectType, AbstractScannerResultHandler<ObjectType>> {

    // WARNING! This task handler is efficiently singleton!
    // It is a spring bean and it is supposed to handle all search task instances
    // Therefore it must not have task-specific fields. It can only contain fields specific to
    // all tasks of a specified type

    public static final String HANDLER_URI = ModelPublicConstants.TRIGGER_SCANNER_TASK_HANDLER_URI;

    private static final transient Trace LOGGER = TraceManager.getTrace(TriggerScannerTaskHandler.class);

    @Autowired
    private TriggerHandlerRegistry triggerHandlerRegistry;

    public TriggerScannerTaskHandler() {
        super(ObjectType.class, "Trigger scan", OperationConstants.TRIGGER_SCAN);
    }

    // task OID -> handlerUri -> OID+TriggerID; cleared on task start
    // we use plain map, as it is much easier to synchronize explicitly than to play with ConcurrentMap methods
    private Map<String, Map<String, Set<String>>> processedTriggersMap = new HashMap<>();

    private synchronized void initProcessedTriggers(Task coordinatorTask) {
        Validate.notNull(coordinatorTask.getOid(), "Task OID is null");
        processedTriggersMap.put(coordinatorTask.getOid(), new HashMap<>());
    }

    // TODO fix possible (although very small) memory leak occurring when task finishes unsuccessfully
    private synchronized void cleanupProcessedOids(Task coordinatorTask) {
        Validate.notNull(coordinatorTask.getOid(), "Task OID is null");
        processedTriggersMap.remove(coordinatorTask.getOid());
    }

    private synchronized boolean triggerAlreadySeen(Task coordinatorTask, String handlerUri,
            String oidPlusTriggerId) {
        Validate.notNull(coordinatorTask.getOid(), "Coordinator task OID is null");
        Map<String, Set<String>> taskTriggersMap = processedTriggersMap.get(coordinatorTask.getOid());
        if (taskTriggersMap == null) {
            throw new IllegalStateException(
                    "ProcessedTriggers map was not initialized for task = " + coordinatorTask);
        }
        Set<String> processedTriggers = taskTriggersMap.get(handlerUri);
        if (processedTriggers != null) {
            return !processedTriggers.add(oidPlusTriggerId);
        } else {
            processedTriggers = new HashSet<>();
            processedTriggers.add(oidPlusTriggerId);
            taskTriggersMap.put(handlerUri, processedTriggers);
            return false;
        }
    }

    @PostConstruct
    private void initialize() {
        taskManager.registerHandler(HANDLER_URI, this);
    }

    @Override
    protected Class<? extends ObjectType> getType(Task task) {
        return ObjectType.class; // TODO - is this ok???
    }

    @Override
    protected ObjectQuery createQuery(AbstractScannerResultHandler<ObjectType> handler, TaskRunResult runResult,
            Task task, OperationResult opResult) throws SchemaException {

        initProcessedTriggers(task);

        ObjectQuery query = new ObjectQuery();
        ObjectFilter filter;

        if (handler.getLastScanTimestamp() == null) {
            filter = QueryBuilder.queryFor(ObjectType.class, prismContext).item(F_TRIGGER, F_TIMESTAMP)
                    .le(handler.getThisScanTimestamp()).buildFilter();
        } else {
            filter = QueryBuilder.queryFor(ObjectType.class, prismContext).exists(F_TRIGGER).block()
                    .item(F_TIMESTAMP).gt(handler.getLastScanTimestamp()).and().item(F_TIMESTAMP)
                    .le(handler.getThisScanTimestamp()).endBlock().buildFilter();
        }

        query.setFilter(filter);
        return query;
    }

    @Override
    protected void finish(AbstractScannerResultHandler<ObjectType> handler, TaskRunResult runResult, Task task,
            OperationResult opResult) throws SchemaException {
        super.finish(handler, runResult, task, opResult);
        cleanupProcessedOids(task);
    }

    @Override
    protected AbstractScannerResultHandler<ObjectType> createHandler(TaskRunResult runResult,
            final Task coordinatorTask, OperationResult opResult) {

        AbstractScannerResultHandler<ObjectType> handler = new AbstractScannerResultHandler<ObjectType>(
                coordinatorTask, TriggerScannerTaskHandler.class.getName(), "trigger", "trigger task",
                taskManager) {
            @Override
            protected boolean handleObject(PrismObject<ObjectType> object, Task workerTask, OperationResult result)
                    throws CommonException {
                fireTriggers(this, object, workerTask, coordinatorTask, result);
                return true;
            }
        };
        handler.setStopOnError(false);
        return handler;
    }

    private void fireTriggers(AbstractScannerResultHandler<ObjectType> handler, PrismObject<ObjectType> object,
            Task workerTask, Task coordinatorTask, OperationResult result) {
        PrismContainer<TriggerType> triggerContainer = object.findContainer(F_TRIGGER);
        if (triggerContainer == null) {
            LOGGER.warn("Strange thing, attempt to fire triggers on {}, but it does not have trigger container",
                    object);
        } else {
            List<PrismContainerValue<TriggerType>> triggerCVals = triggerContainer.getValues();
            if (triggerCVals.isEmpty()) {
                LOGGER.warn(
                        "Strange thing, attempt to fire triggers on {}, but it does not have any triggers in trigger container",
                        object);
            } else {
                LOGGER.trace("Firing triggers for {} ({} triggers)", object, triggerCVals.size());
                List<TriggerType> triggers = getSortedTriggers(triggerCVals);
                for (TriggerType trigger : triggers) {
                    XMLGregorianCalendar timestamp = trigger.getTimestamp();
                    if (timestamp == null) {
                        LOGGER.warn("Trigger without a timestamp in {}", object);
                    } else {
                        if (isHot(handler, timestamp)) {
                            fireTrigger(trigger, object, triggerContainer.getDefinition(), workerTask,
                                    coordinatorTask, result);
                        } else {
                            LOGGER.trace(
                                    "Trigger {} is not hot (timestamp={}, thisScanTimestamp={}, lastScanTimestamp={})",
                                    trigger, timestamp, handler.getThisScanTimestamp(),
                                    handler.getLastScanTimestamp());
                        }
                    }
                }
            }
        }
    }

    private List<TriggerType> getSortedTriggers(List<PrismContainerValue<TriggerType>> triggerCVals) {
        List<TriggerType> rv = new ArrayList<>();
        triggerCVals.forEach(cval -> rv.add(cval.clone().asContainerable()));
        rv.sort(Comparator.comparingLong(t -> XmlTypeConverter.toMillis(t.getTimestamp())));
        return rv;
    }

    /**
     * Returns true if the timestamp is in the "range of interest" for this scan run.
     */
    private boolean isHot(AbstractScannerResultHandler<ObjectType> handler, XMLGregorianCalendar timestamp) {
        if (handler.getThisScanTimestamp().compare(timestamp) == DatatypeConstants.LESSER) {
            return false;
        }
        return handler.getLastScanTimestamp() == null
                || handler.getLastScanTimestamp().compare(timestamp) == DatatypeConstants.LESSER;
    }

    private void fireTrigger(TriggerType trigger, PrismObject<ObjectType> object,
            PrismContainerDefinition<TriggerType> triggerContainerDefinition, Task workerTask, Task coordinatorTask,
            OperationResult result) {
        String handlerUri = trigger.getHandlerUri();
        if (handlerUri == null) {
            LOGGER.warn("Trigger without handler URI in {}", object);
            return;
        }
        LOGGER.debug("Firing trigger {} in {}: id={}", handlerUri, object, trigger.getId());
        if (triggerAlreadySeen(coordinatorTask, handlerUri, object.getOid() + ":" + trigger.getId())) {
            LOGGER.debug("Handler {} already executed for {}:{}", handlerUri, ObjectTypeUtil.toShortString(object),
                    trigger.getId());
            return;
        }
        TriggerHandler handler = triggerHandlerRegistry.getHandler(handlerUri);
        if (handler == null) {
            LOGGER.warn("No registered trigger handler for URI {} in {}", handlerUri, trigger);
        } else {
            try {
                handler.handle(object, trigger, workerTask, result);
                // Properly handle everything that the handler spits out. We do not want this task to die.
                // Looks like the impossible happens and checked exceptions can somehow get here. Hence the heavy artillery below.
            } catch (Throwable e) {
                LOGGER.error("Trigger handler {} executed on {} thrown an error: {}", handler, object,
                        e.getMessage(), e);
                result.recordPartialError(e);
            }
        }
        removeTrigger(object, trigger.asPrismContainerValue(), workerTask, triggerContainerDefinition);
    }

    private void removeTrigger(PrismObject<ObjectType> object, PrismContainerValue<TriggerType> triggerCVal,
            Task task, PrismContainerDefinition<TriggerType> triggerContainerDef) {
        ContainerDelta<TriggerType> triggerDelta = triggerContainerDef.createEmptyDelta(new ItemPath(F_TRIGGER));
        triggerDelta.addValuesToDelete(triggerCVal.clone());
        Collection<? extends ItemDelta> modifications = MiscSchemaUtil.createCollection(triggerDelta);
        // This is detached result. It will not take part of the task result. We do not really care.
        OperationResult result = new OperationResult(TriggerScannerTaskHandler.class.getName() + ".removeTrigger");
        try {
            repositoryService.modifyObject(object.getCompileTimeClass(), object.getOid(), modifications, result);
            result.computeStatus();
            task.recordObjectActionExecuted(object, ChangeType.MODIFY, null);
        } catch (ObjectNotFoundException e) {
            // Object is gone. Ergo there are no triggers left. Ergo the trigger was removed.
            // Ergo this is not really an error.
            task.recordObjectActionExecuted(object, ChangeType.MODIFY, e);
            LOGGER.trace("Unable to remove trigger from {}: {} (but this is probably OK)", object, e.getMessage(),
                    e);
        } catch (SchemaException | ObjectAlreadyExistsException e) {
            task.recordObjectActionExecuted(object, ChangeType.MODIFY, e);
            LOGGER.error("Unable to remove trigger from {}: {}", object, e.getMessage(), e);
        } catch (Throwable t) {
            task.recordObjectActionExecuted(object, ChangeType.MODIFY, t);
            throw t;
        } finally {
            task.markObjectActionExecutedBoundary(); // maybe OK (absolute correctness is not quite important here)
        }
    }

}