com.evolveum.midpoint.schema.result.OperationResult.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.midpoint.schema.result.OperationResult.java

Source

/*
 * Copyright (c) 2010-2015 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.schema.result;

import java.io.BufferedReader;
import java.io.Serializable;
import java.io.StringReader;
import java.util.*;
import java.util.Map.Entry;

import com.evolveum.midpoint.prism.util.CloneUtil;

import com.evolveum.midpoint.util.DebugUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.w3c.dom.Element;

import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.util.ParamsTypeUtil;
import com.evolveum.midpoint.schema.util.SchemaDebugUtil;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.DebugDumpable;
import com.evolveum.midpoint.util.MiscUtil;
import com.evolveum.midpoint.util.exception.CommonException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.LocalizedMessageType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.OperationResultType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ParamsType;

/**
 * Nested Operation Result.
 * 
 * This class provides information for better error handling in complex
 * operations. It contains a status (success, failure, warning, ...) and an
 * error message. It also contains a set of sub-results - results on inner
 * operations.
 * 
 * This object can be used by GUI to display smart (and interactive) error
 * information. It can also be used by the client code to detect deeper problems
 * in the invocations, retry or otherwise compensate for the errors or decide
 * how severe the error was and it is possible to proceed.
 * 
 * @author lazyman
 * @author Radovan Semancik
 * 
 */
public class OperationResult implements Serializable, DebugDumpable, Cloneable {

    private static final long serialVersionUID = -2467406395542291044L;
    private static final String INDENT_STRING = "    ";

    /**
     * This constant provides count threshold for same subresults (same operation and
     * status) during summarize operation.
     */
    private static final int SUBRESULT_STRIP_THRESHOLD = 10;

    public static final String CONTEXT_IMPLEMENTATION_CLASS = "implementationClass";
    public static final String CONTEXT_PROGRESS = "progress";
    public static final String CONTEXT_OID = "oid";
    public static final String CONTEXT_OBJECT = "object";
    public static final String CONTEXT_ITEM = "item";
    public static final String CONTEXT_TASK = "task";

    public static final String PARAM_OID = "oid";
    public static final String PARAM_TYPE = "type";
    public static final String PARAM_OPTIONS = "options";
    public static final String PARAM_TASK = "task";
    public static final String PARAM_OBJECT = "object";
    public static final String PARAM_QUERY = "query";

    public static final String RETURN_COUNT = "count";

    private static long TOKEN_COUNT = 1000000000000000000L;
    private String operation;
    private OperationResultStatus status;
    private Map<String, Serializable> params;
    private Map<String, Serializable> context;
    private Map<String, Serializable> returns;
    private long token;
    private String messageCode;
    private String message;
    private String localizationMessage;
    private List<Serializable> localizationArguments;
    private Throwable cause;
    private int count = 1;
    private List<OperationResult> subresults;
    private List<String> details;
    private boolean summarizeErrors;
    private boolean summarizePartialErrors;
    private boolean summarizeSuccesses;
    private boolean minor = false;

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

    public OperationResult(String operation) {
        this(operation, null, OperationResultStatus.UNKNOWN, 0, null, null, null, null, null);
    }

    public OperationResult(String operation, String messageCode, String message) {
        this(operation, null, OperationResultStatus.SUCCESS, 0, messageCode, message, null, null, null);
    }

    public OperationResult(String operation, long token, String messageCode, String message) {
        this(operation, null, OperationResultStatus.SUCCESS, token, messageCode, message, null, null, null);
    }

    public OperationResult(String operation, OperationResultStatus status, String message) {
        this(operation, null, status, 0, null, message, null, null, null);
    }

    public OperationResult(String operation, OperationResultStatus status, String messageCode, String message) {
        this(operation, null, status, 0, messageCode, message, null, null, null);
    }

    public OperationResult(String operation, OperationResultStatus status, long token, String messageCode,
            String message) {
        this(operation, null, status, token, messageCode, message, null, null, null);
    }

    public OperationResult(String operation, OperationResultStatus status, long token, String messageCode,
            String message, Throwable cause) {
        this(operation, null, status, token, messageCode, message, null, cause, null);
    }

    public OperationResult(String operation, Map<String, Serializable> params, OperationResultStatus status,
            long token, String messageCode, String message) {
        this(operation, params, status, token, messageCode, message, null, null, null);
    }

    public OperationResult(String operation, Map<String, Serializable> params, OperationResultStatus status,
            long token, String messageCode, String message, List<OperationResult> subresults) {
        this(operation, params, status, token, messageCode, message, null, null, subresults);
    }

    public OperationResult(String operation, Map<String, Serializable> params, OperationResultStatus status,
            long token, String messageCode, String message, String localizationMessage, Throwable cause,
            List<OperationResult> subresults) {
        this(operation, params, status, token, messageCode, message, localizationMessage, null, cause, subresults);
    }

    public OperationResult(String operation, Map<String, Serializable> params, OperationResultStatus status,
            long token, String messageCode, String message, String localizationMessage,
            List<Serializable> localizationArguments, Throwable cause, List<OperationResult> subresults) {
        this(operation, params, null, null, status, token, messageCode, message, localizationMessage, null, cause,
                subresults);
    }

    public OperationResult(String operation, Map<String, Serializable> params, Map<String, Serializable> context,
            Map<String, Serializable> returns, OperationResultStatus status, long token, String messageCode,
            String message, String localizationMessage, List<Serializable> localizationArguments, Throwable cause,
            List<OperationResult> subresults) {
        if (StringUtils.isEmpty(operation)) {
            throw new IllegalArgumentException("Operation argument must not be null or empty.");
        }
        if (status == null) {
            throw new IllegalArgumentException("Operation status must not be null.");
        }
        this.operation = operation;
        this.params = params;
        this.context = context;
        this.returns = returns;
        this.status = status;
        this.token = token;
        this.messageCode = messageCode;
        this.message = message;
        this.localizationMessage = localizationMessage;
        this.localizationArguments = localizationArguments;
        this.cause = cause;
        this.subresults = subresults;
        this.details = new ArrayList<String>();
    }

