/* * Copyright (c) 2010-2017 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.Serializable; 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; /** * 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 CONTEXT_RESOURCE = "resource"; public static final String PARAM_OID = "oid"; public static final String PARAM_NAME = "name"; 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 PARAM_PROJECTION = "projection"; public static final String RETURN_COUNT = "count"; public static final String RETURN_BACKGROUND_TASK_OID = "backgroundTaskOid"; 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 int hiddenRecordsCount; private List<OperationResult> subresults; private List<String> details; private boolean summarizeErrors; private boolean summarizePartialErrors; private boolean summarizeSuccesses; private boolean minor = false; /** * Reference to an asynchronous operation that can be used to retrieve * the status of the running operation. This may be a task identifier, * identifier of a ticket in ITSM system or anything else. The exact * format of this reference depends on the operation which is being * executed. */ private String asynchronousOperationReference; 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<>(); } 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; } /** * Reference to an asynchronous operation that can be used to retrieve * the status of the running operation. This may be a task identifier, * identifier of a ticket in ITSM system or anything else. The exact * format of this reference depends on the operation which is being * executed. */ public String getAsynchronousOperationReference() { return asynchronousOperationReference; } public void setAsynchronousOperationReference(String asyncronousOperationReference) { this.asynchronousOperationReference = asyncronousOperationReference; } /** * 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 int getHiddenRecordsCount() { return hiddenRecordsCount; } public void setHiddenRecordsCount(int hiddenRecordsCount) { this.hiddenRecordsCount = hiddenRecordsCount; } public boolean representsHiddenRecords() { return this.hiddenRecordsCount > 0; } 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<>(); } 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(); } if (asynchronousOperationReference == null) { asynchronousOperationReference = sub.getAsynchronousOperationReference(); } 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 (asynchronousOperationReference == null) { asynchronousOperationReference = sub.getAsynchronousOperationReference(); } } 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<>(); } 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<>(); 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<>(); } 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<>(); } 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 deleteLastSubresultIfError() { OperationResult lastSubresult = getLastSubresult(); if (lastSubresult != null && lastSubresult.isError()) { removeLastSubresult(); } } 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()); Map<String, Serializable> context = ParamsTypeUtil.fromParamsType(result.getContext()); Map<String, Serializable> returns = ParamsTypeUtil.fromParamsType(result.getReturns()); List<OperationResult> subresults = null; if (!result.getPartialResults().isEmpty()) { subresults = new ArrayList<>(); 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()); } if (result.getHiddenRecordsCount() != null) { opResult.setHiddenRecordsCount(result.getHiddenRecordsCount()); } 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()); } if (opResult.getHiddenRecordsCount() != 0) { result.setHiddenRecordsCount(opResult.getHiddenRecordsCount()); } 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 (StackTraceElement aStackTrace : stackTrace) { detailsb.append(aStackTrace.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); } result.setParams(ParamsTypeUtil.toParamsType(opResult.getParams())); result.setContext(ParamsTypeUtil.toParamsType(opResult.getContext())); result.setReturns(ParamsTypeUtil.toParamsType(opResult.getReturns())); for (OperationResult subResult : opResult.getSubresults()) { result.getPartialResults().add(opResult.createOperationResultType(subResult)); } return result; } public void summarize() { summarize(false); } public void summarize(boolean alsoSubresults) { // first phase: summarizing records if explicitly requested 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(); } // second phase: summarizing (better said, eliminating or hiding) subresults if there are too many of them // (we strip subresults that have same operation name and status, if there are more of them than given threshold) // // We implement quite a complex algorithm to ensure "incremental stripping", i.e. calling summarize() repeatedly // on an OperationResult to which new standard entries are continually added. The requirement is that there must // be at most one summarization record, and it must be placed after all standard records of given type. Map<OperationStatusKey, OperationStatusCounter> recordsCounters = new HashMap<>(); iterator = getSubresults().iterator(); while (iterator.hasNext()) { OperationResult sr = iterator.next(); OperationStatusKey key = new OperationStatusKey(sr.getOperation(), sr.getStatus()); if (recordsCounters.containsKey(key)) { OperationStatusCounter counter = recordsCounters.get(key); if (!sr.representsHiddenRecords()) { if (counter.shownRecords < SUBRESULT_STRIP_THRESHOLD) { counter.shownRecords++; counter.shownCount += sr.count; } else { counter.hiddenCount += sr.count; iterator.remove(); } } else { counter.hiddenCount += sr.hiddenRecordsCount; iterator.remove(); // will be re-added at the end (potentially with records counters) } } else { OperationStatusCounter counter = new OperationStatusCounter(); if (!sr.representsHiddenRecords()) { counter.shownRecords = 1; counter.shownCount = sr.count; } else { counter.hiddenCount = sr.hiddenRecordsCount; iterator.remove(); // will be re-added at the end (potentially with records counters) } recordsCounters.put(key, counter); } } for (Map.Entry<OperationStatusKey, OperationStatusCounter> repeatingEntry : recordsCounters.entrySet()) { int shownCount = repeatingEntry.getValue().shownCount; int hiddenCount = repeatingEntry.getValue().hiddenCount; if (hiddenCount > 0) { OperationStatusKey key = repeatingEntry.getKey(); OperationResult hiddenRecordsEntry = new OperationResult(key.operation, key.status, hiddenCount + " record(s) were hidden to save space. Total number of records: " + (shownCount + hiddenCount)); hiddenRecordsEntry.setHiddenRecordsCount(hiddenCount); addSubresult(hiddenRecordsEntry); } } // And now, summarize each of the subresults if (alsoSubresults) { iterator = getSubresults().iterator(); while (iterator.hasNext()) { iterator.next().summarize(true); } } } 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 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(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) { DebugUtil.indentDebugDump(sb, indent); 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); } if (asynchronousOperationReference != null) { sb.append("\n"); DebugUtil.debugDumpWithLabel(sb, "asyncronousOperationReference", asynchronousOperationReference, indent + 2); } sb.append("\n"); for (Map.Entry<String, Serializable> entry : getParams().entrySet()) { DebugUtil.indentDebugDump(sb, indent + 2); 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()) { DebugUtil.indentDebugDump(sb, indent + 2); 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()) { DebugUtil.indentDebugDump(sb, indent + 2); sb.append("[r]"); sb.append(entry.getKey()); sb.append("="); sb.append(dumpEntry(indent+2, entry.getValue())); sb.append("\n"); } for (String line : details) { DebugUtil.indentDebugDump(sb, indent + 2); sb.append("[d]"); sb.append(line); sb.append("\n"); } if (cause != null) { DebugUtil.indentDebugDump(sb, indent + 2); 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 = SchemaDebugUtil.prettyPrint(XmlTypeConverter.toJavaValue(element)); 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; } DebugUtil.indentDebugDump(sb, indent); 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 (StackTraceElement aStackTrace : stackTrace) { DebugUtil.indentDebugDump(sb, indent); StackTraceElement element = aStackTrace; sb.append(element.toString()); sb.append("\n"); } } public void setBackgroundTaskOid(String oid) { addReturn(RETURN_BACKGROUND_TASK_OID, oid); } public String getBackgroundTaskOid() { Object oid = getReturns().get(RETURN_BACKGROUND_TASK_OID); return oid != null ? String.valueOf(oid) : null; } public void setMinor(boolean value) { this.minor = value; } // 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; } } private static class OperationStatusCounter { private int shownRecords; // how many actual records will be shown (after this wave of stripping) private int shownCount; // how many entries will be shown (after this wave of stripping) private int hiddenCount; // how many entries will be hidden (after this wave of stripping) } 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; clone.hiddenRecordsCount = hiddenRecordsCount; 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; clone.asynchronousOperationReference = asynchronousOperationReference; return clone; } }