/* * 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 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2010-2012 ForgeRock AS */ package org.opends.server.workflowelement.externalchangelog; import static org.opends.messages.CoreMessages.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import static org.opends.server.util.LDIFWriter.appendLDIFSeparatorAndValue; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.getExceptionMessage; import java.text.SimpleDateFormat; import java.util.*; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.Severity; import org.opends.server.api.ClientConnection; import org.opends.server.api.plugin.PluginResult; import org.opends.server.config.ConfigConstants; import org.opends.server.controls.*; import org.opends.server.core.*; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.ExternalChangeLogSession; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.replication.plugin.MultimasterReplication; import org.opends.server.replication.protocol.*; import org.opends.server.replication.server.ReplicationServer; import org.opends.server.types.*; import org.opends.server.types.operation.PostOperationSearchOperation; import org.opends.server.types.operation.PreOperationSearchOperation; import org.opends.server.types.operation.SearchEntrySearchOperation; import org.opends.server.types.operation.SearchReferenceSearchOperation; import org.opends.server.util.ServerConstants; /** * This class defines an operation used to search for entries in a local backend * of the Directory Server. */ public class ECLSearchOperation extends SearchOperationWrapper implements PreOperationSearchOperation, PostOperationSearchOperation, SearchEntrySearchOperation, SearchReferenceSearchOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The ECL Start Session we'll send to the RS. */ private StartECLSessionMsg startECLSessionMsg; //The set of supported controls for this WE private static final HashSet<String> CHANGELOG_SUPPORTED_CONTROLS; static { CHANGELOG_SUPPORTED_CONTROLS = new HashSet<String>(0); CHANGELOG_SUPPORTED_CONTROLS .add(ServerConstants.OID_SERVER_SIDE_SORT_REQUEST_CONTROL); CHANGELOG_SUPPORTED_CONTROLS.add(ServerConstants.OID_VLV_REQUEST_CONTROL); } // The set of objectclasses that will be used in ECL root entry. private static final HashMap<ObjectClass, String> CHANGELOG_ROOT_OBJECT_CLASSES; static { CHANGELOG_ROOT_OBJECT_CLASSES = new LinkedHashMap<ObjectClass, String>(2); ObjectClass topOC = DirectoryServer.getObjectClass(OC_TOP, true); CHANGELOG_ROOT_OBJECT_CLASSES.put(topOC, OC_TOP); ObjectClass containerOC = DirectoryServer.getObjectClass("container", true); CHANGELOG_ROOT_OBJECT_CLASSES.put(containerOC, "container"); } // The set of objectclasses that will be used in ECL entries. private static final HashMap<ObjectClass, String> CHANGELOG_ENTRY_OBJECT_CLASSES; static { CHANGELOG_ENTRY_OBJECT_CLASSES = new LinkedHashMap<ObjectClass, String>(2); ObjectClass topOC = DirectoryServer.getObjectClass(OC_TOP, true); CHANGELOG_ENTRY_OBJECT_CLASSES.put(topOC, OC_TOP); ObjectClass eclEntryOC = DirectoryServer.getObjectClass(OC_CHANGELOG_ENTRY, true); CHANGELOG_ENTRY_OBJECT_CLASSES.put(eclEntryOC, OC_CHANGELOG_ENTRY); } // The attribute type for the "creatorsName" attribute. private static final AttributeType CREATORS_NAME_TYPE; // The attribute type for the "modifiersName" attribute. private static final AttributeType MODIFIERS_NAME_TYPE; static { CREATORS_NAME_TYPE = DirectoryConfig.getAttributeType( OP_ATTR_CREATORS_NAME_LC, true); MODIFIERS_NAME_TYPE = DirectoryConfig.getAttributeType( OP_ATTR_MODIFIERS_NAME_LC, true); } // The associated DN. private static final DN CHANGELOG_ROOT_DN; static { try { CHANGELOG_ROOT_DN = DN .decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT); } catch (Exception e) { throw new RuntimeException(e); } } /** * The replication server in which the search on ECL is to be performed. */ private ReplicationServer replicationServer; /** * The client connection for the search operation. */ private ClientConnection clientConnection; /** * The base DN for the search. */ private DN baseDN; /** * The persistent search request, if applicable. */ private PersistentSearch persistentSearch; /** * The filter for the search. */ private SearchFilter filter; private ExternalChangeLogSession eclSession; /** * A flag to know if the ECLControl has been requested. */ private Boolean returnECLControl = false; /** * Creates a new operation that may be used to search for entries in a local * backend of the Directory Server. * * @param search The operation to process. */ ECLSearchOperation(SearchOperation search) { super(search); ECLWorkflowElement.attachLocalOperation(search, this); } /** * Process this search operation against a local backend. * * @param wfe * The local backend work-flow element. * @throws CanceledOperationException * if this operation should be canceled */ void processECLSearch(ECLWorkflowElement wfe) throws CanceledOperationException { boolean executePostOpPlugins = false; // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); // Check for a request to cancel this operation. checkIfCanceled(false); searchProcessing: { replicationServer = wfe.getReplicationServer(); clientConnection = getClientConnection(); startECLSessionMsg = new StartECLSessionMsg(); // Set default behavior as "from draft change number". // "from cookie" is set only when cookie is provided. startECLSessionMsg.setECLRequestType( StartECLSessionMsg.REQUEST_TYPE_FROM_DRAFT_CHANGE_NUMBER); // Set a string operationid that will help correlate any error message // logged for this operation with the 'real' client operation. startECLSessionMsg.setOperationId(this.toString()); // Set a list of excluded domains (also exclude 'cn=changelog' itself) ArrayList<String> excludedDomains = MultimasterReplication.getECLDisabledDomains(); if (!excludedDomains.contains(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT)) excludedDomains.add(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT); startECLSessionMsg.setExcludedDNs(excludedDomains); // Process the search base and filter to convert them from their raw forms // as provided by the client to the forms required for the rest of the // search processing. baseDN = getBaseDN(); filter = getFilter(); if ((baseDN == null) || (filter == null)){ break searchProcessing; } // Test existence of the RS - normally should always be here if (replicationServer == null) { setResultCode(ResultCode.OPERATIONS_ERROR); appendErrorMessage(ERR_SEARCH_BASE_DOESNT_EXIST.get( String.valueOf(baseDN))); break searchProcessing; } // Analyse controls - including the cookie control try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) TRACER.debugCaught(DebugLogLevel.ERROR, de); setResponseData(de); break searchProcessing; } // Process search parameters to optimize session query. try { evaluateSearchParameters(startECLSessionMsg, baseDN, filter); } catch (DirectoryException de) { if (debugEnabled()) TRACER.debugCaught(DebugLogLevel.ERROR, de); setResponseData(de); break searchProcessing; } // Check for a request to cancel this operation. checkIfCanceled(false); // Invoke the pre-operation search plugins. executePostOpPlugins = true; PluginResult.PreOperation preOpResult = pluginConfigManager.invokePreOperationSearchPlugins(this); if (!preOpResult.continueProcessing()) { setResultCode(preOpResult.getResultCode()); appendErrorMessage(preOpResult.getErrorMessage()); setMatchedDN(preOpResult.getMatchedDN()); setReferralURLs(preOpResult.getReferralURLs()); break searchProcessing; } // Check for a request to cancel this operation. checkIfCanceled(false); // Be optimistic by default. setResultCode(ResultCode.SUCCESS); // If there's a persistent search, then register it with the server. if (persistentSearch != null) { wfe.registerPersistentSearch(persistentSearch); persistentSearch.enable(); } // Process the search. try { processSearch(); } catch (DirectoryException de) { if (debugEnabled()) TRACER.debugCaught(DebugLogLevel.ERROR, de); setResponseData(de); if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } break searchProcessing; } catch (CanceledOperationException coe) { if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } if (eclSession != null) { try { eclSession.close(); } catch(Exception ignored){} } throw coe; } catch (Exception e) { if (debugEnabled()) TRACER.debugCaught(DebugLogLevel.ERROR, e); setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_SEARCH_BACKEND_EXCEPTION.get( getExceptionMessage(e))); if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } break searchProcessing; } } // Check for a request to cancel this operation. checkIfCanceled(false); // Invoke the post-operation search plugins. if (executePostOpPlugins) { PluginResult.PostOperation postOpResult = pluginConfigManager.invokePostOperationSearchPlugins(this); if (!postOpResult.continueProcessing()) { setResultCode(postOpResult.getResultCode()); appendErrorMessage(postOpResult.getErrorMessage()); setMatchedDN(postOpResult.getMatchedDN()); setReferralURLs(postOpResult.getReferralURLs()); } } } /** * Handles any controls contained in the request - including the cookie ctrl. * * @throws DirectoryException If there is a problem with any of the request * controls. */ private void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (!AccessControlConfigManager.getInstance().getAccessControlHandler() .isAllowed(baseDN, this, c)) { // As per RFC 4511 4.1.11. if (c.isCritical()) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } else { // We don't want to process this non-critical control, so remove it. removeRequestControl(c); continue; } } if (oid.equals(OID_ECL_COOKIE_EXCHANGE_CONTROL)) { ExternalChangelogRequestControl eclControl = getRequestControl(ExternalChangelogRequestControl.DECODER); MultiDomainServerState cookie = eclControl.getCookie(); returnECLControl = true; if (cookie!=null) { startECLSessionMsg.setECLRequestType( StartECLSessionMsg.REQUEST_TYPE_FROM_COOKIE); startECLSessionMsg.setCrossDomainServerState(cookie.toString()); } } else if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl = getRequestControl(LDAPAssertionRequestControl.DECODER); try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter assertionFilter = assertControl.getSearchFilter(); Entry entry; try { // FIXME: this is broken (recursive)? entry = DirectoryServer.getEntry(baseDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(de.getResultCode(), ERR_SEARCH_CANNOT_GET_ENTRY_FOR_ASSERTION.get( de.getMessageObject())); } if (entry == null) { throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_SEARCH_NO_SUCH_ENTRY_FOR_ASSERTION.get()); } if (! assertionFilter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_SEARCH_ASSERTION_FAILED.get()); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_SEARCH_CANNOT_PROCESS_ASSERTION_FILTER.get( de.getMessageObject()), de); } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // Log usage of legacy proxy authz V1 control. addAdditionalLogItem(AdditionalLogItem.keyOnly(getClass(), "obsoleteProxiedAuthzV1Control")); // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl = getRequestControl(ProxiedAuthV1Control.DECODER); Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl = getRequestControl(ProxiedAuthV2Control.DECODER); Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PERSISTENT_SEARCH)) { PersistentSearchControl psearchControl = getRequestControl(PersistentSearchControl.DECODER); persistentSearch = new PersistentSearch(this, psearchControl.getChangeTypes(), psearchControl.getReturnECs()); // If we're only interested in changes, then we don't actually want // to process the search now. if (psearchControl.getChangesOnly()) startECLSessionMsg.setPersistent( StartECLSessionMsg.PERSISTENT_CHANGES_ONLY); else startECLSessionMsg.setPersistent( StartECLSessionMsg.PERSISTENT); } else if (oid.equals(OID_LDAP_SUBENTRIES)) { SubentriesControl subentriesControl = getRequestControl(SubentriesControl.DECODER); setReturnSubentriesOnly(subentriesControl.getVisibility()); } else if (oid.equals(OID_LDUP_SUBENTRIES)) { // Support for legacy draft-ietf-ldup-subentry. addAdditionalLogItem(AdditionalLogItem.keyOnly(getClass(), "obsoleteSubentryControl")); setReturnSubentriesOnly(true); } else if (oid.equals(OID_MATCHED_VALUES)) { MatchedValuesControl matchedValuesControl = getRequestControl(MatchedValuesControl.DECODER); setMatchedValuesControl(matchedValuesControl); } else if (oid.equals(OID_ACCOUNT_USABLE_CONTROL)) { setIncludeUsableControl(true); } else if (oid.equals(OID_REAL_ATTRS_ONLY)) { setRealAttributesOnly(true); } else if (oid.equals(OID_VIRTUAL_ATTRS_ONLY)) { setVirtualAttributesOnly(true); } else if (oid.equals(OID_GET_EFFECTIVE_RIGHTS) && DirectoryServer.isSupportedControl(OID_GET_EFFECTIVE_RIGHTS)) { // Do nothing here and let AciHandler deal with it. } // TODO: Add support for additional controls, including VLV else if (c.isCritical()) { if ((replicationServer == null) || (! supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_SEARCH_UNSUPPORTED_CRITICAL_CONTROL.get(oid)); } } } } } private void processSearch() throws DirectoryException, CanceledOperationException { if (debugEnabled()) { TRACER.debugInfo(" processSearch toString=[" + toString() + "] opid=[" + startECLSessionMsg.getOperationId() + "]"); } // Start a specific ECL session eclSession = replicationServer.createECLSession(startECLSessionMsg); boolean abortECLSession = false; try { // Get first update (this is needed to determine hasSubordinates. ECLUpdateMsg update = eclSession.getNextUpdate(); // Return root entry if requested. if (CHANGELOG_ROOT_DN.matchesBaseAndScope(baseDN, getScope())) { final Entry entry = createRootEntry(update != null); if (filter.matchesEntry(entry)) { if (!returnEntry(entry, null)) { // Abandon, Size limit reached. abortECLSession = true; return; } } } if (baseDN.equals(CHANGELOG_ROOT_DN) && getScope().equals( SearchScope.BASE_OBJECT)) { // Only the change log root entry was requested. There is no need to // process other entries. return; } // Process change log entries. while (update != null) { // Check for a request to cancel this operation. checkIfCanceled(false); if (!buildAndReturnEntry(update)) { // Abandon, Size limit reached. abortECLSession = true; return; } update = eclSession.getNextUpdate(); } } catch (CanceledOperationException e) { abortECLSession = true; throw e; } catch (DirectoryException e) { abortECLSession = true; throw e; } finally { if (persistentSearch == null || abortECLSession) { eclSession.close(); } } } private boolean supportsControl(String oid) { return CHANGELOG_SUPPORTED_CONTROLS.contains(oid); } /** * Build an ECL entry from a provided ECL msg and return it. * @param eclmsg The provided ECL msg. * @return <CODE>true</CODE> if the caller should continue processing the * search request and sending additional entries and references, or * <CODE>false</CODE> if not for some reason (e.g., the size limit * has been reached or the search has been abandoned). * @throws DirectoryException When an errors occurs. */ private boolean buildAndReturnEntry(ECLUpdateMsg eclmsg) throws DirectoryException { final Entry entry = createEntryFromMsg(eclmsg); if (matchScopeAndFilter(entry)) { List<Control> controls = null; if (returnECLControl) { controls = new ArrayList<Control>(1); EntryChangelogNotificationControl clrc = new EntryChangelogNotificationControl( true, eclmsg.getCookie().toString()); controls.add(clrc); } return returnEntry(entry, controls); } return true; } /** * Test if the provided entry matches the filter, base and scope. * * @param entry * The provided entry * @return whether the entry matches. * @throws DirectoryException * When a problem occurs. */ private boolean matchScopeAndFilter(Entry entry) throws DirectoryException { if (entry.matchesBaseAndScope(getBaseDN(), getScope())) { return getFilter().matchesEntry(entry); } else { return false; } } /** * Create an ECL entry from a provided ECL msg. * * @param eclmsg * the provided ECL msg. * @return the created ECL entry. * @throws DirectoryException * When an error occurs. */ public static Entry createEntryFromMsg(ECLUpdateMsg eclmsg) throws DirectoryException { Entry clEntry = null; // Get the meat from the ecl msg UpdateMsg msg = eclmsg.getUpdateMsg(); if (msg instanceof AddMsg) { AddMsg addMsg = (AddMsg) msg; // Map addMsg to an LDIF string for the 'changes' attribute, and pull // out change initiators name if available which is contained in the // creatorsName attribute. String changeInitiatorsName = null; String ldifChanges = null; try { StringBuilder builder = new StringBuilder(256); for (Attribute a : addMsg.getAttributes()) { if (a.getAttributeType().equals(CREATORS_NAME_TYPE) && !a.isEmpty()) { // This attribute is not multi-valued. changeInitiatorsName = a.iterator().next().toString(); } String attrName = a.getNameWithOptions(); for (AttributeValue v : a) { builder.append(attrName); appendLDIFSeparatorAndValue(builder, v.getValue()); builder.append('\n'); } } ldifChanges = builder.toString(); } catch (Exception e) { // Unable to decode the message - log an error. TRACER.debugCaught(DebugLogLevel.ERROR, e); logError(Message.raw( Category.SYNC, Severity.MILD_ERROR, "An exception was encountered while try to encode a " + "replication add message for entry \"" + addMsg.getDn() + "\" into an External Change Log entry: " + e.getMessage())); } ArrayList<RawAttribute> eclAttributes = addMsg.getEclIncludes(); clEntry = createChangelogEntry(eclmsg.getServiceId(), eclmsg .getCookie().toString(), DN.decode(addMsg.getDn()), addMsg.getChangeNumber(), ldifChanges, // entry as created (in LDIF // format) addMsg.getEntryUUID(), eclAttributes, // entry attributes eclmsg.getDraftChangeNumber(), "add", changeInitiatorsName); } else if (msg instanceof ModifyCommonMsg) { ModifyCommonMsg modifyMsg = (ModifyCommonMsg) msg; // Map the modifyMsg to an LDIF string for the 'changes' attribute, and // pull out change initiators name if available which is contained in the // modifiersName attribute. String changeInitiatorsName = null; String ldifChanges = null; try { StringBuilder builder = new StringBuilder(128); for (Modification m : modifyMsg.getMods()) { Attribute a = m.getAttribute(); if (m.getModificationType() == ModificationType.REPLACE && a.getAttributeType().equals(MODIFIERS_NAME_TYPE) && !a.isEmpty()) { // This attribute is not multi-valued. changeInitiatorsName = a.iterator().next().toString(); } String attrName = a.getNameWithOptions(); builder.append(m.getModificationType().getLDIFName()); builder.append(": "); builder.append(attrName); builder.append('\n'); for (AttributeValue v : a) { builder.append(attrName); appendLDIFSeparatorAndValue(builder, v.getValue()); builder.append('\n'); } builder.append("-\n"); } ldifChanges = builder.toString(); } catch (Exception e) { // Unable to decode the message - log an error. TRACER.debugCaught(DebugLogLevel.ERROR, e); logError(Message.raw( Category.SYNC, Severity.MILD_ERROR, "An exception was encountered while try to encode a " + "replication modify message for entry \"" + modifyMsg.getDn() + "\" into an External Change Log entry: " + e.getMessage())); } String changeType = (modifyMsg instanceof ModifyDNMsg) ? "modrdn" : "modify"; clEntry = createChangelogEntry(eclmsg.getServiceId(), eclmsg .getCookie().toString(), DN.decode(modifyMsg.getDn()), modifyMsg.getChangeNumber(), ldifChanges, modifyMsg.getEntryUUID(), modifyMsg.getEclIncludes(), // entry attributes eclmsg.getDraftChangeNumber(), changeType, changeInitiatorsName); if (modifyMsg instanceof ModifyDNMsg) { ModifyDNMsg modDNMsg = (ModifyDNMsg) modifyMsg; Attribute a = Attributes.create("newrdn", modDNMsg.getNewRDN()); clEntry.addAttribute(a, null); if (modDNMsg.getNewSuperior() != null) { Attribute b = Attributes.create("newsuperior", modDNMsg.getNewSuperior()); clEntry.addAttribute(b, null); } Attribute c = Attributes.create("deleteoldrdn", String.valueOf(modDNMsg.deleteOldRdn())); clEntry.addAttribute(c, null); } } else if (msg instanceof DeleteMsg) { DeleteMsg delMsg = (DeleteMsg) msg; clEntry = createChangelogEntry(eclmsg.getServiceId(), eclmsg .getCookie().toString(), DN.decode(delMsg.getDn()), delMsg.getChangeNumber(), null, // no changes delMsg.getEntryUUID(), delMsg.getEclIncludes(), // entry attributes eclmsg.getDraftChangeNumber(), "delete", delMsg.getInitiatorsName()); } return clEntry; } /** * Creates the root entry of the external changelog. * @param hasSubordinates whether the root entry has subordinates or not. * @return The root entry created. */ private Entry createRootEntry(boolean hasSubordinates) { // Attributes. HashMap<AttributeType,List<Attribute>> userAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); HashMap<AttributeType,List<Attribute>> operationalAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); // CN. AttributeType aType = DirectoryServer.getAttributeType(ATTR_COMMON_NAME); if (aType == null) aType = DirectoryServer.getDefaultAttributeType(ATTR_COMMON_NAME); Attribute a = Attributes.create(ATTR_COMMON_NAME, "changelog"); List<Attribute> attrList = Collections.singletonList(a); userAttrs.put(aType, attrList); // subSchemaSubentry aType = DirectoryServer.getAttributeType(ATTR_SUBSCHEMA_SUBENTRY_LC); if (aType == null) aType = DirectoryServer.getDefaultAttributeType(ATTR_SUBSCHEMA_SUBENTRY); a = Attributes.create(ATTR_SUBSCHEMA_SUBENTRY, ConfigConstants.DN_DEFAULT_SCHEMA_ROOT); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else userAttrs.put(aType, attrList); // TODO:numSubordinates // hasSubordinates aType = DirectoryServer.getAttributeType("hassubordinates"); if (aType == null) aType = DirectoryServer.getDefaultAttributeType("hasSubordinates"); a = Attributes.create("hasSubordinates", Boolean.toString(hasSubordinates)); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else userAttrs.put(aType, attrList); // entryDN aType = DirectoryServer.getAttributeType("entrydn"); if (aType == null) aType = DirectoryServer.getDefaultAttributeType("entryDN"); a = Attributes.create("entryDN", CHANGELOG_ROOT_DN.toNormalizedString()); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else userAttrs.put(aType, attrList); Entry e = new Entry(CHANGELOG_ROOT_DN, CHANGELOG_ROOT_OBJECT_CLASSES, userAttrs, operationalAttrs); return e; } /** * Create an ECL entry from a set of provided information. This is the part * of entry creation common to all types of msgs (ADD, DEL, MOD, MODDN). * * @param serviceID The provided cookie value. * @param cookie The provided cookie value. * @param targetDN The provided targetDN. * @param changeNumber The provided replication changeNumber. * @param clearLDIFchanges The provided LDIF changes for ADD and MODIFY * @param targetUUID The provided targetUUID. * @param includedAttributes The provided attributes to include * @param draftChangenumber The provided draft change number (integer) * @param changetype The provided change type (add, ...) * @param changeInitiatorsName The provided initiators name * @return The created ECL entry. * @throws DirectoryException * When any error occurs. */ private static Entry createChangelogEntry( String serviceID, String cookie, DN targetDN, ChangeNumber changeNumber, String clearLDIFchanges, String targetUUID, List<RawAttribute> includedAttributes, int draftChangenumber, String changetype, String changeInitiatorsName) throws DirectoryException { AttributeType aType; String dnString; if (draftChangenumber == 0) { // Draft uncompat mode dnString = "replicationCSN=" + changeNumber + "," + serviceID + "," + ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT; } else { // Draft compat mode dnString = "changeNumber=" + draftChangenumber + "," + ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT; } // Objectclass HashMap<AttributeType,List<Attribute>> uAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); HashMap<AttributeType,List<Attribute>> operationalAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); // Operational standard attributes // subSchemaSubentry aType = DirectoryServer.getAttributeType(ATTR_SUBSCHEMA_SUBENTRY_LC); if (aType == null) aType = DirectoryServer .getDefaultAttributeType(ATTR_SUBSCHEMA_SUBENTRY_LC); Attribute a = Attributes.create(aType, ConfigConstants.DN_DEFAULT_SCHEMA_ROOT); List<Attribute> attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // numSubordinates aType = DirectoryServer.getAttributeType("numsubordinates"); if (aType == null) aType = DirectoryServer.getDefaultAttributeType("numSubordinates"); a = Attributes.create(aType, "0"); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // hasSubordinates aType = DirectoryServer.getAttributeType("hassubordinates"); if (aType == null) aType = DirectoryServer.getDefaultAttributeType("hasSubordinates"); a = Attributes.create(aType, "false"); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // entryDN aType = DirectoryServer.getAttributeType("entrydn"); if (aType == null) aType = DirectoryServer.getDefaultAttributeType("entryDN"); a = Attributes.create(aType, dnString); attrList = Collections.singletonList(a); if (aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // REQUIRED attributes // ECL Changelog draft change number if((aType = DirectoryServer.getAttributeType("changenumber")) == null) aType = DirectoryServer.getDefaultAttributeType("changeNumber"); a = Attributes.create(aType, String.valueOf(draftChangenumber)); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // if((aType = DirectoryServer.getAttributeType("changetime")) == null) aType = DirectoryServer.getDefaultAttributeType("changeTime"); SimpleDateFormat dateFormat; dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // ?? a = Attributes.create(aType, dateFormat.format(new Date(changeNumber.getTime()))); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // if((aType = DirectoryServer.getAttributeType("changetype")) == null) aType = DirectoryServer.getDefaultAttributeType("changeType"); a = Attributes.create(aType, changetype); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // if((aType = DirectoryServer.getAttributeType("targetdn")) == null) aType = DirectoryServer.getDefaultAttributeType("targetDN"); a = Attributes.create(aType, targetDN.toNormalizedString()); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // NON REQUESTED attributes if((aType = DirectoryServer.getAttributeType("replicationcsn")) == null) aType = DirectoryServer.getDefaultAttributeType("replicationCSN"); a = Attributes.create(aType, changeNumber.toString()); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); // if((aType = DirectoryServer.getAttributeType("replicaidentifier")) == null) aType = DirectoryServer.getDefaultAttributeType("replicaIdentifier"); a = Attributes.create(aType, Integer.toString(changeNumber.getServerId())); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); if (clearLDIFchanges != null) { if ((aType = DirectoryServer.getAttributeType("changes")) == null) { aType = DirectoryServer.getDefaultAttributeType("changes"); } a = Attributes.create(aType, clearLDIFchanges); // force base64 attrList = new ArrayList<Attribute>(1); attrList.add(a); if (aType.isOperational()) { operationalAttrs.put(aType, attrList); } else { uAttrs.put(aType, attrList); } } if (changeInitiatorsName != null) { if ((aType = DirectoryServer .getAttributeType("changeinitiatorsname")) == null) { aType = DirectoryServer .getDefaultAttributeType("changeInitiatorsName"); } a = Attributes.create(aType, changeInitiatorsName); attrList = new ArrayList<Attribute>(1); attrList.add(a); if (aType.isOperational()) { operationalAttrs.put(aType, attrList); } else { uAttrs.put(aType, attrList); } } if (targetUUID != null) { if((aType = DirectoryServer.getAttributeType("targetentryuuid")) == null) aType = DirectoryServer.getDefaultAttributeType("targetEntryUUID"); a = Attributes.create(aType, targetUUID); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); } if((aType = DirectoryServer.getAttributeType("changelogcookie")) == null) aType = DirectoryServer.getDefaultAttributeType("changeLogCookie"); a = Attributes.create(aType, cookie); attrList = new ArrayList<Attribute>(1); attrList.add(a); if(aType.isOperational()) operationalAttrs.put(aType, attrList); else uAttrs.put(aType, attrList); if (includedAttributes != null && !includedAttributes.isEmpty()) { StringBuilder builder = new StringBuilder(256); for (RawAttribute includedAttribute : includedAttributes) { String name = includedAttribute.getAttributeType(); for (ByteString value : includedAttribute.getValues()) { builder.append(name); appendLDIFSeparatorAndValue(builder, value); builder.append('\n'); } } String includedAttributesLDIF = builder.toString(); if ((aType = DirectoryServer .getAttributeType("includedattributes")) == null) { aType = DirectoryServer .getDefaultAttributeType("includedAttributes"); } a = Attributes.create(aType, includedAttributesLDIF); attrList = new ArrayList<Attribute>(1); attrList.add(a); if (aType.isOperational()) { operationalAttrs.put(aType, attrList); } else { uAttrs.put(aType, attrList); } } // at the end build the CL entry to be returned Entry cle = new Entry( DN.decode(dnString), CHANGELOG_ENTRY_OBJECT_CLASSES, uAttrs, operationalAttrs); return cle; } /** * {@inheritDoc} */ @Override public CancelResult cancel(CancelRequest cancelRequest) { if (debugEnabled()) TRACER.debugInfo(this + " cancel() " + eclSession); if (eclSession != null) { try { eclSession.close(); } catch(Exception e){} } return super.cancel(cancelRequest); } /** * {@inheritDoc} */ @Override public void abort(CancelRequest cancelRequest) { if (debugEnabled()) TRACER.debugInfo(this + " abort() " + eclSession); if (eclSession != null) { try { eclSession.close(); } catch(Exception e){} } } /** * Traverse the provided search filter, looking for some conditions * on attributes that can be optimized in the ECL. * When found, populate the provided StartECLSessionMsg. * @param startCLmsg the startCLMsg to be populated. * @param baseDN the provided search baseDN. * @param sf the provided search filter. * @throws DirectoryException when an exception occurs. */ public static void evaluateSearchParameters(StartECLSessionMsg startCLmsg, DN baseDN, SearchFilter sf) throws DirectoryException { // Select whether to use the DN or the filter. switch (baseDN.getNumComponents()) { case 1: // cn=changelog - use user provided search filter. break; case 2: // changeNumber=xxx,cn=changelog - draft ECL - use faked up equality // filter. // The DN could also be a new ECL <service-id>,cn=changelog so be sure it // is draft ECL. RDN rdn = baseDN.getRDN(); AttributeType at = DirectoryServer.getAttributeType("changenumber"); if (at == null) { at = DirectoryServer.getDefaultAttributeType("changeNumber"); } AttributeValue av = rdn.getAttributeValue(at); if (av != null) { sf = SearchFilter.createEqualityFilter(at, av); } break; default: // replicationCSN=xxx,<service-id>,cn=changelog - new ECL - use faked up // equality filter. rdn = baseDN.getRDN(); at = DirectoryServer.getAttributeType("replicationcsn"); if (at == null) { at = DirectoryServer.getDefaultAttributeType("replicationCSN"); } av = rdn.getAttributeValue(at); if (av != null) { sf = SearchFilter.createEqualityFilter(at, av); } break; } StartECLSessionMsg msg = evaluateSearchParameters2(sf); startCLmsg.setFirstDraftChangeNumber(msg.getFirstDraftChangeNumber()); startCLmsg.setLastDraftChangeNumber(msg.getLastDraftChangeNumber()); startCLmsg.setChangeNumber(msg.getChangeNumber()); } private static StartECLSessionMsg evaluateSearchParameters2(SearchFilter sf) throws DirectoryException { StartECLSessionMsg startCLmsg = new StartECLSessionMsg(); startCLmsg.setFirstDraftChangeNumber(-1); startCLmsg.setLastDraftChangeNumber(-1); startCLmsg.setChangeNumber(new ChangeNumber(0,0,(short)0)); // If there's no filter, just return if (sf == null) { return startCLmsg; } // Here are the 3 elementary cases we know how to optimize if ((sf.getFilterType() == FilterType.GREATER_OR_EQUAL) && (sf.getAttributeType() != null) && (sf.getAttributeType().getPrimaryName(). equalsIgnoreCase("changeNumber"))) { int sn = Integer.decode( sf.getAssertionValue().getNormalizedValue().toString()); startCLmsg.setFirstDraftChangeNumber(sn); return startCLmsg; } else if ((sf.getFilterType() == FilterType.LESS_OR_EQUAL) && (sf.getAttributeType() != null) && (sf.getAttributeType().getPrimaryName(). equalsIgnoreCase("changeNumber"))) { int sn = Integer.decode( sf.getAssertionValue().getNormalizedValue().toString()); startCLmsg.setLastDraftChangeNumber(sn); return startCLmsg; } else if ((sf.getFilterType() == FilterType.EQUALITY) && (sf.getAttributeType() != null) && (sf.getAttributeType().getPrimaryName(). equalsIgnoreCase("replicationcsn"))) { // == exact changenumber ChangeNumber cn = new ChangeNumber(sf.getAssertionValue().toString()); startCLmsg.setChangeNumber(cn); return startCLmsg; } else if ((sf.getFilterType() == FilterType.EQUALITY) && (sf.getAttributeType() != null) && (sf.getAttributeType().getPrimaryName(). equalsIgnoreCase("changenumber"))) { int sn = Integer.decode( sf.getAssertionValue().getNormalizedValue().toString()); startCLmsg.setFirstDraftChangeNumber(sn); startCLmsg.setLastDraftChangeNumber(sn); return startCLmsg; } else if (sf.getFilterType() == FilterType.AND) { // Here is the only binary operation we know how to optimize Collection<SearchFilter> comps = sf.getFilterComponents(); SearchFilter sfs[] = comps.toArray(new SearchFilter[0]); int l1 = -1; int f1 = -1; int l2 = -1; int f2 = -1; StartECLSessionMsg m1; StartECLSessionMsg m2; if (sfs.length > 0) { m1 = evaluateSearchParameters2(sfs[0]); l1 = m1.getLastDraftChangeNumber(); f1 = m1.getFirstDraftChangeNumber(); } if (sfs.length > 1) { m2 = evaluateSearchParameters2(sfs[1]); l2 = m2.getLastDraftChangeNumber(); f2 = m2.getFirstDraftChangeNumber(); } if (l1 == -1) startCLmsg.setLastDraftChangeNumber(l2); else if (l2 == -1) startCLmsg.setLastDraftChangeNumber(l1); else startCLmsg.setLastDraftChangeNumber(Math.min(l1,l2)); startCLmsg.setFirstDraftChangeNumber(Math.max(f1,f2)); return startCLmsg; } else { return startCLmsg; } } }