    public OperationResult createSubresult(String operation) {
        OperationResult subresult = new OperationResult(operation);
        addSubresult(subresult);
        return subresult;
    }

    public OperationResult createMinorSubresult(String operation) {
        OperationResult subresult = createSubresult(operation);
        subresult.minor = true;
        return subresult;
    }

    /**
     * Contains operation name. Operation name must be defined as {@link String}
     * constant in module interface with description and possible parameters. It
     * can be used for further processing. It will be used as key for
     * translation in admin-gui.
     * 
     * @return always return non null, non empty string
     */
    public String getOperation() {
        return operation;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public void incrementCount() {
        this.count++;
    }

    public boolean isSummarizeErrors() {
        return summarizeErrors;
    }

    public void setSummarizeErrors(boolean summarizeErrors) {
        this.summarizeErrors = summarizeErrors;
    }

    public boolean isSummarizePartialErrors() {
        return summarizePartialErrors;
    }

    public void setSummarizePartialErrors(boolean summarizePartialErrors) {
        this.summarizePartialErrors = summarizePartialErrors;
    }

    public boolean isSummarizeSuccesses() {
        return summarizeSuccesses;
    }

    public void setSummarizeSuccesses(boolean summarizeSuccesses) {
        this.summarizeSuccesses = summarizeSuccesses;
    }

    public boolean isEmpty() {
        return (status == null || status == OperationResultStatus.UNKNOWN)
                && (subresults == null || subresults.isEmpty());
    }

    /**
     * Method returns list of operation subresults @{link
     * {@link OperationResult}.
     * 
     * @return never returns null
     */
    public List<OperationResult> getSubresults() {
        if (subresults == null) {
            subresults = new ArrayList<OperationResult>();
        }
        return subresults;
    }

    /**
     * @return last subresult, or null if there are no subresults.
     */
    public OperationResult getLastSubresult() {
        if (subresults == null || subresults.isEmpty()) {
            return null;
        } else {
            return subresults.get(subresults.size() - 1);
        }
    }

    public void removeLastSubresult() {
        if (subresults != null && !subresults.isEmpty()) {
            subresults.remove(subresults.size() - 1);
        }
    }

    /**
     * @return last subresult status, or null if there are no subresults.
     */
    public OperationResultStatus getLastSubresultStatus() {
        OperationResult last = getLastSubresult();
        return last != null ? last.getStatus() : null;
    }

    public void addSubresult(OperationResult subresult) {
        getSubresults().add(subresult);
    }

    public OperationResult findSubresult(String operation) {
        if (subresults == null) {
            return null;
        }
        for (OperationResult subResult : getSubresults()) {
            if (operation.equals(subResult.getOperation())) {
                return subResult;
            }
        }
        return null;
    }

    public List<OperationResult> findSubresults(String operation) {
        List<OperationResult> found = new ArrayList<>();
        if (subresults == null) {
            return found;
        }
        for (OperationResult subResult : getSubresults()) {
            if (operation.equals(subResult.getOperation())) {
                found.add(subResult);
            }
        }
        return found;
    }

    /**
     * Contains operation status as defined in {@link OperationResultStatus}
     * 
     * @return never returns null
     */
    public OperationResultStatus getStatus() {
        return status;
    }

    public void setStatus(OperationResultStatus status) {
        this.status = status;
    }

    /**
     * Returns true if the result is success.
     * 
     * This returns true if the result is absolute success. Presence of partial
     * failures or warnings fail this test.
     * 
     * @return true if the result is success.
     */
    public boolean isSuccess() {
        return (status == OperationResultStatus.SUCCESS);
    }

    public boolean isWarning() {
        return status == OperationResultStatus.WARNING;
    }

    /**
     * Returns true if the result is acceptable for further processing.
     * 
     * In other words: if there were no fatal errors. Warnings and partial
     * errors are acceptable. Yet, this test also fails if the operation state
     * is not known.
     * 
     * @return true if the result is acceptable for further processing.
     */
    public boolean isAcceptable() {
        return (status != OperationResultStatus.FATAL_ERROR);
    }

    public boolean isUnknown() {
        return (status == OperationResultStatus.UNKNOWN);
    }

    public boolean isInProgress() {
        return (status == OperationResultStatus.IN_PROGRESS);
    }

    public boolean isError() {
        return (status == OperationResultStatus.FATAL_ERROR) || (status == OperationResultStatus.PARTIAL_ERROR);
    }

    public boolean isFatalError() {
        return (status == OperationResultStatus.FATAL_ERROR);
    }

    public boolean isPartialError() {
        return (status == OperationResultStatus.PARTIAL_ERROR);
    }

    public boolean isHandledError() {
        return (status == OperationResultStatus.HANDLED_ERROR);
    }

    public boolean isNotApplicable() {
        return (status == OperationResultStatus.NOT_APPLICABLE);
    }

    /**
     * Set all error status in this result and all subresults as handled.
     */
    public void setErrorsHandled() {
        if (isError()) {
            setStatus(OperationResultStatus.HANDLED_ERROR);
        }
        for (OperationResult subresult : getSubresults()) {
            subresult.setErrorsHandled();
        }
    }

    /**
     * Computes operation result status based on subtask status and sets an
     * error message if the status is FATAL_ERROR.
     * 
     * @param errorMessage
     *            error message
     */
    public void computeStatus(String errorMessage) {
        computeStatus(errorMessage, errorMessage);
    }

    public void computeStatus(String errorMessage, String warnMessage) {
        Validate.notEmpty(errorMessage, "Error message must not be null.");

        // computeStatus sets a message if none is set,
        // therefore we need to check before calling computeStatus
        boolean noMessage = StringUtils.isEmpty(message);
        computeStatus();

        switch (status) {
        case FATAL_ERROR:
        case PARTIAL_ERROR:
            if (noMessage) {
                message = errorMessage;
            }
            break;
        case UNKNOWN:
        case WARNING:
        case NOT_APPLICABLE:
            if (noMessage) {
                if (StringUtils.isNotEmpty(warnMessage)) {
                    message = warnMessage;
                } else {
                    message = errorMessage;
                }
            }
            break;
        }
    }

    /**
     * Computes operation result status based on subtask status.
     */
    public void computeStatus() {
        if (getSubresults().isEmpty()) {
            if (status == OperationResultStatus.UNKNOWN) {
                status = OperationResultStatus.SUCCESS;
            }
            return;
        }
        if (status == OperationResultStatus.FATAL_ERROR) {
            return;
        }
        OperationResultStatus newStatus = OperationResultStatus.UNKNOWN;
        boolean allSuccess = true;
        boolean allNotApplicable = true;
        String newMessage = null;
        for (OperationResult sub : getSubresults()) {
            if (sub.getStatus() != OperationResultStatus.NOT_APPLICABLE) {
                allNotApplicable = false;
            }
            if (sub.getStatus() == OperationResultStatus.FATAL_ERROR) {
                status = OperationResultStatus.FATAL_ERROR;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ": " + sub.getMessage();
                }
                return;
            }
            if (sub.getStatus() == OperationResultStatus.IN_PROGRESS) {
                status = OperationResultStatus.IN_PROGRESS;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ": " + sub.getMessage();
                }
                return;
            }
            if (sub.getStatus() == OperationResultStatus.PARTIAL_ERROR) {
                newStatus = OperationResultStatus.PARTIAL_ERROR;
                newMessage = sub.getMessage();
            }
            if (newStatus != OperationResultStatus.PARTIAL_ERROR) {
                if (sub.getStatus() == OperationResultStatus.HANDLED_ERROR) {
                    newStatus = OperationResultStatus.HANDLED_ERROR;
                    newMessage = sub.getMessage();
                }
            }
            if (sub.getStatus() != OperationResultStatus.SUCCESS
                    && sub.getStatus() != OperationResultStatus.NOT_APPLICABLE) {
                allSuccess = false;
            }
            if (newStatus != OperationResultStatus.HANDLED_ERROR) {
                if (sub.getStatus() == OperationResultStatus.WARNING) {
                    newStatus = OperationResultStatus.WARNING;
                    newMessage = sub.getMessage();
                }
            }
        }

