/* * 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); } }