001package com.pingidentity.ds.plugin;
002
003import com.unboundid.directory.sdk.common.operation.AddRequest;
004import com.unboundid.directory.sdk.common.operation.DeleteRequest;
005import com.unboundid.directory.sdk.common.operation.ModifyDNRequest;
006import com.unboundid.directory.sdk.common.operation.ModifyRequest;
007import com.unboundid.directory.sdk.common.operation.*;
008import com.unboundid.directory.sdk.common.types.CompletedOperationContext;
009import com.unboundid.directory.sdk.common.types.LogSeverity;
010import com.unboundid.directory.sdk.common.types.RegisteredMonitorProvider;
011import com.unboundid.directory.sdk.ds.api.Plugin;
012import com.unboundid.directory.sdk.ds.config.PluginConfig;
013import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
014import com.unboundid.directory.sdk.ds.types.PostResponsePluginResult;
015import com.unboundid.ldap.sdk.*;
016import com.unboundid.util.StaticUtils;
017import com.unboundid.util.args.*;
018
019import java.io.File;
020import java.io.FileWriter;
021import java.io.IOException;
022import java.text.SimpleDateFormat;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Date;
026import java.util.List;
027
028public class SnapshotChangelog extends Plugin
029{
030    public static final String OBJECT_CLASS = "objectClass";
031    public static final String CHANGELOG_PREFIX = "snapshot-changelog-";
032    public static final String CHANGE_NUMBER_ATTRIBUTE_TYPE = "scl-change-number";
033    private static final String ARG_NAME_BASE_DN = "base-dn";
034    private static final String ARG_BASE_DN_DEFAULT = "cn=snapshot-changelog";
035    private static final String ARG_NAME_SPECIAL_ATTRIBUTE = "do-not-log-if-modify-only-attributes";
036    private static final String ARG_NAME_FILTER_ENTRY_BEFORE = "filter-entry-before";
037    private static final String ARG_NAME_FILTER_ENTRY_AFTER = "filter-entry-after";
038    private static final String ARG_NAME_INCLUDE_ATTRIBUTE = "include-attribute";
039    private static final String ARG_NAME_EXCLUDE_ATTRIBUTE = "exclude-attribute";
040    private static final String ATTRIBUTE_ENTRY_AFTER = "scl-entry-after";
041    private static final String ATTRIBUTE_ENTRY_BEFORE = "scl-entry-before";
042    private static final List<File> DEFAULT_DIRECTORY = Arrays.asList(new File("logs/snapshot-changelog"));
043    private static final String ARG_FAILED_PERSISTENCE_FOLDER = "failed-persistence-folder";
044    private static final String ARG_FAILED_PERSISTENCE_FORMAT = "failed-persistence-date-format";
045    private static final String ARG_FAILED_PERSISTENCE_USE_FORMATTED_DATE = "failed-persistence-use-formatted-date";
046    private List<String> specialAttributes;
047    private List<String> includeAttributes;
048    private List<String> excludeAttributes;
049    private List<Filter> entryBeforeFilters;
050    private List<Filter> entryAfterFilters;
051    private List<DN> publicBackends;
052    private SnapshotChangelogMonitorProvider monitor;
053    private RegisteredMonitorProvider registeredMonitorProvider;
054    private Thread persisterThread;
055    private String ARG_PERSISTENCE_FAILED_FORMAT_DEFAULT = "yyyyMMdd-HHmmss.SSS";
056    DirectoryServerContext serverContext;
057    DN baseDN;
058    SnapshotChangeNumberPersister persister;
059    PluginConfig config;
060    private File failedPersistPath;
061    private SimpleDateFormat failedPersistDateFormat;
062    private boolean failedPersistUseDate;
063
064    public SnapshotChangelog() {
065    }
066
067    public void defineConfigArguments(ArgumentParser parser) throws ArgumentException {
068        try {
069            parser.addArgument(new DNArgument((Character)null, "base-dn", true, 1, "{DN}", "The base DN where the enhanced changelog should be written (Default: cn=snapshot-changelog)", new DN("cn=snapshot-changelog")));
070        } catch (LDAPException var9) {
071            var9.printStackTrace();
072        }
073        
074        StringArgument includeAttributesArgument = new StringArgument(null, ARG_NAME_INCLUDE_ATTRIBUTE, false, 0,
075                "{attribute-name}", "zero or more attributes to explicitly whitelist. All other attributes will be " +
076                "removed from the entries committed in this changelog.");
077        parser.addArgument(includeAttributesArgument);
078        
079        StringArgument excludeAttributesArgument = new StringArgument(null, ARG_NAME_EXCLUDE_ATTRIBUTE, false, 0,
080                "{attribute-name}", "zero or more attributes to explicitly blacklist. These attributes will be " +
081                "removed from the entries committed to this changelog");
082        parser.addArgument(excludeAttributesArgument);
083        
084        StringArgument specialAttributesArgument = new StringArgument(null, ARG_NAME_SPECIAL_ATTRIBUTE, false, 0,
085                "{attribute-name}", "zero or more special attributes. Special attributes are attributes that may be " +
086                "included in the changelog only if other attributes of interest are present. This is only evaluated " +
087                "for modify operations.");
088        parser.addArgument(specialAttributesArgument);
089        
090        FilterArgument preEntryIncludeFilterArgument = new FilterArgument(null, ARG_NAME_FILTER_ENTRY_BEFORE, false,
091                0, "{ldap-filter}", "zero or more filters that the entry prior to the modification(s) must match for " +
092                "the change to be committed to the changelog. This is not evaluated for add operations.");
093        parser.addArgument(preEntryIncludeFilterArgument);
094        
095        FilterArgument postEntryFilterArgument = new FilterArgument(null, ARG_NAME_FILTER_ENTRY_AFTER, false, 0,
096                "{ldap-filter}", "zero or more filters that the entry after the modification(s) must match for the " +
097                "change to be committed to the changelog. This is not evaluated for delete operations.");
098        parser.addArgument(postEntryFilterArgument);
099        FileArgument failedPersistenceFolder = new FileArgument(null, ARG_FAILED_PERSISTENCE_FOLDER, false, 1, "{path}", "The path to write snapshot entries in case there is an issue writing to the changelog", true, true, false, false, DEFAULT_DIRECTORY);
100        parser.addArgument(failedPersistenceFolder);
101        StringArgument failedPersistenceFormatArgument = new StringArgument(null, ARG_FAILED_PERSISTENCE_FORMAT, false, 1, "{SimpleDataFormat}", "The Date format for the persistence failed file name suffix", ARG_PERSISTENCE_FAILED_FORMAT_DEFAULT);
102        failedPersistenceFormatArgument.addValueValidator(new ArgumentValueValidator() {
103            public void validateArgumentValue(Argument argument, String argumentValue) throws ArgumentException {
104                try {
105                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(argumentValue);
106                    simpleDateFormat.format(new Date());
107                } catch (IllegalArgumentException iae) {
108                    throw new ArgumentException(iae.getMessage());
109                }
110            }
111        });
112        parser.addArgument(failedPersistenceFormatArgument);
113        parser.addArgument(new BooleanArgument(null, ARG_FAILED_PERSISTENCE_USE_FORMATTED_DATE, "By default the failed persistence file will be appended with a suffix equal to the changelog number. This option allows to override this behaviour to append a formatted timestamp instead"));
114    }
115    
116    /**
117     * Performs the necessary processing to ensure that configuration is properly applied to the instance of the
118     * extension
119     * This is used to update configuration without restarting the plugin if possible
120     *
121     * @param config               the configuration object of the instance of the extension
122     * @param parser               the argument parser
123     * @param adminActionsRequired a list of messages describing reason administrative actions necessary to apply
124     *                             configuration
125     * @param messages             a list of message providing details about the application of the configuration to
126     *                             the instance
127     * @return SUCCESS if the configuration could be applied
128     */
129    @Override
130    public ResultCode applyConfiguration(PluginConfig config, ArgumentParser parser, List<String>
131            adminActionsRequired, List<String> messages)
132    {
133        specialAttributes = parser.getStringArgument(ARG_NAME_SPECIAL_ATTRIBUTE).getValues();
134        includeAttributes = parser.getStringArgument(ARG_NAME_INCLUDE_ATTRIBUTE).getValues();
135        excludeAttributes = parser.getStringArgument(ARG_NAME_EXCLUDE_ATTRIBUTE).getValues();
136        
137        entryBeforeFilters = parser.getFilterArgument(ARG_NAME_FILTER_ENTRY_BEFORE).getValues();
138        entryAfterFilters = parser.getFilterArgument(ARG_NAME_FILTER_ENTRY_AFTER).getValues();
139        failedPersistPath = parser.getFileArgument(ARG_FAILED_PERSISTENCE_FOLDER).getValue();
140        failedPersistDateFormat = new SimpleDateFormat(parser.getStringArgument(ARG_FAILED_PERSISTENCE_FORMAT).getValue());
141        failedPersistUseDate = parser.getBooleanArgument(ARG_FAILED_PERSISTENCE_USE_FORMATTED_DATE).isPresent();
142
143        baseDN = parser.getDNArgument(ARG_NAME_BASE_DN).getValue();
144        return ResultCode.SUCCESS;
145    }
146    
147    /**
148     * Performs the necessary processing to initialize the instance of the extension
149     *
150     * @param serverContext the server context
151     * @param config        the configuration object of the instance of the extension
152     * @param parser        the argument parser
153     * @throws LDAPException if initialization was not successful
154     */
155    @Override
156    public void initializePlugin(DirectoryServerContext serverContext, PluginConfig config, ArgumentParser parser)
157            throws LDAPException
158    {
159        this.serverContext = serverContext;
160        this.config = config;
161        
162        publicBackends = new ArrayList<>();
163        for (String namingContext : serverContext.getInternalRootConnection().getRootDSE().getNamingContextDNs())
164        {
165            publicBackends.add(new DN(namingContext));
166        }
167        
168        // Use the applyConfiguration method to set up the plugin.
169        final ArrayList<String> adminActionsRequired = new ArrayList<>(5);
170        final ArrayList<String> messages = new ArrayList<>(5);
171        final ResultCode resultCode = applyConfiguration(config, parser, adminActionsRequired, messages);
172        if (resultCode != ResultCode.SUCCESS)
173        {
174            throw new LDAPException(resultCode,
175                    "One or more errors occurred while trying to initialize  " + getExtensionName() + " extension" +
176                            "  in '"
177                            + config.getConfigObjectDN() + ":  " +
178                            StaticUtils.concatenateStrings(messages));
179        }
180        
181        this.monitor = new SnapshotChangelogMonitorProvider(this);
182        registeredMonitorProvider = serverContext.registerMonitorProvider(monitor, config);
183        
184    
185        this.persister = new SnapshotChangeNumberPersister(this);
186        persisterThread = serverContext.createThread(persister, "Snapshot Changelog " +
187                "Persister Thread");
188        persisterThread.start();
189    }
190    
191    /**
192     * Performs the necessary processing to gracefully shutdown the instance of the extension by committing state to the
193     * enhanced changelog backend
194     */
195    @Override
196    public void finalizePlugin()
197    {
198        persister.shutdown();
199        serverContext.deregisterMonitorProvider(registeredMonitorProvider);
200    }
201    
202    /**
203     * Performs the necessary processing to generate the extension name
204     *
205     * @return the extension name
206     */
207    @Override
208    public String getExtensionName()
209    {
210        return "Snapshot Changelog";
211    }
212    
213    /**
214     * Performs the necessary
215     *
216     * @return an array of descriptive strings about the extension, each of which appears as a paragraph
217     */
218    @Override
219    public String[] getExtensionDescription()
220    {
221        return new String[]{"In a nutshell, this changelog provides the full entry before and after change."};
222    }
223    
224    /**
225     * Performs the necessary processing to evaluate if a changelog entry should be committed to the snapshot changelog
226     * for an add operation
227     *
228     * @param operationContext the operation context
229     * @param request          the request to evaluate
230     * @param result           the result that was sent to the client
231     * @return SUCCESS
232     */
233    @Override
234    public PostResponsePluginResult doPostResponse(CompletedOperationContext operationContext, AddRequest request,
235                                                   AddResult result)
236    {
237        if (accept(result) && notInSnapshotChangelog(request.getEntry().getDN()))
238        {
239            SearchResultEntry entry = null;
240            try
241            {
242                entry = serverContext.getInternalRootConnection().getEntry(request.getEntry().getDN(),"*","+");
243            } catch (LDAPException e)
244            {
245                debug(e);
246            }
247            Entry candidate = ( entry != null ? entry : request.getEntry().toLDAPSDKEntry() );
248            if (entryMatchesAny(candidate, entryAfterFilters))
249            {
250                processChangelogEntry(null, candidate, ChangeType.ADD);
251            }
252        }
253        return PostResponsePluginResult.SUCCESS;
254    }
255    
256    /**
257     * This method catches ADD received via replication
258     *
259     * @param operationContext the operation context
260     * @param request          the request to evaluate
261     * @param result           the result of the ADD
262     */
263    @Override
264    public void doPostReplication(CompletedOperationContext operationContext, AddRequest request, AddResult result)
265    {
266        doPostResponse(operationContext, request, result);
267    }
268    
269    /**
270     * Performs the necessary processing to evaluate if a changelog entry should be committed to the snapshot changelog
271     * for a delete operation
272     *
273     * @param operationContext the operation context
274     * @param request          the request to evaluate
275     * @param result           the result that was sent to the client
276     * @return SUCCESS
277     */
278    @Override
279    public PostResponsePluginResult doPostResponse(CompletedOperationContext operationContext, DeleteRequest
280            request,
281                                                   DeleteResult result)
282    {
283        if (accept(result) && notInSnapshotChangelog(request.getDN()))
284        {
285            Entry candidate = result.getDeletedEntry().toLDAPSDKEntry();
286            if (entryMatchesAny(candidate, entryBeforeFilters))
287            {
288                processChangelogEntry(candidate, null, ChangeType.DELETE);
289            }
290        }
291        return PostResponsePluginResult.SUCCESS;
292    }
293    
294    /**
295     * This method catches DELETE requests received via replication
296     *
297     * @param operationContext the operation context
298     * @param request          the replicated DELETE request
299     * @param result           the result
300     */
301    @Override
302    public void doPostReplication(CompletedOperationContext operationContext, DeleteRequest request, DeleteResult
303            result)
304    {
305        doPostResponse(operationContext, request, result);
306    }
307    
308    /**
309     * Performs the necessary processing to evaluate if a changelog entry should be committed to the snapshot changelog
310     * for a modify operation
311     *
312     * @param operationContext the operation context
313     * @param request          the request to evaluate
314     * @param result           the result that was sent to the client
315     * @return SUCCESS
316     */
317    @Override
318    public PostResponsePluginResult doPostResponse(CompletedOperationContext operationContext, ModifyRequest
319            request, ModifyResult result)
320    {
321        if (accept(result) && notInSnapshotChangelog(request.getDN()) && !onlySpecialAttributesLeft(request.getModifications()))
322        {
323            Entry candidateBefore = result.getOldEntry().toLDAPSDKEntry();
324            Entry candidateAfter = null;
325            try {
326                candidateAfter = serverContext.getInternalRootConnection().getEntry(result.getNewEntry().getDN());
327            } catch (LDAPException e) {
328                e.printStackTrace();
329            }
330            if (filterMatchForMod(candidateBefore,candidateAfter))
331            {
332                processChangelogEntry(candidateBefore, candidateAfter, ChangeType.MODIFY);
333            }
334        }
335        return PostResponsePluginResult.SUCCESS;
336    }
337
338    /**
339     * This method catches MODIFY operations received via replication
340     *
341     * @param operationContext the operation context
342     * @param request          the replicated MODIFY request
343     * @param result           the result
344     */
345    @Override
346    public void doPostReplication(CompletedOperationContext operationContext, ModifyRequest request, ModifyResult
347            result)
348    {
349        doPostResponse(operationContext, request, result);
350    }
351    
352    /**
353     * Performs the necessary processing to evaluate if a changelog entry should be committed to the snapshot changelog
354     * for a modify dn operation
355     *
356     * @param operationContext the operation context
357     * @param request          the request to evaluate
358     * @param result           the result that was sent to the client
359     * @return SUCCESS
360     */
361    @Override
362    public PostResponsePluginResult doPostResponse(CompletedOperationContext operationContext, ModifyDNRequest
363            request, ModifyDNResult result)
364    {
365        if (accept(result) && notInSnapshotChangelog(request.getDN()))
366        {
367            Entry candidateBefore = result.getOldEntry().toLDAPSDKEntry();
368            Entry candidateAfter = result.getNewEntry().toLDAPSDKEntry();
369            if (filterMatchForMod(candidateBefore,candidateAfter))
370            {
371                processChangelogEntry(candidateBefore, candidateAfter, ChangeType.MODIFY_DN);
372            }
373        }
374        return PostResponsePluginResult.SUCCESS;
375    }
376    
377    @Override
378    public void doPostReplication(CompletedOperationContext operationContext, ModifyDNRequest request, ModifyDNResult
379            result)
380    {
381        doPostResponse(operationContext, request, result);
382    }
383    
384    /**
385     * This method handles the special case of modifications where unlike for other operations, both filters before and
386     * after might be use to select whether to commit the entry to the snapshot changelog
387     *
388     * If only the before or after is defined, then either can match -- or rather whichever is define must match
389     * If both the before and after are defined, then both must match
390     * @param entryBefore the candidate entry before
391     * @param entryAfter the candidate entry after
392     * @return true if the operation should result in an entry being committed to the changelog backend
393     */
394    private boolean filterMatchForMod(Entry entryBefore, Entry entryAfter)
395    {
396        boolean evaluateBeforeFilters = entryBeforeFilters != null && entryBeforeFilters.size() > 0;
397        boolean evaluateAfterFilters = entryAfterFilters != null && entryAfterFilters.size() > 0;
398        if (!(evaluateBeforeFilters || evaluateAfterFilters))
399        {
400            // neither filters are defined
401            return true;
402        } else if ( evaluateBeforeFilters && evaluateAfterFilters )
403        {
404            // both filters are defined
405            return entryMatchesAny(entryBefore, entryBeforeFilters) && entryMatchesAny(entryAfter, entryAfterFilters);
406        } else if ( evaluateBeforeFilters )
407        {
408            //only the before filters are defined
409            return entryMatchesAny(entryBefore, entryBeforeFilters);
410        } else if ( evaluateAfterFilters )
411        {
412            // only the after filters are defined
413            return entryMatchesAny(entryAfter, entryAfterFilters);
414        }
415        // never here
416        return true;
417    }
418    
419    /**
420     * creates the changelog entry
421     *
422     * @param entryBefore the contents of the entry prior to the operation
423     * @param entryAfter  the contents of the entry after the operation
424     * @param changeType  the operation type
425     */
426    private void processChangelogEntry(final Entry entryBefore, final Entry entryAfter, final ChangeType
427            changeType)
428    {
429        if (entryBefore == null && entryAfter == null)
430        {
431            return;
432        }
433        
434        Entry changelogEntry = new Entry(DN.NULL_DN);
435        changelogEntry.addAttribute(OBJECT_CLASS, CHANGELOG_PREFIX + changeType);
436        if (entryBefore != null)
437        {
438            Entry entry = sanitizeEntry(entryBefore);
439            changelogEntry.addAttribute(ATTRIBUTE_ENTRY_BEFORE, String.join("\n", entry.toLDIFString()));
440        }
441        if (entryAfter != null)
442        {
443            Entry entry = sanitizeEntry(entryAfter);
444            changelogEntry.addAttribute(ATTRIBUTE_ENTRY_AFTER, String.join("\n", entry.toLDIFString()));
445        }
446        commitEntry(changelogEntry);
447    }
448    
449    /**
450     * This method checks whether the entry only has attributes remaining that are special attribute
451     * if that is the case then the entry should not result in a commit to the changelog
452     *
453     * @param modifications the list of modifications in the operation
454     * @return true if after removing all special attributes the operation has affected no attributes of interest
455     */
456    private boolean onlySpecialAttributesLeft(final List<Modification> modifications)
457    {
458        final List<String> attributes = new ArrayList<>();
459        for (Modification m : modifications)
460        {
461            // there are include attributes white listed
462            if (includeAttributes != null && includeAttributes.size() > 0 && includeAttributes.stream().noneMatch(m
463                    .getAttributeName()
464                    ::equalsIgnoreCase))
465            {
466                // it is not one of them
467                return false;
468            }
469            
470            // there are exclude attribute black listed
471            if (excludeAttributes != null && excludeAttributes.size() > 0 && excludeAttributes.stream().anyMatch(m
472                    .getAttributeName()
473                    ::equalsIgnoreCase))
474            {
475                // it is one of them
476                continue;
477            }
478            
479            // there attributes in which we are only interested if something else of interest was altered
480            if (specialAttributes != null && specialAttributes.size() > 0 && specialAttributes.stream().anyMatch(m
481                    .getAttributeName()
482                    ::equalsIgnoreCase))
483            {
484                // it is one of them
485                continue;
486            }
487            attributes.add(m.getAttributeName());
488        }
489        return (attributes.size() == 0);
490    }
491    
492    /**
493     * Performs the necessary processing to ensure that an entry should be evaluated
494     *
495     * @param entry the entry to check
496     * @return true if it is under a public backend
497     */
498    private boolean entryInPublicBackend(final Entry entry)
499    {
500        for (DN publicBackend : publicBackends)
501        {
502            try
503            {
504                if (publicBackend.isAncestorOf(entry.getDN(), true))
505                {
506                    return true;
507                }
508            } catch (LDAPException e)
509            {
510                serverContext.debugCaught(e);
511            }
512        }
513        return false;
514    }
515    
516    /**
517     * Performs a check to avoid infinite looping by invoking the plugin for operations targeting itself
518     * @param dn the distinguished name to evaluate
519     * @return true if the entry is not in the snapshot changelog
520     */
521    private boolean notInSnapshotChangelog(final String dn)
522    {
523        try
524        {
525            if  ( baseDN != null && baseDN.isAncestorOf(dn,true) )
526            {
527                return false;
528            }
529        } catch (LDAPException e)
530        {
531            debug(e);
532        }
533        return true;
534    }
535    
536    /**
537     * Performs the necessary processing to verify that an entry matches any of the provided filters
538     * If no filters are provided, we consider it to be a match
539     *
540     * @param entry   the entry to evaluate
541     * @param filters the list of filters to assert against the entry
542     * @return true if no filters were provided or any of the provided filters match
543     */
544    private boolean entryMatchesAny(final Entry entry, final List<Filter> filters)
545    {
546        boolean result = true;
547        if (!entryInPublicBackend(entry))
548        {
549            return false;
550        }
551        if (filters != null && filters.size() > 0)
552        {
553            for (Filter filter : filters)
554            {
555                try
556                {
557                    if (filter.matchesEntry(entry))
558                    {
559                        return true;
560                    }
561                } catch (LDAPException e)
562                {
563                    debug(e);
564                    log(e.getDiagnosticMessage(),LogSeverity.SEVERE_ERROR);
565                }
566            }
567            result = false;
568        }
569        return result;
570    }
571    
572    /**
573     * Performs the necessary processing to compute the DN of a changelog entry
574     * @param changeNumber the change number of the entry for which to build the DN
575     * @return the DN string
576     */
577    public String getChangelogEntryDN(final Long changeNumber)
578    {
579        return CHANGE_NUMBER_ATTRIBUTE_TYPE + "=" +  changeNumber + "," +baseDN;
580    }
581
582    /**
583     * Commits the entry to the snapshot changelog backend and handles the possible exceptions
584     *
585     * @param changelogEntry the changelog entry to commit
586     */
587    private synchronized void commitEntry(final Entry changelogEntry)
588    {
589        Long changeNumber = persister.getNextChangeNumber();
590        try
591        {
592            changelogEntry.setDN(getChangelogEntryDN(changeNumber));
593            serverContext.getInternalRootConnection().add(changelogEntry);
594        } catch (LDAPException e)
595        {
596            File persisterFile = new File(failedPersistPath.getPath()+"."+(failedPersistUseDate ? failedPersistDateFormat.format(new Date()) : changeNumber.toString()));
597            try {
598                FileWriter fw = new FileWriter(persisterFile);
599                fw.write(changelogEntry.toLDIFString());
600                fw.flush();
601                fw.close();
602            } catch (IOException ioe) {
603                out("Could not persist changelog entry " + changelogEntry.getDN());
604                out(changelogEntry.toLDIFString());
605                serverContext.debugCaught(ioe);
606            }
607        }
608    }
609
610    /**
611     * Performs the necessary processing to sanitize an entry to only include attributes of interest
612     *
613     * @param entry             the original entry
614     * @return the resulting entry with the pared down list of attributes
615     */
616    private Entry sanitizeEntry(Entry entry)
617    {
618        Entry result = entry.duplicate();
619        boolean evaluateIncludes = includeAttributes != null && includeAttributes.size() > 0;
620        boolean evaluateExcludes = excludeAttributes != null && excludeAttributes.size() > 0;
621        
622        if (evaluateIncludes)
623        {
624            for (Attribute attribute : entry.getAttributes())
625            {
626                if (includeAttributes.stream().noneMatch(attribute.getBaseName()::equalsIgnoreCase))
627                {
628                    result.removeAttribute(attribute.getName());
629                }
630            }
631        }
632        if (evaluateExcludes)
633        {
634            for (String attributeType : excludeAttributes)
635            {
636                result.removeAttribute(attributeType);
637            }
638        }
639        
640        return result;
641    }
642    
643    
644    /**
645     * Performs the necessary processing to determine whether to accept processing the operation
646     * This is a check to try and bail early before most processing is done
647     * @param operationResult the operation to evaluate
648     * @return true if the operation should be processed
649     * @throws LDAPException
650     */
651    private boolean accept(final GenericResult operationResult)
652    {
653        boolean result = false;
654        if (ResultCode.SUCCESS.equals(operationResult.getResultCode()))
655        {
656            return true;
657        }
658        return result;
659    }
660    
661    /**
662     * Shorthand convenience method to log to server context
663     * @param message the message to log
664     * @param logSeverity the log severity to log with
665     */
666    private void log(String message, LogSeverity logSeverity)
667    {
668        if ( serverContext == null )
669        {
670            out(message);
671        } else {
672            serverContext.logMessage(logSeverity,message);
673        }
674    }
675    
676    /**
677     * Shorthand convenience method to catch throwables
678     * @param throwable the throwable caught
679     */
680    private void debug(Throwable throwable)
681    {
682        if ( serverContext == null )
683        {
684            out(throwable.getMessage());
685        } else {
686            serverContext.debugCaught(throwable);
687        }
688    }
689    
690    /**
691     * Shorthand utility method to print to STD OUT
692     * @param message the message to print
693     */
694    private void out(final String message)
695    {
696        System.out.println(message);
697    }
698}