/** Container for the language and body of a script. */ @Plugin(name = "Script", category = Node.CATEGORY, printObject = true) public class Script extends AbstractScript { private static final Logger logger = StatusLogger.getLogger(); public Script(String name, String language, String scriptText) { super(name, language, scriptText); } @PluginFactory public static Script createScript( // @formatter:off @PluginAttribute("name") final String name, @PluginAttribute("language") String language, @PluginValue("scriptText") final String scriptText) { // @formatter:on if (language == null) { logger.info("No script language supplied, defaulting to {}", DEFAULT_LANGUAGE); language = DEFAULT_LANGUAGE; } if (scriptText == null) { logger.error("No scriptText attribute provided for ScriptFile {}", name); return null; } return new Script(name, language, scriptText); } @Override public String toString() { return getName() != null ? getName() : super.toString(); } }
@AfterClass public static void cleanupClass() { System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); final LoggerContext ctx = LoggerContext.getContext(); ctx.reconfigure(); StatusLogger.getLogger().reset(); }
/** Represents a key/value pair in the configuration. */ @Plugin(name = "property", category = Node.CATEGORY, printObject = true) public final class Property { private static final Logger LOGGER = StatusLogger.getLogger(); private final String name; private final String value; private final boolean valueNeedsLookup; private Property(final String name, final String value) { this.name = name; this.value = value; this.valueNeedsLookup = value != null && value.contains("${"); } /** * Returns the property name. * * @return the property name. */ public String getName() { return name; } /** * Returns the property value. * * @return the value of the property. */ public String getValue() { return Objects.toString( value, Strings.EMPTY); // LOG4J2-1313 null would be same as Property not existing } /** * Returns {@code true} if the value contains a substitutable property that requires a lookup to be resolved. * @return {@code true} if the value contains {@code "${"}, {@code false} otherwise */ public boolean isValueNeedsLookup() { return valueNeedsLookup; } /** * Creates a Property. * * @param name The key. * @param value The value. * @return A Property. */ @PluginFactory public static Property createProperty( @PluginAttribute("name") final String name, @PluginValue("value") final String value) { if (name == null) { LOGGER.error("Property name cannot be null"); } return new Property(name, value); } @Override public String toString() { return name + '=' + getValue(); } }
/** * PathCondition that accepts paths after the accumulated file size threshold is exceeded during the * file tree walk. */ @Plugin(name = "IfAccumulatedFileSize", category = "Core", printObject = true) public final class IfAccumulatedFileSize implements PathCondition { private static final Logger LOGGER = StatusLogger.getLogger(); private final long thresholdBytes; private long accumulatedSize; private final PathCondition[] nestedConditions; private IfAccumulatedFileSize(final long thresholdSize, final PathCondition[] nestedConditions) { if (thresholdSize <= 0) { throw new IllegalArgumentException( "Count must be a positive integer but was " + thresholdSize); } this.thresholdBytes = thresholdSize; this.nestedConditions = nestedConditions == null ? new PathCondition[0] : Arrays.copyOf(nestedConditions, nestedConditions.length); } public long getThresholdBytes() { return thresholdBytes; } public List<PathCondition> getNestedConditions() { return Collections.unmodifiableList(Arrays.asList(nestedConditions)); } /* * (non-Javadoc) * * @see org.apache.logging.log4j.core.appender.rolling.action.PathCondition#accept(java.nio.file.Path, * java.nio.file.Path, java.nio.file.attribute.BasicFileAttributes) */ @Override public boolean accept( final Path basePath, final Path relativePath, final BasicFileAttributes attrs) { accumulatedSize += attrs.size(); final boolean result = accumulatedSize > thresholdBytes; final String match = result ? ">" : "<="; final String accept = result ? "ACCEPTED" : "REJECTED"; LOGGER.trace( "IfAccumulatedFileSize {}: {} accumulated size '{}' {} thresholdBytes '{}'", accept, relativePath, accumulatedSize, match, thresholdBytes); if (result) { return IfAll.accept(nestedConditions, basePath, relativePath, attrs); } return result; } /* * (non-Javadoc) * * @see org.apache.logging.log4j.core.appender.rolling.action.PathCondition#beforeFileTreeWalk() */ @Override public void beforeFileTreeWalk() { accumulatedSize = 0; IfAll.beforeFileTreeWalk(nestedConditions); } /** * Create an IfAccumulatedFileSize condition. * * @param threshold The threshold accumulated file size from which files will be deleted. * @return An IfAccumulatedFileSize condition. */ @PluginFactory public static IfAccumulatedFileSize createFileSizeCondition( // // @formatter:off @PluginAttribute("exceeds") final String size, @PluginElement("PathConditions") final PathCondition... nestedConditions) { // @formatter:on if (size == null) { LOGGER.error("IfAccumulatedFileSize missing mandatory size threshold."); } final long threshold = size == null ? Long.MAX_VALUE : FileSize.parse(size, Long.MAX_VALUE); return new IfAccumulatedFileSize(threshold, nestedConditions); } @Override public String toString() { final String nested = nestedConditions.length == 0 ? "" : " AND " + Arrays.toString(nestedConditions); return "IfAccumulatedFileSize(exceeds=" + thresholdBytes + nested + ")"; } }
public abstract class AbstractRolloverStrategy implements RolloverStrategy { protected static final Logger LOGGER = StatusLogger.getLogger(); public AbstractRolloverStrategy() {} }
/** The MongoDB implementation of {@link NoSqlProvider}. */ @Plugin(name = "MongoDb", category = "Core", printObject = true) public final class MongoDbProvider implements NoSqlProvider<MongoDbConnection> { private static final WriteConcern DEFAULT_WRITE_CONCERN = WriteConcern.ACKNOWLEDGED; private static final Logger LOGGER = StatusLogger.getLogger(); private static final int DEFAULT_PORT = 27017; private final String collectionName; private final DB database; private final String description; private final WriteConcern writeConcern; private MongoDbProvider( final DB database, final WriteConcern writeConcern, final String collectionName, final String description) { this.database = database; this.writeConcern = writeConcern; this.collectionName = collectionName; this.description = "mongoDb{ " + description + " }"; } @Override public MongoDbConnection getConnection() { return new MongoDbConnection(this.database, this.writeConcern, this.collectionName); } @Override public String toString() { return this.description; } /** * Factory method for creating a MongoDB provider within the plugin manager. * * @param collectionName The name of the MongoDB collection to which log events should be written. * @param writeConcernConstant The {@link WriteConcern} constant to control writing details, * defaults to {@link WriteConcern#ACKNOWLEDGED}. * @param writeConcernConstantClassName The name of a class containing the aforementioned static * WriteConcern constant. Defaults to {@link WriteConcern}. * @param databaseName The name of the MongoDB database containing the collection to which log * events should be written. Mutually exclusive with {@code * factoryClassName&factoryMethodName!=null}. * @param server The host name of the MongoDB server, defaults to localhost and mutually exclusive * with {@code factoryClassName&factoryMethodName!=null}. * @param port The port the MongoDB server is listening on, defaults to the default MongoDB port * and mutually exclusive with {@code factoryClassName&factoryMethodName!=null}. * @param userName The username to authenticate against the MongoDB server with. * @param password The password to authenticate against the MongoDB server with. * @param factoryClassName A fully qualified class name containing a static factory method capable * of returning a {@link DB} or a {@link MongoClient}. * @param factoryMethodName The name of the public static factory method belonging to the * aforementioned factory class. * @return a new MongoDB provider. */ @PluginFactory public static MongoDbProvider createNoSqlProvider( @PluginAttribute("collectionName") final String collectionName, @PluginAttribute("writeConcernConstant") final String writeConcernConstant, @PluginAttribute("writeConcernConstantClass") final String writeConcernConstantClassName, @PluginAttribute("databaseName") final String databaseName, @PluginAttribute(value = "server", defaultString = "localhost") @ValidHost final String server, @PluginAttribute(value = "port", defaultString = "" + DEFAULT_PORT) @ValidPort final String port, @PluginAttribute("userName") final String userName, @PluginAttribute(value = "password", sensitive = true) final String password, @PluginAttribute("factoryClassName") final String factoryClassName, @PluginAttribute("factoryMethodName") final String factoryMethodName) { DB database; String description; if (Strings.isNotEmpty(factoryClassName) && Strings.isNotEmpty(factoryMethodName)) { try { final Class<?> factoryClass = LoaderUtil.loadClass(factoryClassName); final Method method = factoryClass.getMethod(factoryMethodName); final Object object = method.invoke(null); if (object instanceof DB) { database = (DB) object; } else if (object instanceof MongoClient) { if (Strings.isNotEmpty(databaseName)) { database = ((MongoClient) object).getDB(databaseName); } else { LOGGER.error( "The factory method [{}.{}()] returned a MongoClient so the database name is " + "required.", factoryClassName, factoryMethodName); return null; } } else if (object == null) { LOGGER.error( "The factory method [{}.{}()] returned null.", factoryClassName, factoryMethodName); return null; } else { LOGGER.error( "The factory method [{}.{}()] returned an unsupported type [{}].", factoryClassName, factoryMethodName, object.getClass().getName()); return null; } description = "database=" + database.getName(); final List<ServerAddress> addresses = database.getMongo().getAllAddress(); if (addresses.size() == 1) { description += ", server=" + addresses.get(0).getHost() + ", port=" + addresses.get(0).getPort(); } else { description += ", servers=["; for (final ServerAddress address : addresses) { description += " { " + address.getHost() + ", " + address.getPort() + " } "; } description += "]"; } } catch (final ClassNotFoundException e) { LOGGER.error("The factory class [{}] could not be loaded.", factoryClassName, e); return null; } catch (final NoSuchMethodException e) { LOGGER.error( "The factory class [{}] does not have a no-arg method named [{}].", factoryClassName, factoryMethodName, e); return null; } catch (final Exception e) { LOGGER.error( "The factory method [{}.{}()] could not be invoked.", factoryClassName, factoryMethodName, e); return null; } } else if (Strings.isNotEmpty(databaseName)) { final List<MongoCredential> credentials = new ArrayList<>(); description = "database=" + databaseName; if (Strings.isNotEmpty(userName) && Strings.isNotEmpty(password)) { description += ", username="******", passwordHash=" + NameUtil.md5(password + MongoDbProvider.class.getName()); credentials.add( MongoCredential.createCredential(userName, databaseName, password.toCharArray())); } try { final int portInt = TypeConverters.convert(port, int.class, DEFAULT_PORT); description += ", server=" + server + ", port=" + portInt; database = new MongoClient(new ServerAddress(server, portInt), credentials).getDB(databaseName); } catch (final Exception e) { LOGGER.error( "Failed to obtain a database instance from the MongoClient at server [{}] and " + "port [{}].", server, port); return null; } } else { LOGGER.error("No factory method was provided so the database name is required."); return null; } try { database.getCollectionNames(); // Check if the database actually requires authentication } catch (final Exception e) { LOGGER.error( "The database is not up, or you are not authenticated, try supplying a username and password to the MongoDB provider.", e); return null; } final WriteConcern writeConcern = toWriteConcern(writeConcernConstant, writeConcernConstantClassName); return new MongoDbProvider(database, writeConcern, collectionName, description); } private static WriteConcern toWriteConcern( final String writeConcernConstant, final String writeConcernConstantClassName) { WriteConcern writeConcern; if (Strings.isNotEmpty(writeConcernConstant)) { if (Strings.isNotEmpty(writeConcernConstantClassName)) { try { final Class<?> writeConcernConstantClass = LoaderUtil.loadClass(writeConcernConstantClassName); final Field field = writeConcernConstantClass.getField(writeConcernConstant); writeConcern = (WriteConcern) field.get(null); } catch (final Exception e) { LOGGER.error( "Write concern constant [{}.{}] not found, using default.", writeConcernConstantClassName, writeConcernConstant); writeConcern = DEFAULT_WRITE_CONCERN; } } else { writeConcern = WriteConcern.valueOf(writeConcernConstant); if (writeConcern == null) { LOGGER.warn( "Write concern constant [{}] not found, using default.", writeConcernConstant); writeConcern = DEFAULT_WRITE_CONCERN; } } } else { writeConcern = DEFAULT_WRITE_CONCERN; } return writeConcern; } }
/** * LogEventListener server that receives LogEvents over a JMS {@link javax.jms.Destination}. * * @since 2.1 */ public class JmsServer extends LogEventListener implements MessageListener, LifeCycle { private static final Logger LOGGER = StatusLogger.getLogger(); private final AtomicReference<State> state = new AtomicReference<State>(State.INITIALIZED); private final JmsManager jmsManager; private MessageConsumer messageConsumer; public JmsServer( final String connectionFactoryBindingName, final String destinationBindingName, final String username, final String password) { final String managerName = JmsServer.class.getName() + '@' + JmsServer.class.hashCode(); final JndiManager jndiManager = JndiManager.getDefaultManager(managerName); jmsManager = JmsManager.getJmsManager( managerName, jndiManager, connectionFactoryBindingName, destinationBindingName, username, password); } @Override public void onMessage(final Message message) { try { if (message instanceof ObjectMessage) { final Object body = ((ObjectMessage) message).getObject(); if (body instanceof LogEvent) { log((LogEvent) body); } else { LOGGER.warn( "Expected ObjectMessage to contain LogEvent. Got type {} instead.", body.getClass()); } } else { LOGGER.warn( "Received message of type {} and JMSType {} which cannot be handled.", message.getClass(), message.getJMSType()); } } catch (final JMSException e) { LOGGER.catching(e); } } @Override public void start() { if (state.compareAndSet(State.INITIALIZED, State.STARTING)) { try { messageConsumer = jmsManager.createMessageConsumer(); messageConsumer.setMessageListener(this); } catch (final JMSException e) { throw new LoggingException(e); } } } @Override public void stop() { try { messageConsumer.close(); } catch (final JMSException ignored) { } jmsManager.release(); } @Override public boolean isStarted() { return state.get() == State.STARTED; } @Override public boolean isStopped() { return state.get() == State.STOPPED; } }
/** * Creates {@link AsyncEventRouter} instances based on user-specified system properties. The {@code * AsyncEventRouter} created by this factory is used in AsyncLogger, AsyncLoggerConfig and * AsyncAppender to control if events are logged in the current thread, the background thread, or * discarded. * * <p>Property {@code "log4j2.AsyncEventRouter"} controls the routing behaviour. If this property is * not specified or has value {@code "Default"}, this factory creates {@link * DefaultAsyncEventRouter} objects. * * <p>If this property has value {@code "Discard"}, this factory creates {@link * DiscardingAsyncEventRouter} objects. By default, this router discards events of level {@code * INFO}, {@code DEBUG} and {@code TRACE} if the queue is full. This can be adjusted with property * {@code "log4j2.DiscardThreshold"} (name of the level at which to start discarding). * * <p>For any other value, this factory interprets the value as the fully qualified name of a class * implementing the {@link AsyncEventRouter} interface. The class must have a default constructor. * * @since 2.6 */ public class AsyncEventRouterFactory { static final String PROPERTY_NAME_ASYNC_EVENT_ROUTER = "log4j2.AsyncEventRouter"; static final String PROPERTY_VALUE_DEFAULT_ASYNC_EVENT_ROUTER = "Default"; static final String PROPERTY_VALUE_DISCARDING_ASYNC_EVENT_ROUTER = "Discard"; static final String PROPERTY_NAME_DISCARDING_THRESHOLD_LEVEL = "log4j2.DiscardThreshold"; private static final Logger LOGGER = StatusLogger.getLogger(); /** * Creates and returns {@link AsyncEventRouter} instances based on user-specified system * properties. * * <p>Property {@code "log4j2.AsyncEventRouter"} controls the routing behaviour. If this property * is not specified or has value {@code "Default"}, this method returns {@link * DefaultAsyncEventRouter} objects. * * <p>If this property has value {@code "Discard"}, this method returns {@link * DiscardingAsyncEventRouter} objects. * * <p>For any other value, this method interprets the value as the fully qualified name of a class * implementing the {@link AsyncEventRouter} interface. The class must have a default constructor. * * @return a new AsyncEventRouter */ public static AsyncEventRouter create() { final String router = PropertiesUtil.getProperties().getStringProperty(PROPERTY_NAME_ASYNC_EVENT_ROUTER); if (router == null || PROPERTY_VALUE_DEFAULT_ASYNC_EVENT_ROUTER.equals(router) || DefaultAsyncEventRouter.class.getSimpleName().equals(router) || DefaultAsyncEventRouter.class.getName().equals(router)) { return new DefaultAsyncEventRouter(); } if (PROPERTY_VALUE_DISCARDING_ASYNC_EVENT_ROUTER.equals(router) || DiscardingAsyncEventRouter.class.getSimpleName().equals(router) || DiscardingAsyncEventRouter.class.getName().equals(router)) { return createDiscardingAsyncEventRouter(); } return createCustomRouter(router); } private static AsyncEventRouter createCustomRouter(final String router) { try { final Class<? extends AsyncEventRouter> cls = LoaderUtil.loadClass(router).asSubclass(AsyncEventRouter.class); LOGGER.debug("Creating custom AsyncEventRouter '{}'", router); return cls.newInstance(); } catch (final Exception ex) { LOGGER.debug( "Using DefaultAsyncEventRouter. Could not create custom AsyncEventRouter '{}': {}", router, ex.toString()); return new DefaultAsyncEventRouter(); } } private static AsyncEventRouter createDiscardingAsyncEventRouter() { final PropertiesUtil util = PropertiesUtil.getProperties(); final String level = util.getStringProperty(PROPERTY_NAME_DISCARDING_THRESHOLD_LEVEL, Level.INFO.name()); final Level thresholdLevel = Level.toLevel(level, Level.INFO); LOGGER.debug("Creating custom DiscardingAsyncEventRouter(discardThreshold:{})", thresholdLevel); return new DiscardingAsyncEventRouter(thresholdLevel); } }
/** * Base class for PluginVisitor implementations. Provides convenience methods as well as all method * implementations other than the {@code visit} method. * * @param <A> the Plugin annotation type. */ public abstract class AbstractPluginVisitor<A extends Annotation> implements PluginVisitor<A> { protected static final Logger LOGGER = StatusLogger.getLogger(); protected final Class<A> clazz; protected A annotation; protected String[] aliases; protected Class<?> conversionType; protected StrSubstitutor substitutor; protected Member member; /** * This constructor must be overridden by implementation classes as a no-arg constructor. * * @param clazz the annotation class this PluginVisitor is for. */ protected AbstractPluginVisitor(final Class<A> clazz) { this.clazz = clazz; } @SuppressWarnings("unchecked") @Override public PluginVisitor<A> setAnnotation(final Annotation annotation) { final Annotation a = Assert.requireNonNull(annotation, "No annotation was provided"); if (this.clazz.isInstance(a)) { this.annotation = (A) a; } return this; } @Override public PluginVisitor<A> setAliases(final String... aliases) { this.aliases = aliases; return this; } @Override public PluginVisitor<A> setConversionType(final Class<?> conversionType) { this.conversionType = Assert.requireNonNull(conversionType, "No conversion type class was provided"); return this; } @Override public PluginVisitor<A> setStrSubstitutor(final StrSubstitutor substitutor) { this.substitutor = Assert.requireNonNull(substitutor, "No StrSubstitutor was provided"); return this; } @Override public PluginVisitor<A> setMember(final Member member) { this.member = member; return this; } /** * Removes an Entry from a given Map using a key name and aliases for that key. Keys are * case-insensitive. * * @param attributes the Map to remove an Entry from. * @param name the key name to look up. * @param aliases optional aliases of the key name to look up. * @return the value corresponding to the given key or {@code null} if nonexistent. */ protected static String removeAttributeValue( final Map<String, String> attributes, final String name, final String... aliases) { for (final Map.Entry<String, String> entry : attributes.entrySet()) { final String key = entry.getKey(); final String value = entry.getValue(); if (key.equalsIgnoreCase(name)) { attributes.remove(key); return value; } if (aliases != null) { for (final String alias : aliases) { if (key.equalsIgnoreCase(alias)) { attributes.remove(key); return value; } } } } return null; } /** * Converts the given value into the configured type falling back to the provided default value. * * @param value the value to convert. * @param defaultValue the fallback value to use in case of no value or an error. * @return the converted value whether that be based on the given value or the default value. */ protected Object convert(final String value, final Object defaultValue) { if (defaultValue instanceof String) { return TypeConverters.convert( value, this.conversionType, Strings.trimToNull((String) defaultValue)); } return TypeConverters.convert(value, this.conversionType, defaultValue); } }
/** * AsyncLogger is a logger designed for high throughput and low latency logging. It does not perform * any I/O in the calling (application) thread, but instead hands off the work to another thread as * soon as possible. The actual logging is performed in the background thread. It uses the LMAX * Disruptor library for inter-thread communication. (<a * href="http://lmax-exchange.github.com/disruptor/" * >http://lmax-exchange.github.com/disruptor/</a>) * * <p>To use AsyncLogger, specify the System property {@code * -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector} before you * obtain a Logger, and all Loggers returned by LogManager.getLogger will be AsyncLoggers. * * <p>Note that for performance reasons, this logger does not include source location by default. * You need to specify {@code includeLocation="true"} in the configuration or any %class, %location * or %line conversion patterns in your log4j.xml configuration will produce either a "?" character * or no output at all. * * <p>For best performance, use AsyncLogger with the RandomAccessFileAppender or * RollingRandomAccessFileAppender, with immediateFlush=false. These appenders have built-in support * for the batching mechanism used by the Disruptor library, and they will flush to disk at the end * of each batch. This means that even with immediateFlush=false, there will never be any items left * in the buffer; all log events will all be written to disk in a very efficient manner. */ public class AsyncLogger extends Logger { private static final long serialVersionUID = 1L; private static final int SLEEP_MILLIS_BETWEEN_DRAIN_ATTEMPTS = 50; private static final int MAX_DRAIN_ATTEMPTS_BEFORE_SHUTDOWN = 200; private static final int RINGBUFFER_MIN_SIZE = 128; private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024; private static final StatusLogger LOGGER = StatusLogger.getLogger(); private static final ThreadNameStrategy THREAD_NAME_STRATEGY = ThreadNameStrategy.create(); /** Strategy for deciding whether thread name should be cached or not. */ static enum ThreadNameStrategy { // LOG4J2-467 CACHED { @Override public String getThreadName(final Info info) { return info.cachedThreadName; } }, UNCACHED { @Override public String getThreadName(final Info info) { return Thread.currentThread().getName(); } }; abstract String getThreadName(Info info); static ThreadNameStrategy create() { final String name = PropertiesUtil.getProperties() .getStringProperty("AsyncLogger.ThreadNameStrategy", CACHED.name()); try { return ThreadNameStrategy.valueOf(name); } catch (final Exception ex) { LOGGER.debug( "Using AsyncLogger.ThreadNameStrategy.CACHED: '{}' not valid: {}", name, ex.toString()); return CACHED; } } } private static volatile Disruptor<RingBufferLogEvent> disruptor; private static final Clock CLOCK = ClockFactory.getClock(); private static volatile NanoClock nanoClock = new DummyNanoClock(); private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(new DaemonThreadFactory("AsyncLogger-")); static { initInfoForExecutorThread(); LOGGER.debug("AsyncLogger.ThreadNameStrategy={}", THREAD_NAME_STRATEGY); final int ringBufferSize = calculateRingBufferSize(); final WaitStrategy waitStrategy = createWaitStrategy(); disruptor = new Disruptor<>( RingBufferLogEvent.FACTORY, ringBufferSize, EXECUTOR, ProducerType.MULTI, waitStrategy); disruptor.handleExceptionsWith(getExceptionHandler()); disruptor.handleEventsWith(new RingBufferLogEventHandler()); LOGGER.debug( "Starting AsyncLogger disruptor with ringbuffer size {}...", disruptor.getRingBuffer().getBufferSize()); disruptor.start(); } /** * Constructs an {@code AsyncLogger} with the specified context, name and message factory. * * @param context context of this logger * @param name name of this logger * @param messageFactory message factory of this logger */ public AsyncLogger( final LoggerContext context, final String name, final MessageFactory messageFactory) { super(context, name, messageFactory); } private static int calculateRingBufferSize() { int ringBufferSize = RINGBUFFER_DEFAULT_SIZE; final String userPreferredRBSize = PropertiesUtil.getProperties() .getStringProperty("AsyncLogger.RingBufferSize", String.valueOf(ringBufferSize)); try { int size = Integer.parseInt(userPreferredRBSize); if (size < RINGBUFFER_MIN_SIZE) { size = RINGBUFFER_MIN_SIZE; LOGGER.warn( "Invalid RingBufferSize {}, using minimum size {}.", userPreferredRBSize, RINGBUFFER_MIN_SIZE); } ringBufferSize = size; } catch (final Exception ex) { LOGGER.warn( "Invalid RingBufferSize {}, using default size {}.", userPreferredRBSize, ringBufferSize); } return Integers.ceilingNextPowerOfTwo(ringBufferSize); } /** * Initialize an {@code Info} object that is threadlocal to the consumer/appender thread. This * Info object uniquely has attribute {@code isAppenderThread} set to {@code true}. All other Info * objects will have this attribute set to {@code false}. This allows us to detect Logger.log() * calls initiated from the appender thread, which may cause deadlock when the RingBuffer is full. * (LOG4J2-471) */ private static void initInfoForExecutorThread() { EXECUTOR.submit( new Runnable() { @Override public void run() { final boolean isAppenderThread = true; final Info info = new Info( new RingBufferLogEventTranslator(), // Thread.currentThread().getName(), isAppenderThread); Info.THREADLOCAL.set(info); } }); } private static WaitStrategy createWaitStrategy() { final String strategy = PropertiesUtil.getProperties().getStringProperty("AsyncLogger.WaitStrategy"); LOGGER.debug("property AsyncLogger.WaitStrategy={}", strategy); if ("Sleep".equals(strategy)) { return new SleepingWaitStrategy(); } else if ("Yield".equals(strategy)) { return new YieldingWaitStrategy(); } else if ("Block".equals(strategy)) { return new BlockingWaitStrategy(); } LOGGER.debug("disruptor event handler uses BlockingWaitStrategy"); return new BlockingWaitStrategy(); } private static ExceptionHandler<RingBufferLogEvent> getExceptionHandler() { final String cls = PropertiesUtil.getProperties().getStringProperty("AsyncLogger.ExceptionHandler"); if (cls == null) { LOGGER.debug("No AsyncLogger.ExceptionHandler specified"); return null; } try { @SuppressWarnings("unchecked") final ExceptionHandler<RingBufferLogEvent> result = Loader.newCheckedInstanceOf(cls, ExceptionHandler.class); LOGGER.debug("AsyncLogger.ExceptionHandler={}", result); return result; } catch (final Exception ignored) { LOGGER.debug("AsyncLogger.ExceptionHandler not set: error creating " + cls + ": ", ignored); return null; } } /** Tuple with the event translator and thread name for a thread. */ static class Info { private static final ThreadLocal<Info> THREADLOCAL = new ThreadLocal<Info>() { @Override protected Info initialValue() { // by default, set isAppenderThread to false return new Info( new RingBufferLogEventTranslator(), Thread.currentThread().getName(), false); } }; private final RingBufferLogEventTranslator translator; private final String cachedThreadName; private final boolean isAppenderThread; public Info( final RingBufferLogEventTranslator translator, final String threadName, final boolean appenderThread) { this.translator = translator; this.cachedThreadName = threadName; this.isAppenderThread = appenderThread; } // LOG4J2-467 private String threadName() { return THREAD_NAME_STRATEGY.getThreadName(this); } } @Override public void logMessage( final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { final Disruptor<RingBufferLogEvent> temp = disruptor; if (temp == null) { // LOG4J2-639 LOGGER.fatal("Ignoring log event after log4j was shut down"); } else { logMessage0(temp, fqcn, level, marker, message, thrown); } } private void logMessage0( final Disruptor<RingBufferLogEvent> theDisruptor, final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { final Info info = Info.THREADLOCAL.get(); logMessageInAppropriateThread(info, theDisruptor, fqcn, level, marker, message, thrown); } private void logMessageInAppropriateThread( final Info info, final Disruptor<RingBufferLogEvent> theDisruptor, final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { if (!logMessageInCurrentThread(info, theDisruptor, fqcn, level, marker, message, thrown)) { logMessageInBackgroundThread(info, fqcn, level, marker, message, thrown); } } /** * LOG4J2-471: prevent deadlock when RingBuffer is full and object being logged calls Logger.log() * from its toString() method * * @param info threadlocal information - used to determine if the current thread is the background * appender thread * @param theDisruptor used to check if the buffer is full * @param fqcn fully qualified caller name * @param level log level * @param marker optional marker * @param message log message * @param thrown optional exception * @return {@code true} if the event has been logged in the current thread, {@code false} if it * should be passed to the background thread */ private boolean logMessageInCurrentThread( Info info, final Disruptor<RingBufferLogEvent> theDisruptor, final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { if (info.isAppenderThread && theDisruptor.getRingBuffer().remainingCapacity() == 0) { // bypass RingBuffer and invoke Appender directly final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy(); strategy.log(this, getName(), fqcn, marker, level, message, thrown); return true; } return false; } /** * Enqueues the specified message to be logged in the background thread. * * @param info holds some cached information * @param fqcn fully qualified caller name * @param level log level * @param marker optional marker * @param message log message * @param thrown optional exception */ private void logMessageInBackgroundThread( Info info, final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { message.getFormattedMessage(); // LOG4J2-763: ask message to freeze parameters initLogMessageInfo(info, fqcn, level, marker, message, thrown); enqueueLogMessageInfo(info); } private void initLogMessageInfo( Info info, final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { info.translator.setValues( this, getName(), marker, fqcn, level, message, // // don't construct ThrowableProxy until required thrown, // // config properties are taken care of in the EventHandler // thread in the #actualAsyncLog method // needs shallow copy to be fast (LOG4J2-154) ThreadContext.getImmutableContext(), // // needs shallow copy to be fast (LOG4J2-154) ThreadContext.getImmutableStack(), // // Thread.currentThread().getName(), // // info.cachedThreadName, // info.threadName(), // // location: very expensive operation. LOG4J2-153: // Only include if "includeLocation=true" is specified, // exclude if not specified or if "false" was specified. calcLocationIfRequested(fqcn), // System.currentTimeMillis()); // CoarseCachedClock: 20% faster than system clock, 16ms gaps // CachedClock: 10% faster than system clock, smaller gaps // LOG4J2-744 avoid calling clock altogether if message has the timestamp eventTimeMillis(message), // nanoClock.nanoTime() // ); } private long eventTimeMillis(final Message message) { return message instanceof TimestampMessage ? ((TimestampMessage) message).getTimestamp() : CLOCK.currentTimeMillis(); } /** * Returns the caller location if requested, {@code null} otherwise. * * @param fqcn fully qualified caller name. * @return the caller location if requested, {@code null} otherwise. */ private StackTraceElement calcLocationIfRequested(String fqcn) { final boolean includeLocation = privateConfig.loggerConfig.isIncludeLocation(); return includeLocation ? location(fqcn) : null; } private void enqueueLogMessageInfo(Info info) { // LOG4J2-639: catch NPE if disruptor field was set to null after our check above try { // Note: do NOT use the temp variable above! // That could result in adding a log event to the disruptor after it was shut down, // which could cause the publishEvent method to hang and never return. disruptor.publishEvent(info.translator); } catch (final NullPointerException npe) { LOGGER.fatal("Ignoring log event after log4j was shut down."); } } private static StackTraceElement location(final String fqcnOfLogger) { return Log4jLogEvent.calcLocation(fqcnOfLogger); } /** * This method is called by the EventHandler that processes the RingBufferLogEvent in a separate * thread. * * @param event the event to log */ public void actualAsyncLog(final RingBufferLogEvent event) { final Map<Property, Boolean> properties = privateConfig.loggerConfig.getProperties(); event.mergePropertiesIntoContextMap(properties, privateConfig.config.getStrSubstitutor()); final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy(); strategy.log(this, event); } public static void stop() { final Disruptor<RingBufferLogEvent> temp = disruptor; // Must guarantee that publishing to the RingBuffer has stopped // before we call disruptor.shutdown() disruptor = null; // client code fails with NPE if log after stop = OK if (temp == null) { return; // stop() has already been called } // Calling Disruptor.shutdown() will wait until all enqueued events are fully processed, // but this waiting happens in a busy-spin. To avoid (postpone) wasting CPU, // we sleep in short chunks, up to 10 seconds, waiting for the ringbuffer to drain. for (int i = 0; hasBacklog(temp) && i < MAX_DRAIN_ATTEMPTS_BEFORE_SHUTDOWN; i++) { try { Thread.sleep(SLEEP_MILLIS_BETWEEN_DRAIN_ATTEMPTS); // give up the CPU for a while } catch (final InterruptedException e) { // ignored } } temp.shutdown(); // busy-spins until all events currently in the disruptor have been processed EXECUTOR.shutdown(); // finally, kill the processor thread Info.THREADLOCAL.remove(); // LOG4J2-323 } /** Returns {@code true} if the specified disruptor still has unprocessed events. */ private static boolean hasBacklog(final Disruptor<?> theDisruptor) { final RingBuffer<?> ringBuffer = theDisruptor.getRingBuffer(); return !ringBuffer.hasAvailableCapacity(ringBuffer.getBufferSize()); } /** * Creates and returns a new {@code RingBufferAdmin} that instruments the ringbuffer of the {@code * AsyncLogger}. * * @param contextName name of the global {@code AsyncLoggerContext} * @return a new {@code RingBufferAdmin} that instruments the ringbuffer */ public static RingBufferAdmin createRingBufferAdmin(final String contextName) { return RingBufferAdmin.forAsyncLogger(disruptor.getRingBuffer(), contextName); } /** * Returns the {@code NanoClock} to use for creating the nanoTime timestamp of log events. * * @return the {@code NanoClock} to use for creating the nanoTime timestamp of log events */ public static NanoClock getNanoClock() { return nanoClock; } /** * Sets the {@code NanoClock} to use for creating the nanoTime timestamp of log events. * * <p>FOR INTERNAL USE. This method may be called with a different {@code NanoClock} * implementation when the configuration changes. * * @param nanoClock the {@code NanoClock} to use for creating the nanoTime timestamp of log events */ public static void setNanoClock(NanoClock nanoClock) { AsyncLogger.nanoClock = Objects.requireNonNull(nanoClock, "NanoClock must be non-null"); } }
public class ElasticsearchHttpClient { private static final Logger logger = StatusLogger.getLogger(); private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static final Set<Character> JS_ESCAPE_CHARS; static { Set<Character> mandatoryEscapeSet = new HashSet<Character>(); mandatoryEscapeSet.add('"'); mandatoryEscapeSet.add('\\'); JS_ESCAPE_CHARS = Collections.unmodifiableSet(mandatoryEscapeSet); } private final Queue<String> requests = new ConcurrentLinkedQueue<String>(); private final ReentrantLock lock = new ReentrantLock(true); private final String url; private final String index; private final String type; private final boolean create; private final int maxActionsPerBulkRequest; private final boolean logresponses; private final ScheduledExecutorService service; private volatile boolean closed = false; private HttpURLConnection connection; public ElasticsearchHttpClient( String url, String index, String type, boolean create, int maxActionsPerBulkRequest, long flushSecs, boolean logresponses) { this.url = url; this.index = index; this.type = type; this.create = create; this.maxActionsPerBulkRequest = maxActionsPerBulkRequest; this.logresponses = logresponses; this.closed = false; this.service = Executors.newScheduledThreadPool(1); service.schedule( new Callable<Object>() { @Override public Object call() throws Exception { try { flush(); } catch (IOException e) { logger.error(e.getMessage(), e); throw new AppenderLoggingException(e); } return null; } }, flushSecs, TimeUnit.SECONDS); } public ElasticsearchHttpClient index(Map<String, Object> source) { if (closed) { logger.error("logger is closed"); throw new AppenderLoggingException("logger is closed"); } try { requests.add(build(index, type, create, source)); } catch (Exception e) { logger.error(e); closed = true; } return this; } public void flush() throws IOException { lock.lock(); try { while (!requests.isEmpty()) { if (closed) { logger.error("logger is closed"); return; } if (connection == null) { connection = (HttpURLConnection) new URL(url).openConnection(); } try { connection.setDoOutput(true); connection.setRequestMethod("POST"); } catch (Exception e) { logger.error(e); // retry connection = (HttpURLConnection) new URL(url).openConnection(); connection.setDoOutput(true); connection.setRequestMethod("POST"); } StringBuilder sb = new StringBuilder(); int i = maxActionsPerBulkRequest; String request; while ((request = requests.poll()) != null && (i-- >= 0)) { sb.append(request); } OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); writer.write(sb.toString()); writer.close(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { // read response if (logresponses) { sb.setLength(0); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); String s; while ((s = in.readLine()) != null) { sb.append(s); } in.close(); logger.info(sb.toString()); } } else { throw new AppenderLoggingException( "no OK response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); } } } catch (Throwable t) { logger.error(t); closed = true; throw new AppenderLoggingException("Elasticsearch HTTP error", t); } finally { lock.unlock(); } } public void close() throws IOException { if (!closed) { service.shutdownNow(); flush(); connection.getOutputStream().close(); connection.disconnect(); } closed = true; } private String build(String index, String type, boolean create, Map<String, Object> source) { index = index.indexOf('\'') < 0 ? index : getIndexNameDateFormat(index).format(new Date()); StringBuilder sb = new StringBuilder(); sb.append("{\"") .append(create ? "create" : "index") .append("\":{\"_index\":\"") .append(index) .append("\",\"_type\":\"") .append(type) .append("\"}}\n{"); build(sb, source); sb.append("}\n"); return sb.toString(); } @SuppressWarnings("unchecked") private void build(StringBuilder sb, Object object) { if (object instanceof Map) { sb.append('{'); build(sb, (Map<String, Object>) object); sb.append('}'); } else if (object instanceof List) { sb.append("["); build(sb, (List<Object>) object); sb.append("]"); } else if (object != null) { if (object instanceof Number) { sb.append(object); } else if (object instanceof Boolean) { sb.append(((Boolean) object) ? "true" : "false"); } else if (object instanceof Date) { sb.append('"'); sb.append(format((Date) object)); sb.append('"'); } else { sb.append('"'); escape(sb, object.toString()); sb.append('"'); } } else { sb.append("null"); } } private void build(StringBuilder sb, List<Object> list) { boolean started = false; for (Object object : list) { if (started) { sb.append(','); } build(sb, object); started = true; } } private void build(StringBuilder sb, Map<String, Object> map) { boolean started = false; for (Map.Entry<String, Object> me : map.entrySet()) { if (started) { sb.append(','); } // try to parse message as JSON if ("message".equals(me.getKey()) && me.getValue() != null) { JsonParser parser = new JsonParser(new StringReader(me.getValue().toString())); try { build(sb, (Map<String, Object>) parser.parse()); } catch (Throwable e) { sb.append("\"").append(me.getKey()).append("\":"); build(sb, me.getValue()); } } else { sb.append("\"").append(me.getKey()).append("\":"); build(sb, me.getValue()); } started = true; } } private void escape(StringBuilder out, CharSequence plainText) { int pos = 0; int len = plainText.length(); for (int charCount, i = 0; i < len; i += charCount) { int codePoint = Character.codePointAt(plainText, i); charCount = Character.charCount(codePoint); if (!isControlCharacter(codePoint) && !mustEscapeCharInJsString(codePoint)) { continue; } out.append(plainText, pos, i); pos = i + charCount; switch (codePoint) { case '\b': out.append("\\b"); break; case '\t': out.append("\\t"); break; case '\n': out.append("\\n"); break; case '\f': out.append("\\f"); break; case '\r': out.append("\\r"); break; case '\\': out.append("\\\\"); break; case '/': out.append("\\/"); break; case '"': out.append("\\\""); break; default: appendHexJavaScriptRepresentation(out, codePoint); break; } } out.append(plainText, pos, len); } private boolean isControlCharacter(int codePoint) { return codePoint < 0x20 || codePoint == 0x2028 // Line separator || codePoint == 0x2029 // Paragraph separator || (codePoint >= 0x7f && codePoint <= 0x9f); } private void appendHexJavaScriptRepresentation(StringBuilder sb, int codePoint) { sb.append("\\u") .append(HEX_CHARS[(codePoint >>> 12) & 0xf]) .append(HEX_CHARS[(codePoint >>> 8) & 0xf]) .append(HEX_CHARS[(codePoint >>> 4) & 0xf]) .append(HEX_CHARS[codePoint & 0xf]); } private boolean mustEscapeCharInJsString(int codepoint) { if (!Character.isSupplementaryCodePoint(codepoint)) { char c = (char) codepoint; return JS_ESCAPE_CHARS.contains(c); } return false; } private static final String ISO_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.S'Z'"; private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); private static final ThreadLocal<Map<String, SimpleDateFormat>> df = new ThreadLocal<Map<String, SimpleDateFormat>>() { public Map<String, SimpleDateFormat> initialValue() { return new HashMap<String, SimpleDateFormat>(); } }; private SimpleDateFormat getDateFormat(String format) { Map<String, SimpleDateFormat> formatters = df.get(); SimpleDateFormat formatter = formatters.get(format); if (formatter == null) { formatter = new SimpleDateFormat(); formatter.applyPattern(ISO_FORMAT); formatter.setTimeZone(GMT); formatters.put(format, formatter); } return formatter; } private SimpleDateFormat getIndexNameDateFormat(String index) { Map<String, SimpleDateFormat> formatters = df.get(); SimpleDateFormat formatter = formatters.get(index); if (formatter == null) { formatter = new SimpleDateFormat(); formatter.applyPattern(index); formatters.put(index, formatter); } return formatter; } private String format(Date date) { if (date == null) { return null; } return getDateFormat(ISO_FORMAT).format(date); } class JsonParser { private static final int DEFAULT_BUFFER_SIZE = 1024; private final Reader reader; private final char[] buf; private int index; private int fill; private int ch; private StringBuilder sb; private int start; public JsonParser(Reader reader) { this(reader, DEFAULT_BUFFER_SIZE); } public JsonParser(Reader reader, int buffersize) { this.reader = reader; buf = new char[buffersize]; start = -1; } public Object parse() throws IOException { read(); skipBlank(); Object result = parseValue(); skipBlank(); if (ch != -1) { throw new IOException("unexpected character: " + ch); } return result; } private Object parseValue() throws IOException { switch (ch) { case 'n': return parseNull(); case 't': return parseTrue(); case 'f': return parseFalse(); case '"': return parseString(); case '[': return parseList(); case '{': return parseMap(); case '-': case '+': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return parseNumber(); } throw new IOException("value"); } private List<Object> parseList() throws IOException { read(); List<Object> list = new ArrayList<Object>(); skipBlank(); if (parseChar(']')) { return list; } do { skipBlank(); list.add(parseValue()); skipBlank(); } while (parseChar(',')); if (!parseChar(']')) { expected("',' or ']'"); } return list; } private Map<String, Object> parseMap() throws IOException { read(); Map<String, Object> object = new LinkedHashMap<String, Object>(); skipBlank(); if (parseChar('}')) { return object; } do { skipBlank(); if (ch != '"') { expected("name"); } String name = parseString(); skipBlank(); if (!parseChar(':')) { expected("':'"); } skipBlank(); object.put(name, parseValue()); skipBlank(); } while (parseChar(',')); if (!parseChar('}')) { expected("',' or '}'"); } return object; } private Object parseNull() throws IOException { read(); checkForChar('u'); checkForChar('l'); checkForChar('l'); return null; } private Object parseTrue() throws IOException { read(); checkForChar('r'); checkForChar('u'); checkForChar('e'); return Boolean.TRUE; } private Object parseFalse() throws IOException { read(); checkForChar('a'); checkForChar('l'); checkForChar('s'); checkForChar('e'); return Boolean.FALSE; } private void checkForChar(char ch) throws IOException { if (!parseChar(ch)) { expected("'" + ch + "'"); } } private String parseString() throws IOException { read(); startCapture(); while (ch != '"') { if (ch == '\\') { pauseCapture(); parseEscaped(); startCapture(); } else if (ch < 0x20) { expected("valid string character"); } else { read(); } } String s = endCapture(); read(); return s; } private void parseEscaped() throws IOException { read(); switch (ch) { case '"': case '/': case '\\': sb.append((char) ch); break; case 'b': sb.append('\b'); break; case 't': sb.append('\t'); break; case 'f': sb.append('\f'); break; case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 'u': char[] hex = new char[4]; for (int i = 0; i < 4; i++) { read(); if (!isHexDigit()) { expected("hexadecimal digit"); } hex[i] = (char) ch; } sb.append((char) Integer.parseInt(String.valueOf(hex), 16)); break; default: expected("valid escape sequence"); } read(); } private Object parseNumber() throws IOException { startCapture(); parseChar('-'); int firstDigit = ch; if (!parseDigit()) { expected("digit"); } if (firstDigit != '0') { while (parseDigit()) {} } parseFraction(); parseExponent(); return endCapture(); } private boolean parseFraction() throws IOException { if (!parseChar('.')) { return false; } if (!parseDigit()) { expected("digit"); } while (parseDigit()) {} return true; } private boolean parseExponent() throws IOException { if (!parseChar('e') && !parseChar('E')) { return false; } if (!parseChar('+')) { parseChar('-'); } if (!parseDigit()) { expected("digit"); } while (parseDigit()) {} return true; } private boolean parseChar(char ch) throws IOException { if (this.ch != ch) { return false; } read(); return true; } private boolean parseDigit() throws IOException { if (!isDigit()) { return false; } read(); return true; } private void skipBlank() throws IOException { while (isWhiteSpace()) { read(); } } private void read() throws IOException { if (ch == -1) { throw new IOException("unexpected end of input"); } if (index == fill) { if (start != -1) { sb.append(buf, start, fill - start); start = 0; } fill = reader.read(buf, 0, buf.length); index = 0; if (fill == -1) { ch = -1; return; } } ch = buf[index++]; } private void startCapture() { if (sb == null) { sb = new StringBuilder(); } start = index - 1; } private void pauseCapture() { int end = ch == -1 ? index : index - 1; sb.append(buf, start, end - start); start = -1; } private String endCapture() { int end = ch == -1 ? index : index - 1; String captured; if (sb.length() > 0) { sb.append(buf, start, end - start); captured = sb.toString(); sb.setLength(0); } else { captured = new String(buf, start, end - start); } start = -1; return captured; } private boolean isWhiteSpace() { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; } private boolean isDigit() { return ch >= '0' && ch <= '9'; } private boolean isHexDigit() { return ch >= '0' && ch <= '9' || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F'; } private void expected(String expected) throws IOException { if (ch == -1) { throw new IOException("unexpected end of input"); } throw new IOException("expected " + expected); } } }
public class StoreConfiguration<T> { protected static final StatusLogger LOGGER = StatusLogger.getLogger(); private String location; private String password; public StoreConfiguration(final String location, final String password) { this.location = location; this.password = password; } public String getLocation() { return this.location; } public void setLocation(final String location) { this.location = location; } public String getPassword() { return this.password; } public char[] getPasswordAsCharArray() { return this.password == null ? null : this.password.toCharArray(); } public void setPassword(final String password) { this.password = password; } /** @throws StoreConfigurationException May be thrown by subclasses */ protected T load() throws StoreConfigurationException { return null; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((this.location == null) ? 0 : this.location.hashCode()); result = prime * result + ((this.password == null) ? 0 : this.password.hashCode()); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof StoreConfiguration)) { return false; } final StoreConfiguration<?> other = (StoreConfiguration<?>) obj; if (this.location == null) { if (other.location != null) { return false; } } else if (!this.location.equals(other.location)) { return false; } if (this.password == null) { if (other.password != null) { return false; } } else if (!this.password.equals(other.password)) { return false; } return true; } }