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