        if (allNotApplicable && !getSubresults().isEmpty()) {
            status = OperationResultStatus.NOT_APPLICABLE;
        }
        if (allSuccess && !getSubresults().isEmpty()) {
            status = OperationResultStatus.SUCCESS;
        } else {
            status = newStatus;
            if (message == null) {
                message = newMessage;
            } else {
                message = message + ": " + newMessage;
            }
        }
    }

    /**
     * Used when the result contains several composite sub-result that are of equivalent meaning.
     * If all of them fail the result will be fatal error as well. If only some of them fail the
     * result will be partial error. Handled error is considered a success.
     */
    public void computeStatusComposite() {
        if (getSubresults().isEmpty()) {
            if (status == OperationResultStatus.UNKNOWN) {
                status = OperationResultStatus.NOT_APPLICABLE;
            }
            return;
        }

        boolean allFatalError = true;
        boolean allNotApplicable = true;
        boolean hasInProgress = false;
        boolean hasHandledError = false;
        boolean hasError = false;
        boolean hasWarning = false;
        for (OperationResult sub : getSubresults()) {
            if (sub.getStatus() != OperationResultStatus.NOT_APPLICABLE) {
                allNotApplicable = false;
            }
            if (sub.getStatus() != OperationResultStatus.FATAL_ERROR) {
                allFatalError = false;
            }
            if (sub.getStatus() == OperationResultStatus.FATAL_ERROR) {
                hasError = true;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ", " + sub.getMessage();
                }
            }
            if (sub.getStatus() == OperationResultStatus.PARTIAL_ERROR) {
                hasError = true;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ", " + sub.getMessage();
                }
            }
            if (sub.getStatus() == OperationResultStatus.HANDLED_ERROR) {
                hasHandledError = true;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ", " + sub.getMessage();
                }
            }
            if (sub.getStatus() == OperationResultStatus.IN_PROGRESS) {
                hasInProgress = true;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ", " + sub.getMessage();
                }
            }
            if (sub.getStatus() == OperationResultStatus.WARNING) {
                hasWarning = true;
                if (message == null) {
                    message = sub.getMessage();
                } else {
                    message = message + ", " + sub.getMessage();
                }
            }
        }

        if (allNotApplicable) {
            status = OperationResultStatus.NOT_APPLICABLE;
        } else if (allFatalError) {
            status = OperationResultStatus.FATAL_ERROR;
        } else if (hasInProgress) {
            status = OperationResultStatus.IN_PROGRESS;
        } else if (hasError) {
            status = OperationResultStatus.PARTIAL_ERROR;
        } else if (hasWarning) {
            status = OperationResultStatus.WARNING;
        } else if (hasHandledError) {
            status = OperationResultStatus.HANDLED_ERROR;
        } else {
            status = OperationResultStatus.SUCCESS;
        }
    }

    public OperationResultStatus getComputeStatus() {
        OperationResultStatus origStatus = status;
        String origMessage = message;
        computeStatus();
        OperationResultStatus computedStatus = status;
        status = origStatus;
        message = origMessage;
        return computedStatus;
    }

    public void computeStatusIfUnknown() {
        if (isUnknown()) {
            computeStatus();
        }
    }

    public void recomputeStatus() {
        // Only recompute if there are subresults, otherwise keep original
        // status
        if (subresults != null && !subresults.isEmpty()) {
            computeStatus();
        }
    }

    public void recomputeStatus(String message) {
        // Only recompute if there are subresults, otherwise keep original
        // status
        if (subresults != null && !subresults.isEmpty()) {
            computeStatus(message);
        }
    }

    public void recomputeStatus(String errorMessage, String warningMessage) {
        // Only recompute if there are subresults, otherwise keep original
        // status
        if (subresults != null && !subresults.isEmpty()) {
            computeStatus(errorMessage, warningMessage);
        }
    }

    public void recordSuccessIfUnknown() {
        if (isUnknown()) {
            recordSuccess();
        }
    }

    public void recordNotApplicableIfUnknown() {
        if (isUnknown()) {
            status = OperationResultStatus.NOT_APPLICABLE;
        }
    }

    /**
     * Method returns {@link Map} with operation parameters. Parameters keys are
     * described in module interface for every operation.
     * 
     * @return never returns null
     */
    public Map<String, Serializable> getParams() {
        if (params == null) {
            params = new HashMap<String, Serializable>();
        }
        return params;
    }

    public void addParam(String paramName, Serializable paramValue) {
        getParams().put(paramName, paramValue);
    }

    public void addArbitraryObjectAsParam(String paramName, Object paramValue) {
        addParam(paramName, String.valueOf(paramValue));
    }

    // Copies a collection to a OperationResult's param field. Primarily used to overcome the fact that Collection is not Serializable
    public void addCollectionOfSerializablesAsParam(String paramName,
            Collection<? extends Serializable> paramValue) {
        addParam(paramName, paramValue != null ? new ArrayList(paramValue) : null);
    }

    public void addCollectionOfSerializablesAsReturn(String name, Collection<? extends Serializable> value) {
        addReturn(name, value != null ? new ArrayList(value) : null);
    }

    public void addArbitraryCollectionAsParam(String paramName, Collection values) {
        if (values != null) {
            ArrayList<String> valuesAsStrings = new ArrayList<String>();
            for (Object value : values) {
                valuesAsStrings.add(String.valueOf(value));
            }
            addParam(paramName, valuesAsStrings);
        } else {
            addParam(paramName, null);
        }
    }

    public void addParams(String[] names, Serializable... objects) {
        if (names.length != objects.length) {
            throw new IllegalArgumentException(
                    "Bad result parameters size, names '" + names.length + "', objects '" + objects.length + "'.");
        }

        for (int i = 0; i < names.length; i++) {
            addParam(names[i], objects[i]);
        }
    }

    public Map<String, Serializable> getContext() {
        if (context == null) {
            context = new HashMap<String, Serializable>();
        }
        return context;
    }

    @SuppressWarnings("unchecked")
    public <T> T getContext(Class<T> type, String contextName) {
        return (T) getContext().get(contextName);
    }

    public void addContext(String contextName, Serializable value) {
        getContext().put(contextName, value);
    }

    public Map<String, Serializable> getReturns() {
        if (returns == null) {
            returns = new HashMap<String, Serializable>();
        }
        return returns;
    }

    public void addReturn(String returnName, Serializable value) {
        getReturns().put(returnName, value);
    }

    public Serializable getReturn(String returnName) {
        return getReturns().get(returnName);
    }

    /**
     * @return Contains random long number, for better searching in logs.
     */
    public long getToken() {
        if (token == 0) {
            token = TOKEN_COUNT++;
        }
        return token;
    }

    /**
     * Contains mesage code based on module error catalog.
     * 
     * @return Can return null.
     */
    public String getMessageCode() {
        return messageCode;
    }

    /**
     * @return Method returns operation result message. Message is required. It
     *         will be key for translation in admin-gui.
     */
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * @return Method returns message key for translation, can be null.
     */
    public String getLocalizationMessage() {
        return localizationMessage;
    }

    /**
     * @return Method returns arguments if needed for localization, can be null.
     */
    public List<Serializable> getLocalizationArguments() {
        return localizationArguments;
    }

    /**
     * @return Method returns operation result exception. Not required, can be
     *         null.
     */
    public Throwable getCause() {
        return cause;
    }

    public void recordSuccess() {
        // Success, no message or other explanation is needed.
        status = OperationResultStatus.SUCCESS;
    }

    public void recordInProgress() {
        status = OperationResultStatus.IN_PROGRESS;
    }

    public void recordUnknown() {
        status = OperationResultStatus.UNKNOWN;
    }

    public void recordFatalError(Throwable cause) {
        recordStatus(OperationResultStatus.FATAL_ERROR, cause.getMessage(), cause);
    }

    /**
     * If the operation is an error then it will switch the status to EXPECTED_ERROR.
     * This is used if the error is expected and properly handled.
     */
    public void muteError() {
        if (isError()) {
            status = OperationResultStatus.HANDLED_ERROR;
        }
    }

    public void muteLastSubresultError() {
        OperationResult lastSubresult = getLastSubresult();
        if (lastSubresult != null) {
            lastSubresult.muteError();
        }
    }

    public void recordPartialError(Throwable cause) {
        recordStatus(OperationResultStatus.PARTIAL_ERROR, cause.getMessage(), cause);
    }

    public void recordWarning(Throwable cause) {
        recordStatus(OperationResultStatus.WARNING, cause.getMessage(), cause);
    }

    public void recordStatus(OperationResultStatus status, Throwable cause) {
        this.status = status;
        this.cause = cause;
        // No other message was given, so use message from the exception
        // not really correct, but better than nothing.
        message = cause.getMessage();
    }

    public void recordFatalError(String message, Throwable cause) {
        recordStatus(OperationResultStatus.FATAL_ERROR, message, cause);
    }

    public void recordPartialError(String message, Throwable cause) {
        recordStatus(OperationResultStatus.PARTIAL_ERROR, message, cause);
    }

    public void recordWarning(String message, Throwable cause) {
        recordStatus(OperationResultStatus.WARNING, message, cause);
    }

    public void recordHandledError(String message) {
        recordStatus(OperationResultStatus.HANDLED_ERROR, message);
    }

    public void recordHandledError(String message, Throwable cause) {
        recordStatus(OperationResultStatus.HANDLED_ERROR, message, cause);
    }

    public void recordHandledError(Throwable cause) {
        recordStatus(OperationResultStatus.HANDLED_ERROR, cause.getMessage(), cause);
    }

    public void recordStatus(OperationResultStatus status, String message, Throwable cause) {
        this.status = status;
        this.message = message;
        this.cause = cause;
    }

    public void recordFatalError(String message) {
        recordStatus(OperationResultStatus.FATAL_ERROR, message);
    }

    public void recordPartialError(String message) {
        recordStatus(OperationResultStatus.PARTIAL_ERROR, message);
    }

    public void recordWarning(String message) {
        recordStatus(OperationResultStatus.WARNING, message);
    }

    /**
     * Records result from a common exception type. This automatically
     * determines status and also sets appropriate message.
     * 
     * @param exception
     *            common exception
     */
    public void record(CommonException exception) {
        // TODO: switch to a localized message later
        // Exception is a fatal error in this context
        recordFatalError(exception.getOperationResultMessage(), exception);
    }

    public void recordStatus(OperationResultStatus status, String message) {
        this.status = status;
        this.message = message;
    }

    /**
     * Returns true if result status is UNKNOWN or any of the subresult status
     * is unknown (recursive).
     * 
     * May come handy in tests to check if all the operations fill out the
     * status as they should.
     */
    public boolean hasUnknownStatus() {
        if (status == OperationResultStatus.UNKNOWN) {
            return true;
        }
        for (OperationResult subresult : getSubresults()) {
            if (subresult.hasUnknownStatus()) {
                return true;
            }
        }
        return false;
    }

    public void appendDetail(String detailLine) {
        // May be switched to a more structured method later
        details.add(detailLine);
    }

    public List<String> getDetail() {
        return details;
    }

    @Override
    public String toString() {
        return "R(" + operation + " " + status + " " + message + ")";
    }

    public static OperationResult createOperationResult(OperationResultType result) {
        if (result == null) {
            return null;
        }

        Map<String, Serializable> params = ParamsTypeUtil.fromParamsType(result.getParams());
        //      if (result.getParams() != null) {
        //         params = new HashMap<String, Serializable>();
        //         for (EntryType entry : result.getParams().getEntry()) {
        //            params.put(entry.getKey(), (Serializable) entry.getEntryValue());
        //         }
        //      }

        Map<String, Serializable> context = ParamsTypeUtil.fromParamsType(result.getContext());
        //      if (result.getContext() != null) {
        //         context = new HashMap<String, Serializable>();
        //         for (EntryType entry : result.getContext().getEntry()) {
        //            context.put(entry.getKey(), (Serializable) entry.getEntryValue());
        //         }
        //      }

        Map<String, Serializable> returns = ParamsTypeUtil.fromParamsType(result.getReturns());
        //      if (result.getReturns() != null) {
        //         returns = new HashMap<String, Serializable>();
        //         for (EntryType entry : result.getReturns().getEntry()) {
        //            returns.put(entry.getKey(), (Serializable) entry.getEntryValue());
        //         }
        //      }

        List<OperationResult> subresults = null;
        if (!result.getPartialResults().isEmpty()) {
            subresults = new ArrayList<OperationResult>();
            for (OperationResultType subResult : result.getPartialResults()) {
                subresults.add(createOperationResult(subResult));
            }
        }

        LocalizedMessageType message = result.getLocalizedMessage();
        String localizedMessage = message == null ? null : message.getKey();
        List<Serializable> localizedArguments = message == null ? null
                : (List<Serializable>) (List) message.getArgument(); // FIXME: brutal hack

        OperationResult opResult = new OperationResult(result.getOperation(), params, context, returns,
                OperationResultStatus.parseStatusType(result.getStatus()), result.getToken(),
                result.getMessageCode(), result.getMessage(), localizedMessage, localizedArguments, null,
                subresults);
        if (result.getCount() != null) {
            opResult.setCount(result.getCount());
        }
        return opResult;
    }

    public OperationResultType createOperationResultType() {
        return createOperationResultType(this);
    }

    private OperationResultType createOperationResultType(OperationResult opResult) {
        OperationResultType result = new OperationResultType();
        result.setToken(opResult.getToken());
        result.setStatus(OperationResultStatus.createStatusType(opResult.getStatus()));
        if (opResult.getCount() != 1) {
            result.setCount(opResult.getCount());
        }
        result.setOperation(opResult.getOperation());
        result.setMessage(opResult.getMessage());
        result.setMessageCode(opResult.getMessageCode());

        if (opResult.getCause() != null || !opResult.details.isEmpty()) {
            StringBuilder detailsb = new StringBuilder();

            // Record text messages in details (if present)
            if (!opResult.details.isEmpty()) {
                for (String line : opResult.details) {
                    detailsb.append(line);
                    detailsb.append("\n");
                }
            }

            // Record stack trace in details if a cause is present
            if (opResult.getCause() != null) {
                Throwable ex = opResult.getCause();
                detailsb.append(ex.getClass().getName());
                detailsb.append(": ");
                detailsb.append(ex.getMessage());
                detailsb.append("\n");
                StackTraceElement[] stackTrace = ex.getStackTrace();
                for (int i = 0; i < stackTrace.length; i++) {
                    detailsb.append(stackTrace[i].toString());
                    detailsb.append("\n");
                }
            }

            result.setDetails(detailsb.toString());
        }

        if (StringUtils.isNotEmpty(opResult.getLocalizationMessage())) {
            LocalizedMessageType message = new LocalizedMessageType();
            message.setKey(opResult.getLocalizationMessage());
            if (opResult.getLocalizationArguments() != null) {
                message.getArgument().addAll(opResult.getLocalizationArguments());
            }
            result.setLocalizedMessage(message);
        }

        //      Set<Entry<String, Serializable>> params = opResult.getParams();
        //      if (!params.isEmpty()) {
        ParamsType paramsType = ParamsTypeUtil.toParamsType(opResult.getParams());
        result.setParams(paramsType);

        //         for (Entry<String, Serializable> entry : params) {
        //            paramsType.getEntry().add(createEntryElement(entry.getKey(),entry.getValue()));
        //         }
        //      }

        //      Set<Entry<String, Serializable>> context = opResult.getContext().entrySet();
        //      if (!context.isEmpty()) {
        paramsType = ParamsTypeUtil.toParamsType(opResult.getContext());
        result.setContext(paramsType);

        //         for (Entry<String, Serializable> entry : context) {
        //            paramsType.getEntry().add(createEntryElement(entry.getKey(),entry.getValue()));
        //         }
        //      }

        //      Set<Entry<String, Serializable>> returns = opResult.getReturns().entrySet();
        //      if (!returns.isEmpty()) {
        paramsType = ParamsTypeUtil.toParamsType(opResult.getReturns());
        result.setReturns(paramsType);

        //         for (Entry<String, Serializable> entry : returns) {
        //            paramsType.getEntry().add(createEntryElement(entry.getKey(),entry.getValue()));
        //         }
        //      }

        for (OperationResult subResult : opResult.getSubresults()) {
            result.getPartialResults().add(opResult.createOperationResultType(subResult));
        }

        return result;
    }

    //   /**
    //    * Temporary workaround, brutally hacked -- so that the conversion 
    //    * of OperationResult into OperationResultType 'somehow' works, at least to the point
    //    * where when we:
    //    * - have OR1
    //    * - serialize it into ORT1
    //    * - then deserialize into OR2
    //    * - serialize again into ORT2
    //    * so we get ORT1.equals(ORT2) - at least in our simple test case :)
    //    * 
    //    * FIXME: this should be definitely reworked
    //    * 
    //    * @param entry
    //    * @return
    //    */
    //   private EntryType createEntryElement(String key, Serializable value) {
    //      EntryType entryType = new EntryType();
    //      entryType.setKey(key);
    //      if (value != null) {
    //         Document doc = DOMUtil.getDocument();
    //         if (value instanceof ObjectType && ((ObjectType)value).getOid() != null) {
    //            // Store only reference on the OID. This is faster and getObject can be used to retrieve
    //            // the object if needed. Although is does not provide 100% accuracy, it is a good tradeoff.
    //            setObjectReferenceEntry(entryType, ((ObjectType)value));
    //         // these values should be put 'as they are', in order to be deserialized into themselves
    //         } else if (value instanceof String || value instanceof Integer || value instanceof Long) {
    //            entryType.setEntryValue(new JAXBElement<Serializable>(SchemaConstants.C_PARAM_VALUE, Serializable.class, value));
    //         } else if (XmlTypeConverter.canConvert(value.getClass())) {
    ////            try {
    ////               entryType.setEntryValue(new JXmlTypeConverter.toXsdElement(value, SchemaConstants.C_PARAM_VALUE, doc, true));
    ////            } catch (SchemaException e) {
    ////               LOGGER.error("Cannot convert value {} to XML: {}",value,e.getMessage());
    ////               setUnknownJavaObjectEntry(entryType, value);
    ////            }
    //         } else if (value instanceof Element || value instanceof JAXBElement<?>) {
    //            entryType.setEntryValue((JAXBElement<?>) value);
    //         // FIXME: this is really bad code ... it means that 'our' JAXB object should be put as is
    //         } else if ("com.evolveum.midpoint.xml.ns._public.common.common_2".equals(value.getClass().getPackage().getName())) {
    //            JAXBElement<Object> o = new JAXBElement<Object>(SchemaConstants.C_PARAM_VALUE, Object.class, value);
    //            entryType.setEntryValue(o);
    //         } else {
    //            setUnknownJavaObjectEntry(entryType, value);
    //         }
    //      }
    //      return entryType;
    //   }
    //
    //   private void setObjectReferenceEntry(EntryType entryType, ObjectType objectType) {
    //      ObjectReferenceType objRefType = new ObjectReferenceType();
    //      objRefType.setOid(objectType.getOid());
    //      ObjectTypes type = ObjectTypes.getObjectType(objectType.getClass());
    //      if (type != null) {
    //         objRefType.setType(type.getTypeQName());
    //      }
    //      JAXBElement<ObjectReferenceType> element = new JAXBElement<ObjectReferenceType>(
    //            SchemaConstants.C_OBJECT_REF, ObjectReferenceType.class, objRefType);
    ////      entryType.setAny(element);
    //   }
    //
    //   private void setUnknownJavaObjectEntry(EntryType entryType, Serializable value) {
    //      UnknownJavaObjectType ujo = new UnknownJavaObjectType();
    //      ujo.setClazz(value.getClass().getName());
    //      ujo.setToString(value.toString());
    //      entryType.setEntryValue(new ObjectFactory().createUnknownJavaObject(ujo));
    //   }

    public void summarize() {
        Iterator<OperationResult> iterator = getSubresults().iterator();
        while (iterator.hasNext()) {
            OperationResult subresult = iterator.next();
            if (subresult.getCount() > 1) {
                // Already summarized
                continue;
            }
            if (subresult.isError() && summarizeErrors) {
                // go on
            } else if (subresult.isPartialError() && summarizePartialErrors) {
                // go on
            } else if (subresult.isSuccess() && summarizeSuccesses) {
                // go on
            } else {
                continue;
            }
            OperationResult similar = findSimilarSubresult(subresult);
            if (similar == null) {
                // Nothing to summarize to
                continue;
            }
            merge(similar, subresult);
            iterator.remove();
        }

        // subresult stripping if necessary
        // we strip subresults that have same operation name and status, if there are more of them than threshold
        Map<OperationStatusKey, Integer> counter = new HashMap<OperationStatusKey, Integer>();
        iterator = getSubresults().iterator();
        while (iterator.hasNext()) {
            OperationResult sr = iterator.next();
            OperationStatusKey key = new OperationStatusKey(sr.getOperation(), sr.getStatus());
            if (counter.containsKey(key)) {
                int count = counter.get(key);
                if (count > SUBRESULT_STRIP_THRESHOLD) {
                    iterator.remove();
                } else {
                    counter.put(key, ++count);
                }
            } else {
                counter.put(key, 1);
            }
        }
    }

    private void merge(OperationResult target, OperationResult source) {
        mergeMap(target.getParams(), source.getParams());
        mergeMap(target.getContext(), source.getContext());
        mergeMap(target.getReturns(), source.getReturns());
        target.incrementCount();
    }

    private void mergeMap(Map<String, Serializable> targetMap, Map<String, Serializable> sourceMap) {
        for (Entry<String, Serializable> targetEntry : targetMap.entrySet()) {
            String targetKey = targetEntry.getKey();
            Serializable targetValue = targetEntry.getValue();
            if (targetValue != null && targetValue instanceof VariousValues) {
                continue;
            }
            Serializable sourceValue = sourceMap.get(targetKey);
            if (MiscUtil.equals(targetValue, sourceValue)) {
                // Entries match, nothing to do
                continue;
            }
            // Entries do not match. The target entry needs to be marked as VariousValues
            targetEntry.setValue(new VariousValues());
        }
        for (Entry<String, Serializable> sourceEntry : sourceMap.entrySet()) {
            String sourceKey = sourceEntry.getKey();
            if (!targetMap.containsKey(sourceKey)) {
                targetMap.put(sourceKey, new VariousValues());
            }
        }
    }

    private OperationResult findSimilarSubresult(OperationResult subresult) {
        OperationResult similar = null;
        for (OperationResult sub : getSubresults()) {
            if (sub == subresult) {
                continue;
            }
            if (!sub.operation.equals(subresult.operation)) {
                continue;
            }
            if (sub.status != subresult.status) {
                continue;
            }
            if (!MiscUtil.equals(sub.message, subresult.message)) {
                continue;
            }
            if (similar == null || (similar.count < sub.count)) {
                similar = sub;
            }
        }
        return similar;
    }

    /**
     * Removes all the successful minor results. Also checks if the result is roughly consistent
     * and complete. (e.g. does not have unknown operation status, etc.)
     */
    public void cleanupResult() {
        cleanupResult(null);
    }

    /**
     * Removes all the successful minor results. Also checks if the result is roughly consistent
     * and complete. (e.g. does not have unknown operation status, etc.)
     * 
     * The argument "e" is for easier use of the cleanup in the exceptions handlers. The original exception is passed
     * to the IAE that this method produces for easier debugging.
     */
    public void cleanupResult(Throwable e) {
        if (status == OperationResultStatus.UNKNOWN) {
            LOGGER.error("Attempt to cleanup result of operation " + operation + " that is still UNKNOWN:\n{}",
                    this.debugDump());
            throw new IllegalStateException(
                    "Attempt to cleanup result of operation " + operation + " that is still UNKNOWN");
        }
        if (subresults == null) {
            return;
        }
        Iterator<OperationResult> iterator = subresults.iterator();
        while (iterator.hasNext()) {
            OperationResult subresult = iterator.next();
            if (subresult.getStatus() == OperationResultStatus.UNKNOWN) {
                String message = "Subresult " + subresult.getOperation() + " of operation " + operation
                        + " is still UNKNOWN during cleanup";
                LOGGER.error("{}:\n{}", message, this.debugDump(), e);
                if (e == null) {
                    throw new IllegalStateException(message);
                } else {
                    throw new IllegalStateException(message + "; during handling of exception " + e, e);
                }
            }
            if (subresult.canCleanup()) {
                iterator.remove();
            }
        }
    }

    private boolean canCleanup() {
        if (!minor) {
            return false;
        }
        return status == OperationResultStatus.SUCCESS || status == OperationResultStatus.NOT_APPLICABLE;
    }

    @Override
    public String debugDump() {
        return debugDump(0);
    }

    @Override
    public String debugDump(int indent) {
        StringBuilder sb = new StringBuilder();
        dumpIndent(sb, indent, true);
        return sb.toString();
    }

    public String dump(boolean withStack) {
        StringBuilder sb = new StringBuilder();
        dumpIndent(sb, 0, withStack);
        return sb.toString();
    }

    private void dumpIndent(StringBuilder sb, int indent, boolean printStackTrace) {
        for (int i = 0; i < indent; i++) {
            sb.append(INDENT_STRING);
        }
        sb.append("*op* ");
        sb.append(operation);
        sb.append(", st: ");
        sb.append(status);
        if (minor) {
            sb.append(", MINOR");
        }
        sb.append(", msg: ");
        sb.append(message);
        if (count > 1) {
            sb.append(" x");
            sb.append(count);
        }
        sb.append("\n");

        for (Map.Entry<String, Serializable> entry : getParams().entrySet()) {
            for (int i = 0; i < indent + 2; i++) {
                sb.append(INDENT_STRING);
            }
            sb.append("[p]");
            sb.append(entry.getKey());
            sb.append("=");
            sb.append(dumpEntry(indent + 2, entry.getValue()));
            sb.append("\n");
        }

        for (Map.Entry<String, Serializable> entry : getContext().entrySet()) {
            for (int i = 0; i < indent + 2; i++) {
                sb.append(INDENT_STRING);
            }
            sb.append("[c]");
            sb.append(entry.getKey());
            sb.append("=");
            sb.append(dumpEntry(indent + 2, entry.getValue()));
            sb.append("\n");
        }

        for (Map.Entry<String, Serializable> entry : getReturns().entrySet()) {
            for (int i = 0; i < indent + 2; i++) {
                sb.append(INDENT_STRING);
            }
            sb.append("[r]");
            sb.append(entry.getKey());
            sb.append("=");
            sb.append(dumpEntry(indent + 2, entry.getValue()));
            sb.append("\n");
        }

        for (String line : details) {
            for (int i = 0; i < indent + 2; i++) {
                sb.append(INDENT_STRING);
            }
            sb.append("[d]");
            sb.append(line);
            sb.append("\n");
        }

        if (cause != null) {
            for (int i = 0; i < indent + 2; i++) {
                sb.append(INDENT_STRING);
            }
            sb.append("[cause]");
            sb.append(cause.getClass().getSimpleName());
            sb.append(":");
            sb.append(cause.getMessage());
            sb.append("\n");
            if (printStackTrace) {
                dumpStackTrace(sb, cause.getStackTrace(), indent + 4);
                dumpInnerCauses(sb, cause.getCause(), indent + 3);
            }
        }

        for (OperationResult sub : getSubresults()) {
            sub.dumpIndent(sb, indent + 1, printStackTrace);
        }
    }

    private String dumpEntry(int indent, Serializable value) {
        if (value instanceof Element) {
            Element element = (Element) value;
            if (SchemaConstants.C_VALUE.equals(DOMUtil.getQName(element))) {
                try {
                    String cvalue = null;
                    if (value == null) {
                        cvalue = "null";
                    } else if (value instanceof Element) {
                        cvalue = SchemaDebugUtil.prettyPrint(XmlTypeConverter.toJavaValue((Element) value));
                    } else {
                        cvalue = SchemaDebugUtil.prettyPrint(value);
                    }
                    return DebugUtil.fixIndentInMultiline(indent, INDENT_STRING, cvalue);
                } catch (Exception e) {
                    return DebugUtil.fixIndentInMultiline(indent, INDENT_STRING,
                            "value: " + element.getTextContent());
                }
            }
        }
        return DebugUtil.fixIndentInMultiline(indent, INDENT_STRING, SchemaDebugUtil.prettyPrint(value));
    }

    private void dumpInnerCauses(StringBuilder sb, Throwable innerCause, int indent) {
        if (innerCause == null) {
            return;
        }
        for (int i = 0; i < indent; i++) {
            sb.append(INDENT_STRING);
        }
        sb.append("Caused by ");
        sb.append(innerCause.getClass().getName());
        sb.append(": ");
        sb.append(innerCause.getMessage());
        sb.append("\n");
        dumpStackTrace(sb, innerCause.getStackTrace(), indent + 1);
        dumpInnerCauses(sb, innerCause.getCause(), indent);
    }

    private static void dumpStackTrace(StringBuilder sb, StackTraceElement[] stackTrace, int indent) {
        for (int i = 0; i < stackTrace.length; i++) {
            for (int j = 0; j < indent; j++) {
                sb.append(INDENT_STRING);
            }
            StackTraceElement element = stackTrace[i];
            sb.append(element.toString());
            sb.append("\n");
        }
    }

    // primitive implementation - uncomment it if needed
    //    public OperationResult clone() {
    //        return CloneUtil.clone(this);
    //    }

    private static class OperationStatusKey {

        private String operation;
        private OperationResultStatus status;

        private OperationStatusKey(String operation, OperationResultStatus status) {
            this.operation = operation;
            this.status = status;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            OperationStatusKey that = (OperationStatusKey) o;

            if (operation != null ? !operation.equals(that.operation) : that.operation != null)
                return false;
            if (status != that.status)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = operation != null ? operation.hashCode() : 0;
            result = 31 * result + (status != null ? status.hashCode() : 0);
            return result;
        }
    }

    public OperationResult clone() {
        OperationResult clone = new OperationResult(operation);

        clone.status = status;
        clone.params = CloneUtil.clone(params);
        clone.context = CloneUtil.clone(context);
        clone.returns = CloneUtil.clone(returns);
        clone.token = token;
        clone.messageCode = messageCode;
        clone.message = message;
        clone.localizationMessage = localizationMessage;
        clone.localizationArguments = CloneUtil.clone(localizationArguments);
        clone.cause = CloneUtil.clone(cause);
        clone.count = count;
        if (subresults != null) {
            clone.subresults = new ArrayList<>(subresults.size());
            for (OperationResult subresult : subresults) {
                if (subresult != null) {
                    clone.subresults.add(subresult.clone());
                }
            }
        }
        clone.details = CloneUtil.clone(details);
        clone.summarizeErrors = summarizeErrors;
        clone.summarizePartialErrors = summarizePartialErrors;
        clone.summarizeSuccesses = summarizeSuccesses;
        clone.minor = minor;

        return clone;
    }

}