/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2013 ForgeRock AS
*/
package org.opends.server.types;
import static org.opends.server.core.CoreConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import java.util.*;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.api.ClientConnection;
import org.opends.server.controls.ControlDecoder;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.operation.PostResponseOperation;
import org.opends.server.types.operation.PreParseOperation;
import org.opends.server.util.Validator;
/**
* This class defines a generic operation that may be processed by the
* Directory Server. Specific subclasses should implement specific
* functionality appropriate for the type of operation.
* <BR><BR>
* Note that this class is not intended to be subclassed by any
* third-party code outside of the OpenDJ project. It should only be
* extended by the operation types included in the
* {@code org.opends.server.core} package.
*/
@org.opends.server.types.PublicAPI(
stability=org.opends.server.types.StabilityLevel.VOLATILE,
mayInstantiate=false,
mayExtend=false,
mayInvoke=true)
public abstract class AbstractOperation
implements Operation, PreParseOperation, PostResponseOperation
{
/**
* The tracer object for the debug logger.
*/
private static final DebugTracer TRACER = getTracer();
/**
* The set of response controls that will always be returned for
* an abandon operation.
*/
protected static final List<Control> NO_RESPONSE_CONTROLS =
new ArrayList<Control>(0);
/**
* The client connection with which this operation is associated.
*/
protected final ClientConnection clientConnection;
/**
* The message ID for this operation.
*/
protected final int messageID;
/**
* The operation ID for this operation.
*/
protected final long operationID;
/**
* Whether nanotime was used for this operation.
*/
protected final boolean useNanoTime;
/**
* The cancel request for this operation.
*/
protected CancelRequest cancelRequest;
/**
* The cancel result for this operation.
*/
protected CancelResult cancelResult;
/**
* Indicates whether this is an internal operation triggered within the server
* itself rather than requested by an external client.
*/
private boolean isInternalOperation;
private Boolean isInnerOperation;
/**
* Indicates whether this operation is involved in data synchronization
* processing.
*/
private boolean isSynchronizationOperation;
/** The matched DN for this operation. */
private DN matchedDN;
/** The entry for the authorization identify for this operation. */
private Entry authorizationEntry;
/**
* A set of attachments associated with this operation that might be used by
* various components during its processing.
*/
private Map<String,Object> attachments;
/** The set of controls included in the request from the client. */
private List<Control> requestControls;
/** The set of referral URLs for this operation. */
private List<String> referralURLs;
/** The result code for this operation. */
private ResultCode resultCode;
/**
* Additional information that should be included in the log but not sent to
* the client.
*/
private List<AdditionalLogItem> additionalLogItems;
/**
* The error message for this operation that should be included in the log and
* in the response to the client.
*/
private MessageBuilder errorMessage;
/**
* Indicates whether this operation needs to be synchronized to other copies
* of the data.
*/
private boolean dontSynchronizeFlag;
/** The time that processing started on this operation in milliseconds. */
private long processingStartTime;
/** The time that processing ended on this operation in milliseconds. */
private long processingStopTime;
/** The time that processing started on this operation in nanoseconds. */
private long processingStartNanoTime;
/** The time that processing ended on this operation in nanoseconds. */
private long processingStopNanoTime;
/** The callbacks to be invoked once a response has been sent. */
private List<Runnable> postResponseCallbacks = null;
/**
* Creates a new operation with the provided information.
*
* @param clientConnection The client connection with which this
* operation is associated.
* @param operationID The identifier assigned to this
* operation for the client connection.
* @param messageID The message ID of the request with
* which this operation is associated.
* @param requestControls The set of controls included in the
* request.
*/
protected AbstractOperation(ClientConnection clientConnection,
long operationID,
int messageID, List<Control> requestControls)
{
this.clientConnection = clientConnection;
this.operationID = operationID;
this.messageID = messageID;
this.useNanoTime = DirectoryServer.getUseNanoTime();
if (requestControls == null)
{
this.requestControls = new ArrayList<Control>(0);
}
else
{
this.requestControls = requestControls;
}
resultCode = ResultCode.UNDEFINED;
additionalLogItems = null;
errorMessage = new MessageBuilder();
attachments = new HashMap<String,Object>();
matchedDN = null;
referralURLs = null;
cancelResult = null;
isInternalOperation = false;
isSynchronizationOperation = false;
authorizationEntry =
clientConnection.getAuthenticationInfo().
getAuthorizationEntry();
}
/**
* Terminates the client connection being used to process this
* operation. If this is called by a plugin, then that plugin must
* return a result indicating that the client connection has been
* terminated.
*
* @param disconnectReason The disconnect reason that provides the
* generic cause for the disconnect.
* @param sendNotification Indicates whether to try to provide
* notification
* to the client that the connection will
* be closed.
* @param message The message to send to the client. It
* may be {@code null} if no notification
* is to be sent.
*/
@Override
public void disconnectClient(DisconnectReason disconnectReason,
boolean sendNotification,
Message message)
{
clientConnection.disconnect(disconnectReason, sendNotification,
message);
}
/**
* Retrieves a set of standard elements that should be logged in all
* requests and responses for all types of operations. Each element
* in the array will itself be a two-element array in which the
* first element is the name of the field and the second is a string
* representation of the value, or {@code null} if there is no value
* for that field.
*
* @return A standard set of elements that should be logged in
* requests and responses for all types of operations.
*/
@Override
public final String[][] getCommonLogElements()
{
// Note that no debugging will be done in this method because
// it is a likely candidate for being called by the logging
// subsystem.
return new String[][]
{
new String[] { LOG_ELEMENT_CONNECTION_ID,
String.valueOf(getConnectionID()) },
new String[] { LOG_ELEMENT_OPERATION_ID,
String.valueOf(operationID) },
new String[] { LOG_ELEMENT_MESSAGE_ID,
String.valueOf(messageID) }
};
}
/**
* Retrieves the client connection with which this operation is
* associated.
*
* @return The client connection with which this operation is
* associated.
*/
@Override
public final ClientConnection getClientConnection()
{
return clientConnection;
}
/**
* Retrieves the unique identifier that is assigned to the client
* connection that submitted this operation.
*
* @return The unique identifier that is assigned to the client
* connection that submitted this operation.
*/
@Override
public final long getConnectionID()
{
return clientConnection.getConnectionID();
}
/**
* Retrieves the operation ID for this operation.
*
* @return The operation ID for this operation.
*/
@Override
public final long getOperationID()
{
return operationID;
}
/**
* Retrieves the message ID assigned to this operation.
*
* @return The message ID assigned to this operation.
*/
@Override
public final int getMessageID()
{
return messageID;
}
/**
* Retrieves the set of controls included in the request from the
* client. The returned list must not be altered.
*
* @return The set of controls included in the request from the
* client.
*/
@Override
public final List<Control> getRequestControls()
{
return requestControls;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public final <T extends Control> T getRequestControl(
ControlDecoder<T> d) throws DirectoryException
{
String oid = d.getOID();
for(int i = 0; i < requestControls.size(); i++)
{
Control c = requestControls.get(i);
if(c.getOID().equals(oid))
{
if(c instanceof LDAPControl)
{
T decodedControl = d.decode(c.isCritical(),
((LDAPControl) c).getValue());
requestControls.set(i, decodedControl);
return decodedControl;
}
else
{
return (T)c;
}
}
}
return null;
}
/**
* Adds the provided control to the set of request controls for this
* operation. This method may only be called by pre-parse plugins.
*
* @param control The control to add to the set of request
* controls for this operation.
*/
@Override
public final void addRequestControl(Control control)
{
requestControls.add(control);
}
/**
* Removes the provided control from the set of request controls for
* this operation. This method may only be called by pre-parse
* plugins.
*
* @param control The control to remove from the set of request
* controls for this operation.
*/
@Override
public final void removeRequestControl(Control control)
{
requestControls.remove(control);
}
/**
* Retrieves the result code for this operation.
*
* @return The result code associated for this operation, or
* {@code UNDEFINED} if the operation has not yet
* completed.
*/
@Override
public final ResultCode getResultCode()
{
return resultCode;
}
/**
* Specifies the result code for this operation. This method may
* not be called by post-response plugins.
*
* @param resultCode The result code for this operation.
*/
@Override
public final void setResultCode(ResultCode resultCode)
{
this.resultCode = resultCode;
}
/**
* Retrieves the error message for this operation. Its contents may
* be altered by pre-parse, pre-operation, and post-operation
* plugins, but not by post-response plugins.
*
* @return The error message for this operation.
*/
@Override
public final MessageBuilder getErrorMessage()
{
return errorMessage;
}
/**
* Specifies the error message for this operation. This method may
* not be called by post-response plugins.
*
* @param errorMessage The error message for this operation.
*/
@Override
public final void setErrorMessage(MessageBuilder errorMessage)
{
if (errorMessage == null)
{
this.errorMessage = new MessageBuilder();
}
else
{
this.errorMessage = errorMessage;
}
}
/**
* Appends the provided message to the error message buffer. If the
* buffer has not yet been created, then this will create it first
* and then add the provided message. This method may not be called
* by post-response plugins.
*
* @param message The message to append to the error message
* buffer.
*/
@Override
public final void appendErrorMessage(Message message)
{
if (errorMessage == null)
{
errorMessage = new MessageBuilder(message);
}
else
{
if (errorMessage.length() > 0)
{
errorMessage.append(" ");
}
errorMessage.append(message);
}
}
/**
* {@inheritDoc}
*/
@Override
public List<AdditionalLogItem> getAdditionalLogItems()
{
if (additionalLogItems == null)
{
return Collections.emptyList();
}
else
{
return Collections.unmodifiableList(additionalLogItems);
}
}
/**
* {@inheritDoc}
*/
@Override
public void addAdditionalLogItem(AdditionalLogItem item)
{
Validator.ensureNotNull(item);
if (additionalLogItems == null)
{
additionalLogItems = new LinkedList<AdditionalLogItem>();
}
additionalLogItems.add(item);
}
/**
* Retrieves the matched DN for this operation.
*
* @return The matched DN for this operation, or {@code null} if the operation
* has not yet completed or does not have a matched DN.
*/
@Override
public final DN getMatchedDN()
{
return matchedDN;
}
/**
* Specifies the matched DN for this operation. This may not be
* called by post-response plugins.
*
* @param matchedDN The matched DN for this operation.
*/
@Override
public final void setMatchedDN(DN matchedDN)
{
this.matchedDN = matchedDN;
}
/**
* Retrieves the set of referral URLs for this operation. Its
* contents must not be altered by the caller.
*
* @return The set of referral URLs for this operation, or
* {@code null} if the operation is not yet complete or
* does not have a set of referral URLs.
*/
@Override
public final List<String> getReferralURLs()
{
return referralURLs;
}
/**
* Specifies the set of referral URLs for this operation. This may
* not be called by post-response plugins.
*
* @param referralURLs The set of referral URLs for this
* operation.
*/
@Override
public final void setReferralURLs(List<String> referralURLs)
{
this.referralURLs = referralURLs;
}
/**
* Sets the response elements for this operation based on the
* information contained in the provided {@code DirectoryException}
* object. This method may not be called by post-response plugins.
*
* @param directoryException The exception containing the
* information to use for the response
* elements.
*/
@Override
public final void setResponseData(
DirectoryException directoryException)
{
this.resultCode = directoryException.getResultCode();
this.matchedDN = directoryException.getMatchedDN();
this.referralURLs = directoryException.getReferralURLs();
appendErrorMessage(directoryException.getMessageObject());
}
/** {@inheritDoc} */
@Override
public final boolean isInternalOperation()
{
return isInternalOperation;
}
/** {@inheritDoc} */
@Override
public final void setInternalOperation(boolean isInternalOperation)
{
this.isInternalOperation = isInternalOperation;
}
/** {@inheritDoc} */
@Override
public boolean isInnerOperation()
{
if (this.isInnerOperation != null)
{
return this.isInnerOperation;
}
return isInternalOperation();
}
/** {@inheritDoc} */
@Override
public void setInnerOperation(boolean isInnerOperation)
{
this.isInnerOperation = isInnerOperation;
}
/** {@inheritDoc} */
@Override
public final boolean isSynchronizationOperation()
{
return isSynchronizationOperation;
}
/** {@inheritDoc} */
@Override
public final void setSynchronizationOperation(
boolean isSynchronizationOperation)
{
this.isSynchronizationOperation = isSynchronizationOperation;
}
/**
* Indicates whether this operation needs to be synchronized to
* other copies of the data.
*
* @return {@code true} if this operation should not be
* synchronized, or {@code false} if it should be
* synchronized.
*/
@Override
public boolean dontSynchronize()
{
return dontSynchronizeFlag;
}
/**
* Specifies whether this operation must be synchronized to other
* copies of the data.
*
* @param dontSynchronize Specifies whether this operation must be
* synchronized to other copies
* of the data.
*/
@Override
public final void setDontSynchronize(boolean dontSynchronize)
{
this.dontSynchronizeFlag = dontSynchronize;
}
/**
* Retrieves the entry for the user that should be considered the
* authorization identity for this operation. In many cases, it
* will be the same as the authorization entry for the underlying
* client connection, or {@code null} if no authentication has been
* performed on that connection. However, it may be some other
* value if special processing has been requested (e.g., the
* operation included a proxied authorization control). This method
* should not be called by pre-parse plugins because the correct
* value may not yet have been determined.
*
* @return The entry for the user that should be considered the
* authorization identity for this operation, or
* {@code null} if the authorization identity should be the
* unauthenticated user.
*/
@Override
public final Entry getAuthorizationEntry()
{
return authorizationEntry;
}
/**
* Provides the entry for the user that should be considered the
* authorization identity for this operation. This must not be
* called from within a plugin.
*
* @param authorizationEntry The entry for the user that should be
* considered the authorization identity
* for this operation, or {@code null}
* if it should be the unauthenticated
* user.
*/
@Override
public final void setAuthorizationEntry(Entry authorizationEntry)
{
this.authorizationEntry = authorizationEntry;
}
/**
* Retrieves the authorization DN for this operation. In many
* cases, it will be the same as the DN of the authenticated user
* for the underlying connection, or the null DN if no
* authentication has been performed on that connection. However,
* it may be some other value if special processing has been
* requested (e.g., the operation included a proxied authorization
* control). This method should not be called by pre-parse plugins
* because the correct value may not have yet been determined.
*
* @return The authorization DN for this operation, or the null DN
* if it should be the unauthenticated user..
*/
@Override
public final DN getAuthorizationDN()
{
if (authorizationEntry == null)
{
return DN.nullDN();
}
else
{
return authorizationEntry.getDN();
}
}
/**
* Retrieves the set of attachments defined for this operation, as a
* mapping between the attachment name and the associated object.
*
* @return The set of attachments defined for this operation.
*/
@Override
public final Map<String,Object> getAttachments()
{
return attachments;
}
/**
* Set the attachments to the operation.
*
* @param attachments - Attachments to register within the
* operation
*/
@Override
public final void setAttachments(Map<String, Object> attachments)
{
this.attachments = attachments;
}
/**
* Retrieves the attachment with the specified name.
*
* @param name The name for the attachment to retrieve. It will
* be treated in a case-sensitive manner.
*
* @return The requested attachment object, or {@code null} if it
* does not exist.
*/
@Override
public final Object getAttachment(String name)
{
return attachments.get(name);
}
/**
* Removes the attachment with the specified name.
*
* @param name The name for the attachment to remove. It will be
* treated in a case-sensitive manner.
*
* @return The attachment that was removed, or {@code null} if it
* does not exist.
*/
@Override
public final Object removeAttachment(String name)
{
return attachments.remove(name);
}
/**
* Sets the value of the specified attachment. If an attachment
* already exists with the same name, it will be replaced.
* Otherwise, a new attachment will be added.
*
* @param name The name to use for the attachment.
* @param value The value to use for the attachment.
*
* @return The former value held by the attachment with the given
* name, or {@code null} if there was previously no such
* attachment.
*/
@Override
public final Object setAttachment(String name, Object value)
{
return attachments.put(name, value);
}
/**
* Indicates that processing on this operation has completed
* successfully and that the client should perform any associated
* cleanup work.
*/
@Override
public final void operationCompleted()
{
// Notify the client connection that this operation is complete
// and that it no longer needs to be retained.
clientConnection.removeOperationInProgress(messageID);
}
/**
* Attempts to cancel this operation before processing has
* completed.
*
* @param cancelRequest Information about the way in which the
* operation should be canceled.
*
* @return A code providing information on the result of the
* cancellation.
*/
@Override
public CancelResult cancel(CancelRequest cancelRequest)
{
abort(cancelRequest);
long stopWaitingTime = System.currentTimeMillis() + 5000;
while ((cancelResult == null) &&
(System.currentTimeMillis() < stopWaitingTime))
{
try
{
Thread.sleep(50);
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
if (cancelResult == null)
{
// This can happen in some rare cases (e.g., if a client
// disconnects and there is still a lot of data to send to
// that client), and in this case we'll prevent the cancel
// thread from blocking for a long period of time.
cancelResult = new CancelResult(ResultCode.CANNOT_CANCEL, null);
}
return cancelResult;
}
/**
* Attempts to cancel this operation before processing has
* completed without waiting for a cancel result.
*
* @param cancelRequest Information about the way in which the
* operation should be canceled.
*/
@Override
public synchronized void abort(CancelRequest cancelRequest)
{
if(cancelResult == null && this.cancelRequest == null)
{
this.cancelRequest = cancelRequest;
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final void
checkIfCanceled(boolean signalTooLate)
throws CanceledOperationException {
if(cancelRequest != null)
{
throw new CanceledOperationException(cancelRequest);
}
if(signalTooLate && cancelResult != null)
{
cancelResult = new CancelResult(ResultCode.TOO_LATE, null);
}
}
/**
* {@inheritDoc}
*/
@Override
public final CancelRequest getCancelRequest()
{
return cancelRequest;
}
/**
* {@inheritDoc}
*/
@Override
public final CancelResult getCancelResult()
{
return cancelResult;
}
/**
* Retrieves a string representation of this operation.
*
* @return A string representation of this operation.
*/
@Override
public final String toString()
{
StringBuilder buffer = new StringBuilder();
toString(buffer);
return buffer.toString();
}
/**
* Retrieves the time that processing started for this operation.
*
* @return The time that processing started for this operation.
*/
@Override
public final long getProcessingStartTime()
{
return processingStartTime;
}
/**
* Set the time at which the processing started for this operation.
*/
public final void setProcessingStartTime()
{
processingStartTime = System.currentTimeMillis();
if(useNanoTime)
{
processingStartNanoTime = System.nanoTime();
}
}
/**
* Retrieves the time that processing stopped for this operation.
* This will actually hold a time immediately before the response
* was sent to the client.
*
* @return The time that processing stopped for this operation.
*/
@Override
public final long getProcessingStopTime()
{
return processingStopTime;
}
/**
* Set the time at which the processing stopped for this operation.
* This will actually hold a time immediately before the response
* was sent to the client.
*/
public final void setProcessingStopTime()
{
this.processingStopTime = System.currentTimeMillis();
if(useNanoTime)
{
this.processingStopNanoTime = System.nanoTime();
}
}
/**
* Retrieves the length of time in milliseconds that
* the server spent processing this operation. This should not be
* called until after the server has sent the response to the
* client.
*
* @return The length of time in milliseconds that
* the server spent processing this operation.
*/
@Override
public final long getProcessingTime()
{
return (processingStopTime - processingStartTime);
}
/**
* Retrieves the length of time in nanoseconds that
* the server spent processing this operation if available.
* This should not be called until after the server has sent the
* response to the client.
*
* @return The length of time in nanoseconds that the server
* spent processing this operation or -1 if its not
* available.
*/
@Override
public final long getProcessingNanoTime()
{
if(useNanoTime)
{
return (processingStopNanoTime - processingStartNanoTime);
}
else
{
return -1;
}
}
/**
* {@inheritDoc}
*/
@Override
public final void registerPostResponseCallback(Runnable callback)
{
if (postResponseCallbacks == null)
{
postResponseCallbacks = new LinkedList<Runnable>();
}
postResponseCallbacks.add(callback);
}
/**
* {@inheritDoc}
*/
@Override
public final int hashCode()
{
return clientConnection.hashCode() * (int) operationID;
}
/**
* {@inheritDoc}
*/
@Override
public final boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj instanceof Operation)
{
Operation other = (Operation) obj;
if (other.getClientConnection().equals(clientConnection))
{
return other.getOperationID() == operationID;
}
}
return false;
}
/**
* Invokes the post response callbacks that were registered with
* this operation.
*/
protected final void invokePostResponseCallbacks()
{
if (postResponseCallbacks != null)
{
for (Runnable callback : postResponseCallbacks)
{
try
{
callback.run();
}
catch (Exception e)
{
// Should not happen.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
}
}
}