UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExamplePolicyDecisionLogger.java

/*
 * 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
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * 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
 * docs/licenses/cddl.txt.  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
 *
 *
 *      Portions Copyright 2010-2024 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;

import com.unboundid.directory.sdk.common.api.DiskSpaceConsumer;
import com.unboundid.directory.sdk.broker.api.PolicyDecisionLogger;
import com.unboundid.directory.sdk.broker.config.PolicyDecisionLoggerConfig;
import com.unboundid.directory.sdk.broker.types.PolicyMessageType;
import com.unboundid.directory.sdk.common.types.RegisteredDiskSpaceConsumer;
import com.unboundid.directory.sdk.common.types.ServerContext;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.FileArgument;

import java.io.File;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
 * This class provides a simple example of a policy decision logger
 * that will append a message to a specified file about any decisions made.
 * <UL>
 *   <LI>log-file -- The path to the log file that will be written.  This must
 *       be provided.</LI>
 * </UL>
 */
public final class ExamplePolicyDecisionLogger
        extends PolicyDecisionLogger
        implements DiskSpaceConsumer
{
    /**
     * The name of the argument that will be used for the argument
     * used to specify the path to the log file.
     */
    private static final String ARG_NAME_LOG_FILE = "log-file";



    // The general configuration for this policy decision logger.
    private volatile PolicyDecisionLoggerConfig config;

    // The path to the log file to be written.
    private volatile File logFile;

    // The lock that will be used to synchronize logging activity.
    private final Object loggerLock;

    // The print writer that will be used to actually write the log messages.
    private volatile PrintWriter writer;

    // A counter used to keep track of how many messages have been logged.
    private AtomicLong messageCount;

    // The registered disk space consumer for this policy decision logger.
    private volatile RegisteredDiskSpaceConsumer registeredConsumer;

    // The server context for the server in which this extension is running.
    private ServerContext serverContext;

    /**
     * Creates a new instance of this policy decision logger.
     * All policy decision logger implementations must include a default
     * constructor, but any initialization should generally be done in the
     * {@code initializePolicyDecisionLogger} method.
     */
    public ExamplePolicyDecisionLogger() {
        loggerLock = new Object();
    }

    @Override
    public String getExtensionName() {
        return "Example Policy Decision Logger";
    }

    /**
     * Retrieves a human-readable description for this extension.  Each element
     * of the array that is returned will be considered a separate paragraph in
     * generated documentation.
     *
     * @return  A human-readable description for this extension, or {@code null}
     *          or an empty array if no description should be available.
     */
    @Override
    public String[] getExtensionDescription() {
        return new String[]
                {
                        "This example policy decision logger serves as an " +
                                "example that may be used to demonstrate the " +
                                "process for creating a third-party policy " +
                                "decision logger. It will write a line to a " +
                                "specified file for each policy decision log " +
                                "message generated by the server.",

                        "For simplicity, this example logger does not take " +
                                "care of all tasks that would be necessary " +
                                "for production use. For example, it does " +
                                "not include any rotation or retention " +
                                "functionality, so the file will grow " +
                                "without bounds. Further, while it is " +
                                "thread-safe, it does so using simple Java " +
                                "synchronized blocks, which is a " +
                                "simple way to achieve the necessary safety, " +
                                "but does not necessarily allow for the " +
                                "highest performance or concurrency.",
                };
    }

    /**
     * Updates the provided argument parser to define any configuration
     * arguments which may be used by this logger.  The argument parser may also
     * be updated to define relationships between arguments (e.g., to specify
     * required, exclusive, or dependent argument sets).
     *
     * @param  parser  The argument parser to be updated with the configuration
     *                 arguments which may be used by this logger.
     *
     * @throws  ArgumentException  If a problem is encountered while updating
     *                             the provided argument parser.
     */
    @Override
    public void defineConfigArguments(final ArgumentParser parser)
            throws ArgumentException
    {
        // Add an argument that allows you to specify the path to the log file.
        boolean   required        = true;
        int       maxOccurrences  = 1;
        String    placeholder     = "{path}";
        String    description     = "The path to the log file to be written.  "
                + "Non-absolute paths will be treated as relative to the " +
                "server root.";
        boolean   fileMustExist   = false;
        boolean   parentMustExist = true;
        boolean   mustBeFile      = true;
        boolean   mustBeDirectory = false;

        parser.addArgument(new FileArgument(null,
                ARG_NAME_LOG_FILE, required, maxOccurrences,
                placeholder, description, fileMustExist,
                parentMustExist, mustBeFile, mustBeDirectory));
    }

    /**
     * Initializes this logger.
     *
     * @param  serverContext  A handle to the server context for the server in
     *                        which this extension is running.
     * @param  config         The general configuration for this logger.
     * @param  parser         The argument parser which has been initialized
     *                        from the configuration for this logger.
     *
     * @throws  LDAPException  If a problem occurs while initializing this
     *                         logger.
     */
    @Override
    public void initializePolicyDecisionLogger(
            final ServerContext serverContext,
            final PolicyDecisionLoggerConfig config,
            final ArgumentParser parser)
            throws LDAPException {
        this.serverContext = serverContext;
        this.config = config;

        this.messageCount = new AtomicLong(0);

        try {
            FileArgument logFileArg = (FileArgument)
                    parser.getNamedArgument(ARG_NAME_LOG_FILE);
            logFile = logFileArg.getValue();

            writer = new PrintWriter(Files.newBufferedWriter(
                    logFile.toPath()), true);

            registeredConsumer = serverContext.registerDiskSpaceConsumer(this);
        } catch (Exception e) {
            throw new LDAPException(ResultCode.OTHER,
                    "Initialization error: " + e.getMessage(), e);
        }
    }





    /**
     * Indicates whether the provided configuration is acceptable for this
     * policy decision log publisher. It should be possible to call this
     * method on an uninitialized policy decision log publisher instance in
     * order to determine whether the trace log publisher would be able to
     * use the provided configuration.
     * <p>
     * Note that implementations which use a subclass of the provided
     * configuration class will likely need to cast the configuration to the
     * appropriate subclass type.
     * </p>
     *
     * @param config
     *          The policy decision log publisher configuration
     *          for which to make the determination.
     * @param unacceptableReasons
     *          A list that may be used to hold the reasons that the provided
     *          configuration is not acceptable.
     * @return {@code true} if the provided configuration is acceptable for this
     *         policy decision log publisher, or {@code false} if not.
     */
    @Override
    public boolean isConfigurationAcceptable(
            final PolicyDecisionLoggerConfig config,
            final ArgumentParser parser,
            final List<String> unacceptableReasons)
    {
        // The argument parser will handle all the necessary validation, so
        // we don't need to do anything here.
        return true;
    }

    /**
     * Attempts to apply the configuration contained in the provided argument
     * parser.
     *
     * @param  config                The general configuration for this
     *                               logger.
     * @param  parser                The argument parser which has been
     *                               initialized with the new configuration.
     * @param  adminActionsRequired  A list that can be updated with information
     *                               about any administrative actions
     *                               that may be required before one or more
     *                               of the configuration changes will be
     *                               applied.
     * @param  messages              A list that can be updated with information
     *                               about the result of applying the new
     *                               configuration.
     *
     * @return  A result code that provides information about the result of
     *          attempting to apply the configuration change.
     */
    @Override
    public ResultCode applyConfiguration(
            final PolicyDecisionLoggerConfig config,
            final ArgumentParser parser,
            final List<String> adminActionsRequired,
            final List<String> messages)
    {
        final PolicyDecisionLoggerConfig cfg = config;
        // Get the path to the log file from the new config.
        final FileArgument arg =
                (FileArgument) parser.getNamedArgument(ARG_NAME_LOG_FILE);
        final File newFile = arg.getValue();

        // If the log file path hasn't changed, then we're done.
        if (newFile.equals(logFile))
        {
            return ResultCode.SUCCESS;
        }

        // Create a print writer that can be used to write to the new log file.
        final PrintWriter newWriter;
        try
        {
            newWriter = new PrintWriter(Files.newBufferedWriter(
                    newFile.toPath()), true);
        }
        catch (final Exception e)
        {
            serverContext.debugCaught(e);
            messages.add(
                    "Unable to open new log file " + newFile.getAbsolutePath() +
                    " for writing:  " + StaticUtils.getExceptionMessage(e));
            return ResultCode.OTHER;
        }

        // Swap the new logger into place.
        final PrintWriter oldWriter;
        synchronized (loggerLock)
        {
            oldWriter = writer;
            writer = newWriter;
            logFile = newFile;
        }

        // Writer must be flushed prior to closing to ensure all data is
        // written.
        oldWriter.flush();

        // Close the old logger.
        oldWriter.close();

        this.config = cfg;

        return ResultCode.SUCCESS;
    }

    /**
     * Performs any cleanup which may be necessary when this logger is
     * to be taken out of service.
     */
    @Override
    public void finalizePolicyDecisionLogger() {
        synchronized (loggerLock) {
            if (registeredConsumer != null) {
                serverContext.deregisterDiskSpaceConsumer(registeredConsumer);
            }

            if (writer != null) {
                writer.close();
                writer = null;
            }
            logFile = null;
        }
    }


    /**
     * Logs a message.
     *
     * @param messageType
     *          The message type.
     * @param logContext
     *          A set of key/value pairs summarizing and providing context for
     *          the policy decision.
     * @param message
     *          The policy decision response. May be {@code null} for message
     *          types that do not record a policy decision response.
     */
    @Override
    public void log(final PolicyMessageType messageType,
                    final Map<String, String> logContext,
                    final String message) {
        final StringBuilder buffer = new StringBuilder();
        buffer.append(new Date());
        buffer.append(" ");
        buffer.append(messageType.toString());
        buffer.append(" ");
        buffer.append(message != null ? message : "null");
        logContext.forEach((key, value) -> {
            buffer.append(" ");
            buffer.append(key);
            buffer.append(" = ");
            buffer.append(value);
        });

        synchronized (loggerLock)
        {
            if (writer == null)
            {
                // This should only happen if the logger has been shut down.
                return;
            }

            writer.println(buffer);
        }

        if (messageCount != null)
        {
            messageCount.incrementAndGet();
        }
    }


    /**
     * Retrieves a map containing examples of configurations that
     * may be used for this extension.
     * <p>
     * The map key should be a list of sample arguments, and the corresponding
     * value should be a description of the behavior that will be
     * exhibited by the extension when used with that configuration.
     * </p>
     * @return  A map containing examples of configurations that may be used for
     *          this extension.  It may be {@code null} or empty if there should
     *          not be any example argument sets.
     */
    @Override
    public Map<List<String>, String> getExamplesArgumentSets() {
        final LinkedHashMap<List<String>, String> exampleMap =
                new LinkedHashMap<>(1);

        exampleMap.put(
                Collections.singletonList(ARG_NAME_LOG_FILE +
                        "=logs/example-policy-decision.log"),
                "Write PDP log messages to the logs/example-policy-decision" +
                        ".log file below the server root.");

        return exampleMap;
    }


    /**
     * Retrieves the name that should be used to identify this disk space
     * consumer.
     *
     * @return  The name used to identify this disk space consumer.
     */
    @Override
    public String getDiskSpaceConsumerName() {
        return "Example Policy Decision Logger " + config.getConfigObjectName();
    }

    /**
     * Retrieves a list of file paths in which this disk space consumer may
     * store files which may consume a significant amount of space.  It is
     * generally recommended that the paths be directories, but they may also be
     * individual files.
     *
     * @return  A list of filesystem paths in which this disk space consumer may
     *          store files which may consume a significant amount of space.
     */
    @Override
    public List<File> getDiskSpaceConsumerPaths() {
        return Collections.singletonList(logFile);
    }

}