diff --git a/java_console/logging-api/build.gradle b/java_console/logging-api/build.gradle index c5fc4ba0d4..21aaaef2f2 100644 --- a/java_console/logging-api/build.gradle +++ b/java_console/logging-api/build.gradle @@ -1,3 +1,11 @@ plugins { id 'java' +} + +dependencies { + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.3' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3' + //implementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + } \ No newline at end of file diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/DefaultLogging.java b/java_console/logging-api/src/main/java/com/devexperts/logging/DefaultLogging.java new file mode 100644 index 0000000000..6d1b321bfd --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/DefaultLogging.java @@ -0,0 +1,150 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Logging implementation that uses {@link java.util.logging} logging facilities. + */ +class DefaultLogging { + + Map configure() { + // Heuristically check if there was an attempt to manually configure logging + if (getProperty("java.util.logging.config.class", null) != null || + getProperty("java.util.logging.config.file", null) != null || + !hasDefaultHandlers(Logger.getLogger(""))) + { + return Collections.emptyMap(); // logging was already manually configured + } + return configureLogFile(getProperty(Logging.LOG_FILE_PROPERTY, null)); + } + + private boolean hasDefaultHandlers(Logger root) { + // Default configuration is 1 ConsoleHandler with SimpleFormatter and INFO level + Handler[] handlers = root.getHandlers(); + if (handlers.length != 1) + return false; + Handler handler = handlers[0]; + return handler.getClass() == ConsoleHandler.class && + handler.getFormatter().getClass() == SimpleFormatter.class && + handler.getLevel() == Level.INFO; + } + + Map configureLogFile(String log_file) { + Logger root = Logger.getLogger(""); + Map errors = new LinkedHashMap(); + + try { + // Don't reset configuration. Retain all manually configured loggers, but + // reconfigure the root logger, which (as we checked) has a default configuration with + // 1 ConsoleHandler with SimpleFormatter and INFO level + for (Handler handler : root.getHandlers()) + root.removeHandler(handler); + + // configure "log" file or console + Handler handler = null; + if (log_file != null) { + try { + handler = new FileHandler(log_file, getLimit(Logging.LOG_MAX_FILE_SIZE_PROPERTY, errors), 2, true); + } catch (IOException e) { + errors.put(log_file, e); + } + } + if (handler == null) + handler = new ConsoleHandler(); + handler.setFormatter(new LogFormatter()); + handler.setLevel(Level.ALL); + root.addHandler(handler); + + // configure "err" file + String err_file = getProperty(Logging.ERR_FILE_PROPERTY, null); + if (err_file != null) { + try { + handler = new FileHandler(err_file, getLimit(Logging.ERR_MAX_FILE_SIZE_PROPERTY, errors), 2, true); + handler.setFormatter(new LogFormatter()); + handler.setLevel(Level.WARNING); + root.addHandler(handler); + } catch (IOException e) { + errors.put(err_file, e); + } + } + } catch (SecurityException e) { + // ignore -- does not have persmission to change configuration + } + return errors; + } + + Object getPeer(String name) { + return Logger.getLogger(name); + } + + String getName(Object peer) { + return ((Logger)peer).getName(); + } + + boolean debugEnabled(Object peer) { + return ((Logger)peer).isLoggable(Level.FINE); + } + + void setDebugEnabled(Object peer, boolean debug_enabled) { + ((Logger)peer).setLevel(debug_enabled ? Level.ALL : Level.INFO); + } + + void log(Object peer, Level level, String msg, Throwable t) { + ((Logger)peer).log(level, msg, t); + } + + // ========== Utility methods ========== + + /** + * Safely, from security point of view, gets system property. + */ + static String getProperty(String key, String def) { + // For applets we need to be ready for security exception in getProperty() call + try { + return System.getProperty(key, def); + } catch (SecurityException e) { + return def; + } + } + + static int getLimit(String key, Map errors) { + String value = getProperty(key, Logging.DEFAULT_MAX_FILE_SIZE).trim(); + int multiplier = 1; + if (value.endsWith("K") || value.endsWith("k")) { + multiplier = 1024; + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("M") || value.endsWith("m")) { + multiplier = 1024 * 1024; + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("G") || value.endsWith("g")) { + multiplier = 1024 * 1024 * 1024; + value = value.substring(0, value.length() - 1); + } + try { + return Integer.valueOf(value) * multiplier; + } catch (NumberFormatException e) { + errors.put(key, e); + return 0; + } + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/DxFeedPatternLayout.java b/java_console/logging-api/src/main/java/com/devexperts/logging/DxFeedPatternLayout.java new file mode 100644 index 0000000000..2ab6e19f3f --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/DxFeedPatternLayout.java @@ -0,0 +1,114 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.DefaultConfiguration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.layout.AbstractStringLayout; +import org.apache.logging.log4j.core.layout.ByteBufferDestination; +import org.apache.logging.log4j.core.layout.Encoder; +import org.apache.logging.log4j.core.pattern.MessagePatternConverter; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Custom pattern layout for log4j2. Message formatting is delegated to {@link LogFormatter}. + */ +@SuppressWarnings("unused") //used by Log4j2 +@Plugin(name = "dxFeedPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public class DxFeedPatternLayout extends AbstractStringLayout { + private static final String LINE_SEP = DefaultLogging.getProperty("line.separator", "\n"); + + private final BiConsumer msgConsumer; + private final LogFormatter logFormatter; + + private DxFeedPatternLayout(Configuration configuration) { + super(configuration, Charset.defaultCharset(), null, null); + MessagePatternConverter messagePatternConverter = MessagePatternConverter.newInstance(configuration, null); + msgConsumer = (o, sb) -> { + // Format message + messagePatternConverter.format(o, sb); + if (o instanceof LogEvent) { + // Format exception + Throwable throwable = ((LogEvent)o).getThrown(); + if (throwable != null) { + sb.append(LINE_SEP); + StringWriter w = new StringWriter(); + throwable.printStackTrace(new PrintWriter(w)); + sb.append(w.getBuffer()); + // Remove extra line separator + sb.setLength(sb.length() - LINE_SEP.length()); + } + } + }; + logFormatter = new LogFormatter(); + } + + @Override + public String toSerializable(LogEvent event) { + StringBuilder text = getStringBuilder(); + String s = format(event, text).toString(); + trimToMaxSize(text); + return s; + } + + @Override + public void encode(LogEvent event, ByteBufferDestination destination) { + StringBuilder text = getStringBuilder(); + format(event, text); + Encoder encoder = getStringBuilderEncoder(); + encoder.encode(text, destination); + trimToMaxSize(text); + } + + private StringBuilder format(LogEvent event, StringBuilder text) { + char level = event.getLevel().name().charAt(0); + logFormatter.format(level, event.getTimeMillis(), event.getThreadName(), event.getLoggerName(), msgConsumer, + event, text); + return text; + } + + @Override + public Map getContentFormat() { + return Collections.emptyMap(); + } + + @Override + public String toString() { + return getContentFormat().toString(); + } + + public static DxFeedPatternLayout createDefaultLayout() { + return createDefaultLayout(null); + } + + /** + * Creates a DxFeedPatternLayout using the default options and the given configuration. Options include using UTF-8. + */ + @PluginFactory + public static DxFeedPatternLayout createDefaultLayout(@PluginConfiguration Configuration configuration) { + if (configuration == null) + configuration = new DefaultConfiguration(); + return new DxFeedPatternLayout(configuration); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/Log4j2Logging.java b/java_console/logging-api/src/main/java/com/devexperts/logging/Log4j2Logging.java new file mode 100644 index 0000000000..a9da590cc0 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/Log4j2Logging.java @@ -0,0 +1,187 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.appender.RollingFileAppender; +import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.NullConfiguration; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.apache.logging.log4j.core.layout.AbstractStringLayout; +import org.apache.logging.log4j.message.SimpleMessage; +import org.apache.logging.log4j.status.StatusLogger; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; + +import static org.apache.logging.log4j.Level.DEBUG; +import static org.apache.logging.log4j.Level.ERROR; +import static org.apache.logging.log4j.Level.INFO; +import static org.apache.logging.log4j.Level.OFF; +import static org.apache.logging.log4j.Level.WARN; +import static org.apache.logging.log4j.core.Filter.Result.ACCEPT; +import static org.apache.logging.log4j.core.Filter.Result.DENY; +import static org.apache.logging.log4j.core.config.ConfigurationSource.NULL_SOURCE; + +/** + * Logging implementation that uses log4j2 logging facilities. + */ +class Log4j2Logging extends DefaultLogging { + private static final String FQCN = Logging.class.getName() + "."; + + static { + StatusLogger.getLogger().setLevel(OFF); + } + + @Override + Map configure() { + LoggerContext ctx = (LoggerContext)LogManager.getContext(false); + if (ctx.getConfiguration().getConfigurationSource() != NULL_SOURCE) + return Collections.emptyMap(); // do nothing since log4j2 was already configured + return configureLogFile(getProperty(Logging.LOG_FILE_PROPERTY, null)); + } + + private static Map reconfigure(String logFile) { + LoggerContext ctx = (LoggerContext)LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + + Map errors = new LinkedHashMap<>(); + config.getRootLogger().setLevel(DEBUG); + String errFile = getProperty(Logging.ERR_FILE_PROPERTY, null); + for (Map.Entry entry : config.getRootLogger().getAppenders().entrySet()) { + entry.getValue().stop(); + // Safe to delete here since config.getRootLogger().getAppenders() returns new map + config.getRootLogger().removeAppender(entry.getKey()); + } + + Appender appender = null; + if (logFile != null) { + try { + appender = createFileAppender("common", logFile, Logging.LOG_MAX_FILE_SIZE_PROPERTY, errors); + } catch (Exception e) { + errors.put(logFile, e); + } + } + + if (appender == null) + appender = ConsoleAppender.newBuilder() + .withName("common") + .withLayout(getDetailedLayout()) + .setTarget(ConsoleAppender.Target.SYSTEM_OUT) + .build(); + + config.getRootLogger().addAppender(appender, DEBUG, + errFile == null ? null : ThresholdFilter.createFilter(WARN, DENY, ACCEPT)); + + if (errFile != null) { + try { + Appender errAppender = createFileAppender("error", errFile, Logging.ERR_MAX_FILE_SIZE_PROPERTY, errors); + config.getRootLogger().addAppender(errAppender, WARN, ThresholdFilter.createFilter(WARN, ACCEPT, DENY)); + } catch (Exception e) { + errors.put(errFile, e); + } + } + ctx.updateLoggers(); + return errors; + } + + private static AbstractStringLayout getDetailedLayout() { + return DxFeedPatternLayout.createDefaultLayout(null); + } + + private static RollingFileAppender createFileAppender(String name, String logFile, String maxSizeKey, + Map errors) + { + RollingFileAppender.Builder builder = RollingFileAppender.newBuilder(); + builder.setConfiguration(new NullConfiguration()); + builder.withName(name); + builder.withLayout(getDetailedLayout()); + builder.withFileName(logFile); + builder.withFilePattern(logFile); + builder.withAppend(true); + builder.withImmediateFlush(true); + + int limit = getLimit(maxSizeKey, errors); + if (limit == 0) + limit = 900 * 1024 * 1024; // Default in Logging.DEFAULT_MAX_FILE_SIZE + builder.withPolicy(SizeBasedTriggeringPolicy.createPolicy(Integer.toString(limit))); + + return builder.build(); + } + + @Override + Map configureLogFile(String logFile) { + return reconfigure(logFile); + } + + @Override + Object getPeer(String name) { + return LogManager.getLogger(name); + } + + @Override + String getName(Object peer) { + return ((Logger)peer).getName(); + } + + @Override + boolean debugEnabled(Object peer) { + return ((Logger)peer).isDebugEnabled(); + } + + @Override + void setDebugEnabled(Object peer, boolean debugEnabled) { + ((Logger)peer).setLevel(debugEnabled ? DEBUG : INFO); + } + + @Override + void log(Object peer, Level level, String msg, Throwable t) { + org.apache.logging.log4j.Level priority; + if (level.intValue() <= Level.FINE.intValue()) + priority = DEBUG; + else if (level.intValue() <= Level.INFO.intValue()) + priority = INFO; + else if (level.intValue() <= Level.WARNING.intValue()) + priority = WARN; + else + priority = ERROR; + + if (!((Logger)peer).isEnabled(priority)) + return; + + // Before calling log4j logger we must clear "interrupted" flag from current thread. + // If this flag is "true", log4j will log error in 1 appender only (and probably clear the flag). + // We will re-establish "interrupted" flag later. + boolean interrupted = Thread.interrupted(); + try { + ((Logger)peer).logMessage(FQCN, priority, null, new SimpleMessage(msg == null ? "" : msg), t); + } catch (Exception e) { + System.err.println(new LogFormatter().format('E', System.currentTimeMillis(), + Thread.currentThread().getName(), "Log4j", e + " during logging of " + msg)); + if (!(e instanceof IllegalStateException) || e.getMessage() == null || + !e.getMessage().equals("Current state = FLUSHED, new state = CODING")) + { + e.printStackTrace(System.err); + } + } finally { + if (interrupted) + Thread.currentThread().interrupt(); + } + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/LogFormatter.java b/java_console/logging-api/src/main/java/com/devexperts/logging/LogFormatter.java new file mode 100644 index 0000000000..2c3eb86700 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/LogFormatter.java @@ -0,0 +1,185 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import com.devexperts.util.TimeUtil; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.function.BiConsumer; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Thread-safe formatter for log messages. + * It is used for formatting log4j, log4j2 and {@link java.util.logging} log messages. + * Performs conversion of thread names according to patterns specified in configuration file. + *

+ * If the system property {@code logformatter.properties} is specified, then it should contain + * an URL to the configuration file. Otherwise, configuration is loaded from classpath, using + * /META-INF/logformatter.properties file. + *

+ * The format of the file is: + *

    + *
  • pattern=replacement + *
  • "Pattern" uses regular expression syntax. + * You can escape "=" in pattern with "\=" syntax. + *
  • "Replacement" string can refer to capturing groups defined in pattern using usual + * regular expression syntax "$n", where "n" stands for the number of the group. + *
  • ISO 8859-1 encoding is used. + *
  • Empty lines and lines starting with # or ! are ignored. + * Lines containing wrong patterns are ignored. + *
+ * Configuration file is loaded during class loading. + * Any errors which occur in this class are printed in {@code System.err}. + *

+ * Sample configuration file can be found in etc/logformatter.properties. + *

+ * This class is not intended to be used standalone. + * It is a part of implementation of {@link com.devexperts.logging} package. + * + * @see DetailedLogLayout + * @see DxFeedPatternLayout + */ +@ThreadSafe +public class LogFormatter extends Formatter { + public static final String CONFIG_FILE_PROPERTY = "logformatter.properties"; + public static final String DEFAULT_CONFIG_FILE = "/META-INF/logformatter.properties"; + + private static final String LINE_SEP = DefaultLogging.getProperty("line.separator", "\n"); + private static final BiConsumer STRING_FORMAT_CONSUMER = (s, sb) -> sb.append(s); + + // ============== Instance ================ + private final ThreadLocal formatter; + + public LogFormatter() { + this(TimeZone.getDefault()); + } + + public LogFormatter(TimeZone zone) { + formatter = ThreadLocal.withInitial(() -> new LocalFormatter(zone)); + } + + /** + * Used by {@link java.util.logging} logging. + * Formats messages with the same format as for log4j. + */ + @Override + public String format(LogRecord record) { + String s = format(getLevelChar(record.getLevel()), + record.getMillis(), Thread.currentThread().getName(), + record.getLoggerName(), formatMessage(record)); + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + sw.write(s); + record.getThrown().printStackTrace(new PrintWriter(sw)); + s = sw.toString(); + } + return s; + } + + /** + * Formats log message. + * + * @return Formatted message. + * @throws NullPointerException when threadName, loggerName, or msg are {@code null}. + */ + public String format(char levelChar, long time, String threadName, String loggerName, String msg) { + StringBuilder out = formatter.get().appendTo; + out.setLength(0); + try { + format(levelChar, time, threadName, loggerName, STRING_FORMAT_CONSUMER, msg, out); + return out.toString(); + } finally { + boolean trim = out.length() > 1000; + out.setLength(0); + if (trim) + out.trimToSize(); + } + } + + void format(char levelChar, long time, String threadName, String loggerName, + BiConsumer msgConsumer, Object msg, StringBuilder out) + { + out.append(levelChar).append(" "); + formatter.get().appendTime(time, out); + out.append(" "); + int threadPosition = out.length(); + out.append("["); + out.append(ThreadNameFormatter.formatThreadName(time, threadName)); + out.append("] "); + out.append(loggerName, loggerName.lastIndexOf('.') + 1, loggerName.length()); + out.append(" - "); + int messagePosition = out.length(); + msgConsumer.accept(msg, out); + out.append(LINE_SEP); + if (out.length() > messagePosition && out.charAt(messagePosition) == '\b') + out.delete(threadPosition, messagePosition + 1); + } + + static char getLevelChar(Level level) { + int levelInt = level.intValue(); + if (levelInt <= Level.FINEST.intValue()) + return 'T'; + if (levelInt <= Level.FINE.intValue()) + return 'D'; + if (levelInt <= Level.INFO.intValue()) + return 'I'; + if (levelInt <= Level.WARNING.intValue()) + return 'W'; + return 'E'; + } + + private static class LocalFormatter { + private final Calendar calendar; + private final char[] timeBuffer = new char[17]; // fixed-size buffer for time data "yyMMdd HHmmss.SSS" + private final StringBuilder appendTo = new StringBuilder(); + + private long translatedMinute; + + private LocalFormatter(TimeZone zone) { + calendar = Calendar.getInstance(zone); + Arrays.fill(timeBuffer, 0, 17, ' '); + timeBuffer[13] = '.'; + } + + private void appendTime(long time, StringBuilder out) { + if (time < translatedMinute || time >= translatedMinute + TimeUtil.MINUTE) { + // set year, month, day, hour and minute + calendar.setTimeInMillis(time); + translatedMinute = calendar.getTime().getTime() - calendar.get(Calendar.SECOND) * 1000 - calendar.get(Calendar.MILLISECOND); + print2(0, calendar.get(Calendar.YEAR)); + print2(2, calendar.get(Calendar.MONTH) + 1); + print2(4, calendar.get(Calendar.DAY_OF_MONTH)); + print2(7, calendar.get(Calendar.HOUR_OF_DAY)); + print2(9, calendar.get(Calendar.MINUTE)); + } + + // set seconds and milliseconds + int millis = (int)(time - translatedMinute); + print2(11, millis / 1000); + print2(14, millis / 10); + timeBuffer[16] = (char)('0' + millis % 10); + out.append(timeBuffer); + } + + private void print2(int offset, int value) { + timeBuffer[offset] = (char)('0' + (value / 10) % 10); + timeBuffer[offset + 1] = (char)('0' + value % 10); + } + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/Logging.java b/java_console/logging-api/src/main/java/com/devexperts/logging/Logging.java new file mode 100644 index 0000000000..6c2c95f736 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/Logging.java @@ -0,0 +1,235 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; + +/** + * Main logging class. + * It supports use of both log4j and {@link java.util.logging} logging facilities. + *

First it tries to use log4j logging. If this attempt fails, it uses {@link java.util.logging} logging, + * so you'll always have some logging running. + *

Usage pattern: + *
public class SomeClass { + *
private static final Logging log = Logging.getLogging(SomeClass.class); + *
} + *
+ * + * @see Log4jLogging + * @see DefaultLogging + * @see LogFormatter + */ +public class Logging { + private static final boolean TRACE_LOGGING = Logging.class.desiredAssertionStatus(); + + private static final int FINEST_INT = Level.FINEST.intValue(); + private static final int FINE_INT = Level.FINE.intValue(); + + public static final String LOG_CLASS_NAME = "log.className"; + public static final String LOG_FILE_PROPERTY = "log.file"; + public static final String ERR_FILE_PROPERTY = "err.file"; + public static final String LOG_MAX_FILE_SIZE_PROPERTY = "log.maxFileSize"; + public static final String ERR_MAX_FILE_SIZE_PROPERTY = "err.maxFileSize"; + public static final String DEFAULT_MAX_FILE_SIZE = "900M"; + + private static final ConcurrentMap INSTANCES = new ConcurrentHashMap<>(); + private static final DefaultLogging IMPL = configure(DefaultLogging.getProperty(LOG_CLASS_NAME, "")); + + public static Logging getLogging(Class clazz) { + return getLogging(clazz.getName()); + } + + public static Logging getLogging(String name) { + Logging logging = INSTANCES.get(name); + if (logging != null) + return logging; + INSTANCES.putIfAbsent(name, new Logging(name)); + return INSTANCES.get(name); + } + + /** + * Programmatically reconfigures logging to a specified file. This method + * overrides the value of {@link #LOG_FILE_PROPERTY} system property. + */ + public static void configureLogFile(String log_file) { + reportErrors(IMPL, IMPL.configureLogFile(log_file)); + } + + // ========== Instance ========= + + private final Object peer; + + /** + * This constructor is designed for abstract framework classes like BeanBase or + * DAOBase that extend Logging to decorate messages by + * overriding {@link #decorateLogMessage(String)} method. + */ + protected Logging() { + peer = IMPL.getPeer(getClass().getName()); + } + + protected Logging(String name) { + peer = IMPL.getPeer(name); + } + + /** + * Returns category name of this logging. + */ + public final String getName() { + return IMPL.getName(peer); + } + + /** + * Changes default {@link #debugEnabled()} behaviour for this logging instance. + * Use this method to turn off debugging information for classes that do not + * need to print their debugging information in production environment. + */ + public final void configureDebugEnabled(boolean defaultDebugEnabled) { + IMPL.setDebugEnabled(peer, Boolean.valueOf(DefaultLogging.getProperty(getName() + ".debug", + String.valueOf(defaultDebugEnabled)))); + } + + public final boolean debugEnabled() { + return IMPL.debugEnabled(peer); + } + + public final void trace(String message) { + log(Level.FINEST, message, null); + } + + public final void debug(String message) { + log(Level.FINE, message, null); + } + + public final void debug(String message, Throwable t) { + log(Level.FINE, message, t); + } + + public final void info(String message) { + log(Level.INFO, message, null); + } + + public final void info(String message, Throwable t) { + log(Level.INFO, message, t); + } + + public final void warn(String message) { + log(Level.WARNING, message, null); + } + + public final void warn(String message, Throwable t) { + log(Level.WARNING, message, t); + } + + public final void error(String message) { + log(Level.SEVERE, message, null); + } + + public final void error(String message, Throwable t) { + log(Level.SEVERE, message, t); + } + + public final RuntimeException log(RuntimeException e) { + log(Level.SEVERE, e.getMessage(), e); + return e; + } + + /** + * Decorates log message (reformatting, auditing, etc). + * This method is invoked one time for each logging event. + */ + protected String decorateLogMessage(String msg) { + return msg; + } + + // ========== Internal ========== + + private void log(Level level, String msg, Throwable t) { + if (TRACE_LOGGING) + TraceLogging.log(getName(), level, decorateLogMessage(msg), t); + int levelInt = level.intValue(); + if (levelInt <= FINEST_INT) + return; // trace never goes to regular log + if (levelInt <= FINE_INT && !IMPL.debugEnabled(peer)) + return; + try { + msg = decorateLogMessage(msg == null ? "" : msg); + } catch (Throwable tt) { + IMPL.log(peer, Level.SEVERE, "Failed to decorate log message", tt); + } + IMPL.log(peer, level, msg, t); + } + + /** + * At first tries to use logging from passed class name. If this attempt fails, tries to use log4j logging. + * If this attempt fails, it uses log4j2 logging. If this attempt fails, it uses {@link java.util.logging} logging. + * + * @return Logging implementation + */ + private static DefaultLogging configure(String className) { + DefaultLogging impl = null; + Map errors = new LinkedHashMap<>(); + if (!className.isEmpty()) { + try { + impl = (DefaultLogging)Class.forName(className).newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure with passed class name + impl = null; + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { + errors.put(className + " link", new IllegalStateException(t)); + } + } + } + if (impl == null) { + try { + impl = (DefaultLogging)Class.forName("com.devexperts.logging.Log4jLogging").newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure log4j + impl = null; + // LinkageError means that log4j is not found at all, otherwise it was found but our config is wrong + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { + errors.put("log4j link", new IllegalStateException(t)); + } + } + } + if (impl == null) { + try { + impl = (DefaultLogging)Class.forName("com.devexperts.logging.Log4j2Logging").newInstance(); + errors.putAll(impl.configure()); + } catch (Throwable t) { + // failed to configure log4j2 + impl = null; + if (!(t instanceof LinkageError) && !(t.getCause() instanceof LinkageError)) { + errors.put("log4j2 link", new IllegalStateException(t)); + } + } + } + if (impl == null) { + impl = new DefaultLogging(); + errors.putAll(impl.configure()); + } + + reportErrors(impl, errors); + return impl; + } + + private static void reportErrors(DefaultLogging impl, Map errors) { + for (Map.Entry entry : errors.entrySet()) + impl.log(impl.getPeer("config"), Level.SEVERE, entry.getKey(), entry.getValue()); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/ThreadNameFormatter.java b/java_console/logging-api/src/main/java/com/devexperts/logging/ThreadNameFormatter.java new file mode 100644 index 0000000000..96163c2de1 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/ThreadNameFormatter.java @@ -0,0 +1,179 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import com.devexperts.util.IndexedSet; +import com.devexperts.util.QuickSort; +import com.devexperts.util.SynchronizedIndexedSet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +class ThreadNameFormatter implements Comparable { + + /** + * Configuration as a set of pairs (pattern, replacement). + */ + private static final Map PATTERNS = new LinkedHashMap<>(); + + private static final int MAX_NAME_CONVERSIONS_CACHE_SIZE = 1000; + + /** + * Thread name replacement cache: (thread name, replacement string). + */ + private static final IndexedSet NAME_CONVERSIONS = SynchronizedIndexedSet.create(ThreadNameFormatter::getThreadName); + + static { + loadPatterns(); + } + + private static void loadPatterns() { + InputStream config_input_stream = null; + try { + String config_path = DefaultLogging.getProperty(LogFormatter.CONFIG_FILE_PROPERTY, null); + if (config_path != null) + throw new UnsupportedOperationException("loadPatterns"); +/* + try { + config_input_stream = new URLInputStream(config_path); + } catch (IOException e) { + System.err.println("Cannot find log formatter configuration file: '" + config_path + "'"); + System.err.println("No thread name conversion will be performed."); + return; + } +*/ else + config_input_stream = LogFormatter.class.getResourceAsStream(LogFormatter.DEFAULT_CONFIG_FILE); + + if (config_input_stream == null) + return; + + BufferedReader reader = new BufferedReader( + new InputStreamReader(config_input_stream, Charset.forName("ISO-8859-1"))); + + Pattern config_line_pattern = Pattern.compile("((?:[^=]|(?:\\\\=))*[^\\\\=])=(.*)"); + Pattern whitespace_line_pattern = Pattern.compile("\\s*"); + Pattern comment_line_pattern = Pattern.compile("#.*|!.*"); + String line; + Set patterns_set = new HashSet(); + while ((line = reader.readLine()) != null) { + Matcher config_line_matcher = config_line_pattern.matcher(line); + // If it is whitespace or comment line + if (whitespace_line_pattern.matcher(line).matches() || comment_line_pattern.matcher(line).matches()) + continue; + if (!config_line_matcher.matches()) { + System.err.println("The following line cannot be parsed in log formatter configuration file: '" + line + "'"); + continue; + } + String config_pattern = config_line_matcher.group(1); + String config_replacement = config_line_matcher.group(2); + if (!patterns_set.add(config_pattern)) { + System.err.println("Duplicate pattern found in log formatter configuration file: '" + config_pattern + "'"); + continue; + } + try { + PATTERNS.put(Pattern.compile(config_pattern), config_replacement); + } catch (PatternSyntaxException e) { + System.err.println("Cannot parse config pattern in log formatter configuration file: '" + config_pattern + "'"); + } + } + } catch (IOException e) { + // Do not wish to log using logger until initialization has completed. + System.err.println("Cannot read log formatter configuration file"); + e.printStackTrace(System.err); + } finally { + try { + if (config_input_stream != null) { + config_input_stream.close(); + } + } catch (IOException e) { + // Do not wish to log using logger until initialization has completed. + System.err.println("Cannot close log formatter configuration file"); + e.printStackTrace(System.err); + } + } + } + + /** + * Formats thread name according to thread name conversion rules. + * + * @return Formatted thread name + */ + static String formatThreadName(long time, String thread_name) { + ThreadNameFormatter entry = NAME_CONVERSIONS.getByKey(thread_name); + if (entry == null) { + cleanupNameConversionsIfNeeded(); + entry = new ThreadNameFormatter(thread_name, calculateThreadNameReplacement(thread_name)); + NAME_CONVERSIONS.put(entry); + } + entry.last_time = time; + return entry.replacement_name; + } + + private static void cleanupNameConversionsIfNeeded() { + if (NAME_CONVERSIONS.size() <= MAX_NAME_CONVERSIONS_CACHE_SIZE) + return; // everything is Ok + + synchronized (NAME_CONVERSIONS) { + if (NAME_CONVERSIONS.size() <= MAX_NAME_CONVERSIONS_CACHE_SIZE) + return; // everything is Ok + ThreadNameFormatter[] entries = NAME_CONVERSIONS.toArray(new ThreadNameFormatter[NAME_CONVERSIONS.size()]); + QuickSort.sort(entries); + for (int i = 0; i < entries.length - MAX_NAME_CONVERSIONS_CACHE_SIZE / 2; i++) + NAME_CONVERSIONS.removeKey(entries[i].thread_name); + } + } + + private static String calculateThreadNameReplacement(String thread_name) { + for (Map.Entry entry : PATTERNS.entrySet()) { + Matcher matcher = entry.getKey().matcher(thread_name); + if (matcher.matches()) { + String config_replacement = entry.getValue(); + try { + return matcher.replaceAll(config_replacement); + } catch (IndexOutOfBoundsException e) { + // The replacement string refers to a capturing group that does not exist in the pattern. + // To prevent cycling log it as is. + // Incorrect replacement. Just use thread name. + System.err.println("Cannot parse replacement string in log formatter configuration file: '" + config_replacement + "'"); + } + } + } + return thread_name; + } + + final String thread_name; + final String replacement_name; + long last_time; // Atomicity, visibility and consistency of this field are unimportant. + + ThreadNameFormatter(String thread_name, String replacement_name) { + this.thread_name = thread_name; + this.replacement_name = replacement_name; + } + + private String getThreadName() { + return thread_name; + } + + public int compareTo(ThreadNameFormatter o) { + return last_time < o.last_time ? -1 : last_time > o.last_time ? 1 : 0; + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/TraceLogging.java b/java_console/logging-api/src/main/java/com/devexperts/logging/TraceLogging.java new file mode 100644 index 0000000000..860655a901 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/TraceLogging.java @@ -0,0 +1,146 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.logging; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +/** + * This is a small in-memory cyclic buffer to keep the log for debugging concurrency problems in order to + * reconstruct what was going on immediately before tests crashes, without actually writing all debug logging + * to the log file or console normally. It is used by some tests when assertions are enabled, + * otherwise this class should not even be loaded. + */ +public class TraceLogging { + private static final int STOPPED_INDEX = -1; + private static final int SIZE = Integer.parseInt(System.getProperty("TraceLogging.size", "4096")); // must be power of 2 + private static final int MASK = SIZE - 1; + + private static final int THREAD_OFS = 0; + private static final int NAME_OFS = 1; + private static final int LEVEL_OFS = 2; + private static final int MSG_OFS = 3; + private static final int THROWABLE_OFS = 4; + private static final int DATA_CNT = 5; + + private static final long[] timeQueue = new long[SIZE]; + private static final Object[] dataQueue = new Object[SIZE * DATA_CNT]; + private static final AtomicInteger index = new AtomicInteger(STOPPED_INDEX); + private static int lastIndex = STOPPED_INDEX; + + static { + if ((SIZE & MASK) != 0) + throw new RuntimeException("Size must be a power of two"); + } + + /** + * Restarts trace logging from scratch (old log entries are cleared). + * Use it at the beginning of the test. + */ + public static synchronized void restart() { + Arrays.fill(dataQueue, null); + lastIndex = STOPPED_INDEX; + index.compareAndSet(STOPPED_INDEX, 0); + } + + /** + * Stops trace logging. + */ + public static void stop() { + stopIndex(-1); + } + + /** + * Adds log entry. It is invoked from {@link Logging#log(Level, String, Throwable)} method when + * assertions are enabled. + */ + public static void log(String loggerName, Level level, String msg, Throwable t) { + append(nextIndex(), loggerName, level, msg, t); + } + + /** + * Adds last entry and stops trace logging. + */ + public static void logAndStop(Class where, String msg) { + logAndStop(where, msg, null); + } + + /** + * Adds last entry with exception and stops trace logging. + */ + public static void logAndStop(Class where, String msg, Throwable t) { + append(stopIndex(0), where.getName(), Level.INFO, msg, t); + } + + private static void append(int i, String loggerName, Level level, String msg, Throwable t) { + if (i < 0) + return; + timeQueue[i] = System.currentTimeMillis(); + dataQueue[i * DATA_CNT + THREAD_OFS] = Thread.currentThread(); + dataQueue[i * DATA_CNT + NAME_OFS] = loggerName; + dataQueue[i * DATA_CNT + LEVEL_OFS] = level; + dataQueue[i * DATA_CNT + MSG_OFS] = msg; + dataQueue[i * DATA_CNT + THROWABLE_OFS] = t; + } + + /** + * Dumps last entries from this trace log. + * It should be called after {@link #stop()} or {@link #logAndStop(Class, String)}. + * It does nothing if called more than once after stop or before stop. + */ + public static synchronized void dump(PrintStream out, String title) { + int stop = lastIndex; + if (stop < 0) + return; + lastIndex = STOPPED_INDEX; + LogFormatter formatter = new LogFormatter(); + out.println("********************** Dump trace log for " + title); + int i = stop; + do { + i = (i + 1) & MASK; + Thread thread = (Thread)dataQueue[i * DATA_CNT + THREAD_OFS]; + if (thread == null) + continue; + String loggerName = (String)dataQueue[i * DATA_CNT + NAME_OFS]; + Level level = (Level)dataQueue[i * DATA_CNT + LEVEL_OFS]; + String msg = (String)dataQueue[i * DATA_CNT + MSG_OFS]; + Throwable t = (Throwable)dataQueue[i * DATA_CNT + THROWABLE_OFS]; + long time = timeQueue[i]; + out.print("* "); + out.print(formatter.format(LogFormatter.getLevelChar(level), time, thread.getName(), loggerName, msg)); + if (t != null) + t.printStackTrace(out); + } while (i != stop); + out.println("********************** Done trace log for " + title); + } + + private static int nextIndex() { + int result; + do { + result = index.get(); + } while (result >= 0 && !index.compareAndSet(result, (result + 1) & MASK)); + return result; + } + + private static synchronized int stopIndex(int lastOffset) { + int result; + do { + result = index.get(); + } while (result >= 0 && !index.compareAndSet(result, STOPPED_INDEX)); + if (result >= 0) + lastIndex = (result + lastOffset) & MASK; + return result; + } + +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/logging/package.html b/java_console/logging-api/src/main/java/com/devexperts/logging/package.html new file mode 100644 index 0000000000..95c5d9998a --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/logging/package.html @@ -0,0 +1,16 @@ + + + +Provides logging classes. + + diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/AbstractConcurrentSet.java b/java_console/logging-api/src/main/java/com/devexperts/util/AbstractConcurrentSet.java new file mode 100644 index 0000000000..30752ab7cf --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/AbstractConcurrentSet.java @@ -0,0 +1,221 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.lang.reflect.Array; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +/** + * Provides a skeletal implementation of the {@link Set Set} interface to minimize the effort + * required to implement this interface. Unlike {@link AbstractSet AbstractSet} skeletal implementation, + * this one is more forgiving to concurrent modifications of this set during implemented bulk operations. + */ +public abstract class AbstractConcurrentSet implements Set { + + // ========== Construction and Clearing ========== + + /** + * Sole constructor; for invocation by subclass constructors, typically implicit. + */ + protected AbstractConcurrentSet() {} + + /** + * Removes all of the elements from this set. + *

+ * This implementation iterates all elements of this set and removes them using {@link Iterator#remove()} method. + */ + public void clear() { + for (Iterator it = iterator(); it.hasNext();) { + it.next(); + it.remove(); + } + } + + // ========== Query Operations ========== + + /** + * Tests if this set has no elements. + *

+ * This implementation checks emptiness using {@link #size()} method. + */ + public boolean isEmpty() { + return size() == 0; + } + + /** + * Returns an array containing all of the elements in this set. + * Obeys the general contract of the {@link Collection#toArray()} method. + *

+ * This implementation iterates all elements of this set and adds them into the array. + */ + public Object[] toArray() { + return toArrayImpl(null); + } + + /** + * Returns an array containing all of the elements in this set whose runtime type + * is that of the specified array. + * Obeys the general contract of the {@link Collection#toArray(Object[])} method. + *

+ * This implementation iterates all elements of this set and adds them into the array. + */ + public T[] toArray(T[] a) { + return toArrayImpl(a); + } + + @SuppressWarnings("unchecked") + private T[] toArrayImpl(T[] a) { + // If (a == null) then returned array shall be of exact length, otherwise it can be larger. + int size = size(); // Atomic read. + Object[] result = a == null ? new Object[size] : a.length >= size ? a : + (Object[])Array.newInstance(a.getClass().getComponentType(), size); + int n = 0; + for (E o : this) { + if (n >= result.length) { + // More elements were added concurrently. Enlarge result array. + // Grow twice in size, but do not fail when (n == 0). + Object[] tmp = (Object[])Array.newInstance(result.getClass().getComponentType(), n + n + 1); + System.arraycopy(result, 0, tmp, 0, n); + result = tmp; + } + result[n++] = o; + } + if (n < result.length && a == null) { + // Shrink allocated array to exact size. + Object[] tmp = new Object[n]; + System.arraycopy(result, 0, tmp, 0, n); + result = tmp; + } + if (n < result.length) + result[n] = null; + return (T[])result; + } + + // ========== Bulk Operations ========== + + /** + * Tests if this set contains all of the elements in the specified collection. + *

+ * This implementation iterates all elements of specified collection and tests + * them one-by-one using {@link #contains(Object) contains(element)} method. + */ + public boolean containsAll(Collection c) { + for (Object o : c) + if (!contains(o)) + return false; + return true; + } + + /** + * Adds all of the elements in the specified collection into this set and + * returns true if this operation has increased the size of this set. + *

+ * This implementation iterates all elements of specified collection and adds + * them one-by-one using {@link #add(Object) add(element)} method. + */ + public boolean addAll(Collection c) { + boolean modified = false; + for (E o : c) + if (add(o)) + modified = true; + return modified; + } + + /** + * Removes all of the elements in the specified collection from this set and + * returns true if this operation has decreased the size of this set. + *

+ * This implementation compares size of specified collection with the size of this set, + * then iterates smaller collection and removes elements that need to be removed. + */ + public boolean removeAll(Collection c) { + boolean modified = false; + if (size() > c.size()) { + for (Object o : c) + if (remove(o)) + modified = true; + } else { + for (Iterator it = iterator(); it.hasNext();) + if (c.contains(it.next())) { + it.remove(); + modified = true; + } + } + return modified; + } + + /** + * Retains only the elements in this set that are contained in the specified collection. + *

+ * This implementation iterates all elements of this set, checks if they are contained in + * the specified collection, and removes them if needed using {@link Iterator#remove()} method. + */ + public boolean retainAll(Collection c) { + boolean modified = false; + for (Iterator it = iterator(); it.hasNext();) + if (!c.contains(it.next())) { + it.remove(); + modified = true; + } + return modified; + } + + // ========== Comparison and Hashing ========== + + /** + * Compares the specified object with this set for equality. + * Obeys the general contract of the {@link Set#equals(Object)} method. + *

+ * This implementation compares size of specified set with the size of this set and then + * checks element containment using {@link #containsAll(Collection) containsAll((Set)o)} method. + */ + public boolean equals(Object o) { + return o == this || o instanceof Set && size() == ((Set)o).size() && containsAll((Set)o); + } + + /** + * Returns the hash code value for this set. + * Obeys the general contract of the {@link Set#hashCode()} method. + *

+ * This implementation iterates all elements of this set and adds their hash codes. + */ + public int hashCode() { + int hash = 0; + for (E o : this) + if (o != null) + hash += o.hashCode(); + return hash; + } + + // ========== String Conversion ========== + + /** + * Returns a string representation of this set. + *

+ * This implementation iterates all elements of this set and concatenates their string representations. + */ + public String toString() { + StringBuilder sb = new StringBuilder(size() * 3 + 10); + sb.append("["); + String separator = ""; + for (E o : this) { + sb.append(separator); + sb.append(o); + separator = ", "; + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSet.java b/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSet.java new file mode 100644 index 0000000000..98ff6bbdb4 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSet.java @@ -0,0 +1,1407 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collector; +import javax.annotation.Nonnull; + +/** + * A collection that contains no duplicate elements according to specified identification strategy. + * The IndexedSet class implements and obeys general contracts of {@link Set Set} interface + * and provides additional benefits over standard implementations: + * + *

    + *
  • delegation of element identification to external strategy + *
  • concurrent asynchronous read access + *
  • smaller memory footprint and faster performance + *
+ * + *

The IndexedSet assumes that identity of an element can be represented by a variable number + * of attributes, therefore it delegates identification to an external strategy — the {@link IndexerFunction}. + * In order to fulfil contracts of {@link Map Map} interface and for convenience, the IndexedSet + * supports concept of explicit key object and also numeric key representation, but these + * identification means are optional and need not be supported by all strategies. + * + *

Note that the IndexedSet is not synchronized! Concurrent modifications of IndexedSet + * from multiple threads must be synchronized externally to preserve data integrity. + * On the other side, the IndexedSet fully supports concurrent asynchronous read operations, + * which works during concurrent modification by other thread. In case of concurrent modification + * each atomic read sees IndexedSet either before or after each atomic write operation. + * + *

The IndexedSet does not support null values, but it supports null keys + * if they are supported by corresponding {@link IndexerFunction}. The IndexedSet is serializable. + */ +public class IndexedSet extends AbstractConcurrentSet implements Cloneable, Serializable { + private static final long serialVersionUID = 0L; + + // 'private' fields and methods of IndexedSet class shall be accessed only from within IndexedSet class itself. + + private final IndexerFunction indexer; + private transient volatile Core core; + + // ========== static factory methods =========== + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT}. + */ + public static IndexedSet create() { + return new IndexedSet<>(); + } + + /** + * Creates new empty set with default identity indexer. + */ + public static IndexedSet createIdentity() { + return new IndexedSet<>((IndexerFunction.IdentityKey)(v -> v)); + } + + /** + * Creates new empty set with specified indexer. + */ + public static IndexedSet create(IndexerFunction indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified identity indexer. + */ + public static IndexedSet createIdentity(IndexerFunction.IdentityKey indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified int indexer. + */ + public static IndexedSet createInt(IndexerFunction.IntKey indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified long indexer. + */ + public static IndexedSet createLong(IndexerFunction.LongKey indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.IntKey indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #createLong(IndexerFunction.LongKey) createLong(indexer)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.LongKey indexer) { + return new IndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction indexer, int initialCapacity) { + return new IndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.IntKey indexer, int initialCapacity) { + return new IndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #createLong(IndexerFunction.LongKey) createLong(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.LongKey indexer, int initialCapacity) { + return new IndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction indexer, Collection c) { + return new IndexedSet<>(indexer, c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.IntKey indexer, Collection c) { + return new IndexedSet<>(indexer, c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #createLong(IndexerFunction.LongKey) createLong(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public static IndexedSet create(IndexerFunction.LongKey indexer, Collection c) { + return new IndexedSet<>(indexer, c); + } + + /** + * Creates a new set with default indexer containing specified elements. + */ + @SafeVarargs + public static IndexedSet of(V... objs) { + return new IndexedSet<>(Arrays.asList(objs)); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with default indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + @SuppressWarnings("unchecked") + public static Collector> collector() { + return collector((IndexerFunction)IndexerFunction.DEFAULT); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with default identity indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorIdentity() { + return collector((IndexerFunction.IdentityKey)(v -> v)); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collector(IndexerFunction indexer) { + return Collector.of(() -> create(indexer), IndexedSet::add, + (left, right) -> { left.addAll(right); return left; }, + Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified identity indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorIdentity(IndexerFunction.IdentityKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified int indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorInt(IndexerFunction.IntKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified long indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorLong(IndexerFunction.LongKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + * + * @deprecated Use {@link #collectorInt(IndexerFunction.IntKey) collectorInt(indexer)} + */ + @Deprecated + public static Collector> collector(IndexerFunction.IntKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code IndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#UNORDERED unordered} Collector. + * + * @deprecated Use {@link #collectorLong(IndexerFunction.LongKey) collectorLong(indexer)} + */ + @Deprecated + public static Collector> collector(IndexerFunction.LongKey indexer) { + return collector((IndexerFunction)indexer); + } + + // ========== Construction and Sizing Operations ========== + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT}. + */ + public IndexedSet() { + this(0); + } + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT} and specified initial capacity. + */ + @SuppressWarnings("unchecked") + public IndexedSet(int initialCapacity) { + this(IndexerFunction.DEFAULT, initialCapacity); + } + + /** + * Creates new empty set with specified indexer. + */ + protected IndexedSet(IndexerFunction indexer) { + this(indexer, 0); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)} + */ + @Deprecated + public IndexedSet(Indexer indexer) { + this((IndexerFunction) indexer); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + */ + @SuppressWarnings("unchecked") + protected IndexedSet(IndexerFunction indexer, int initialCapacity) { + if (indexer == null) + throw new NullPointerException("Indexer is null."); + this.indexer = indexer; + this.core = initialCapacity <= 0 ? (Core)Core.EMPTY_CORE : new Core(indexer, initialCapacity, GOLDEN_RATIO); // Atomic volatile write. + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public IndexedSet(Indexer indexer, int initialCapacity) { + this((IndexerFunction) indexer, initialCapacity); + } + + /** + * Creates a new set containing the elements in the specified collection. + * If specified collection is an {@link IndexedSet}, then new indexed set uses same indexer, + * otherwise it uses default indexer {@link IndexerFunction#DEFAULT}. + */ + @SuppressWarnings("unchecked") + public IndexedSet(Collection c) { + this(c instanceof IndexedSet ? ((IndexedSet) c).getIndexerFunction() : IndexerFunction.DEFAULT, c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + */ + protected IndexedSet(IndexerFunction indexer, Collection c) { + this(indexer, c.size()); + addAll(c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public IndexedSet(Indexer indexer, Collection c) { + this((IndexerFunction) indexer, c); + } + + /** + * Returns a shallow copy of this set - the values themselves are not cloned. + */ + @SuppressWarnings({"unchecked"}) + @Override + public IndexedSet clone() { + try { + IndexedSet result = (IndexedSet)super.clone(); + if (result.core != Core.EMPTY_CORE) + result.core = new Core<>(result.core); + return result; + } catch (CloneNotSupportedException e) { + throw new InternalError(); + } + } + + /** + * Increases the capacity of this set instance, if necessary, to ensure that it + * can hold at least the number of elements specified by the capacity argument. + *

+ * Returns this set instance for convenience. + */ + public IndexedSet withCapacity(int capacity) { + ensureCapacity(capacity); + return this; + } + + /** + * Adds all of the elements in the specified collection into this set. + *

+ * Returns this set instance for convenience. + */ + public IndexedSet withElements(Collection c) { + ensureCapacity(c.size()); + addAll(c); + return this; + } + + /** + * Increases the capacity of this set instance, if necessary, to ensure that it + * can hold at least the number of elements specified by the capacity argument. + */ + public void ensureCapacity(int capacity) { + core = core.ensureCapacity(indexer, capacity); // Atomic volatile read and write. + } + + /** + * Trims the capacity of this set instance to be the set's current size. + * An application can use this operation to minimize the storage of this set instance. + */ + public void trimToSize() { + core = core.trimToSize(indexer); // Atomic volatile read and write. + } + + /** + * Removes all elements from this set. + */ + @Override + public void clear() { + core = core.clear(); // Atomic volatile read and write. + } + + // ========== Query Operations ========== + + /** + * Returns indexer used to distinguish and identify elements in this set. + * + * @deprecated Use {@link #getIndexerFunction()} + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + @Deprecated + public Indexer getIndexer() { + return indexer instanceof Indexer ? (Indexer)indexer : new Indexer.DelegateIndexer(indexer); + } + + /** + * Returns indexer function used to distinguish and identify elements in this set. + */ + public IndexerFunction getIndexerFunction() { + return indexer; + } + + /** + * Returns the number of elements in this set. + */ + @Override + public int size() { + return core.size(); // Atomic volatile read. + } + + /** + * Returns the element from this set which matches specified value or null if none were found. + */ + public V getByValue(V value) { + return core.getByValue(value); // Atomic volatile read. + } + + /** + * Returns the element from this set which matches specified key or null if none were found. + */ + public V getByKey(K key) { + return core.getByKey(key); // Atomic volatile read. + } + + /** + * Returns the element from this set which matches specified key or null if none were found. + */ + public V getByKey(int key) { + return core.getByKey(key); // Atomic volatile read. + } + + /** + * Returns the element from this set which matches specified key or null if none were found. + */ + public V getByKey(long key) { + return core.getByKey(key); // Atomic volatile read. + } + + /** + * Returns true if this set contains element which matches specified value. + *

+ * This implementation delegates to ({@link #getByValue(Object) getByValue(value)} != null) expression. + *

+ * Note, that unlike {@link HashSet#contains}, + * this method might throw {@link ClassCastException} if value is of the wrong class. + * + * @deprecated Use {@link #containsValue} to be explicit about type and intent. + */ + @Override + @SuppressWarnings("unchecked") + public boolean contains(Object value) { + return getByValue((V)value) != null; + } + + /** + * Returns true if this set contains element which matches specified value. + *

+ * This implementation delegates to ({@link #getByValue(Object) getByValue(value)} != null) expression. + */ + public boolean containsValue(V value) { + return getByValue(value) != null; + } + + /** + * Returns true if this set contains element which matches specified key. + *

+ * This implementation delegates to ({@link #getByKey(Object) getByKey(key)} != null) expression. + */ + public boolean containsKey(K key) { + return getByKey(key) != null; + } + + /** + * Returns true if this set contains element which matches specified key. + *

+ * This implementation delegates to ({@link #getByKey(int) getByKey(key)} != null) expression. + */ + public boolean containsKey(int key) { + return getByKey(key) != null; + } + + /** + * Returns true if this set contains element which matches specified key. + *

+ * This implementation delegates to ({@link #getByKey(long) getByKey(key)} != null) expression. + */ + public boolean containsKey(long key) { + return getByKey(key) != null; + } + + /** + * Returns an iterator over the elements in this set. + */ + @Nonnull + @Override + @SuppressWarnings("unchecked") + public Iterator iterator() { + return (Iterator)iterator(IndexedIterator.VALUE_FAILFAST); + } + + /** + * Returns an iterator over the keys of elements in this set. + */ + @SuppressWarnings("unchecked") + public Iterator keyIterator() { + return (Iterator)iterator(IndexedIterator.KEY_FAILFAST); + } + + /** + * Returns an iterator over the entries in this set. + */ + @SuppressWarnings("unchecked") + public Iterator> entryIterator() { + return (Iterator>)iterator(IndexedIterator.ENTRY_FAILFAST); + } + + /** + * Returns concurrent iterator over the elements in this set. + */ + @SuppressWarnings("unchecked") + public Iterator concurrentIterator() { + return (Iterator)iterator(IndexedIterator.VALUE_CONCURRENT); + } + + /** + * Returns an array containing all of the elements in this set. + * Obeys the general contract of the {@link Collection#toArray()} method. + */ + @Nonnull + @Override + public Object[] toArray() { + return core.toArray(null); // Atomic volatile read. + } + + /** + * Returns an array containing all of the elements in this set whose runtime type is that of the specified array. + * Obeys the general contract of the {@link Collection#toArray(Object[])} method. + */ + @Nonnull + @Override + public T[] toArray(T[] a) { + return core.toArray(a); // Atomic volatile read. + } + + /** + * Returns static structure statistics of this set. + */ + public IndexedSetStats getStats() { + return core.getStats(); + } + + // ========== Modification Operations ========== + + /** + * Puts specified element into this set and returns previous element that matches specified one. + */ + public V put(V value) { + return putImpl(core, value); // Atomic volatile read. + } + + /** + * Puts specified element into this set if it is absent and + * returns current element in the set that matches specified one. + * This is equivalent to + *

+	 *   if (set.containsValue(value)) {
+	 *     return set.getByValue(value);
+	 *   } else {
+	 *     set.put(value);
+	 *     return value;
+	 *   }
+	 * 
+ * except that the action is performed atomically if it is properly synchronized. + *

+ * Note, that unlike {@link ConcurrentMap#putIfAbsent}, + * this method returns specified value (not null) if the value was absent. + */ + public V putIfAbsentAndGet(V value) { + Core core = this.core; // Atomic volatile read. + V oldValue = core.getByValue(value); + if (oldValue != null) + return oldValue; + putImpl(core, value); + return value; + } + + /** + * Adds specified element into this set and returns true + * if this operation has increased the size of this set. + *

+ * This implementation adds value using {@link #put(Object) put(value)} method. + */ + @Override + public boolean add(V value) { + return put(value) == null; + } + + /** + * Removes specified element from this set if it is present and returns + * true if this operation has decreased the size of this set. + *

+ * This implementation removes value using {@link #removeValue(Object) removeValue(value)} method. + *

+ * Note, that unlike {@link HashSet#remove}, + * this method might throw {@link ClassCastException} if value is of the wrong class. + */ + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object value) { + return removeValue((V) value) != null; + } + + /** + * Removes the element from this set which matches specified value if it is present + * and returns removed element or null if none were found. + */ + public V removeValue(V value) { + Core core = this.core; // Atomic volatile read. + V oldValue = core.removeValue(value); + this.core = core; // Atomic volatile write. + return oldValue; + } + + /** + * Removes the element from this set which matches specified key if it is present + * and returns removed element or null if none were found. + */ + public V removeKey(K key) { + Core core = this.core; // Atomic volatile read. + V oldValue = core.removeKey(key); + this.core = core; // Atomic volatile write. + return oldValue; + } + + /** + * Removes the element from this set which matches specified key if it is present + * and returns removed element or null if none were found. + */ + public V removeKey(int key) { + return removeKey((long)key); + } + + /** + * Removes the element from this set which matches specified key if it is present + * and returns removed element or null if none were found. + */ + public V removeKey(long key) { + Core core = this.core; // Atomic volatile read. + V oldValue = core.removeKey(key); + this.core = core; // Atomic volatile write. + return oldValue; + } + + // ========== Internal Implementation - Helper Instance Methods ========== + + private V putImpl(Core core, V value) { + V oldValue; + if (core.needRehash()) { + // Rehash shall be done before put in order to move away from EMPTY_CORE and protect from bad magic. + // However in situ replacement of existing value shall keep old modCount and avoid rehash. + if (core == Core.EMPTY_CORE || (oldValue = core.put(value, true)) == null) { + core = core.rehash(indexer, 0); + oldValue = core.put(value, false); + } + } else + oldValue = core.put(value, false); + this.core = core; // Atomic volatile write. + return oldValue; + } + + private Iterator iterator(int type) { + Core core = this.core; // Atomic volatile read. + return core.size() == 0 ? IndexedIterator.EMPTY_ITERATOR : new IndexedIterator<>(this, core, type); + } + + void checkModification(Object checkCore, long checkModCount) { + Core core = this.core; // Atomic volatile read. + if (checkCore != core || checkModCount != core.getModCount()) + throw new ConcurrentModificationException(); + } + + void removeIterated(Object checkCore, long checkModCount, boolean concurrent, V lastValue, int lastIndex) { + Core core = this.core; // Atomic volatile read. + if (!concurrent && (checkCore != core || checkModCount != core.getModCount())) + throw new ConcurrentModificationException(); + if (core.getAt(lastIndex) == lastValue) // Atomic read. + core.removeAt(lastIndex, core.getInitialIndexByValue(lastValue)); + else if (concurrent) + core.removeValue(lastValue); + else + throw new ConcurrentModificationException(); + this.core = core; // Atomic volatile write. + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + writeCore(out); + } + + void writeCore(ObjectOutputStream out) throws IOException { + core.writeObjectImpl(out); // Atomic volatile read. + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + core = Core.readObjectImpl(indexer, in); // Atomic volatile write. + } + + // ========== Internal Implementation - Core ========== + + /** + * Core class to hold all data of {@link IndexedSet}. + */ + private static final class Core { + static final int QUALITY_BASE = 6; + static final Object REMOVED = new Object(); // Marker object for removed values. + + @SuppressWarnings("unchecked") + static final Core EMPTY_CORE = new Core(value -> null, 0, GOLDEN_RATIO); // Empty core for empty sets. + + static { + EMPTY_CORE.overallSize = EMPTY_CORE.matrix.length; // Special value to trigger rehash before first 'put' operation. + } + + // 'private' fields and methods of Core class shall be accessed only from within Core class itself. + + private final int magic; + private final int shift; + private final IndexerFunction indexer; + private final V[] matrix; + + /** + * Quality is a complex value that tracks quality of this core's payload, + * quality = (total_distance_to_all_payload_values << QUALITY_BASE) + distance_shift, + * where {@code distance_shift} is a current tolerance to the bad payload distance. + * Note, that lower value of quality is BETTER. + * @see #exceed(long) + */ + private long quality; + + private int payloadSize; + private int overallSize; // payloadSize + number_of_REMOVED_values + + private long modCount; // counts structural changes when elements are added or removed (replace is Ok) + private long amortizedCost; // total cost of amortization of all removed and rehashed values, see unamortizedCost() + + @SuppressWarnings("unchecked") + Core(IndexerFunction indexer, int capacity, int magic) { + if (indexer == null) + throw new NullPointerException("Indexer is null."); + this.magic = magic; + shift = getShift(capacity); + this.indexer = indexer; + matrix = (V[])new Object[(-1 >>> shift) + 1]; + quality = QUALITY_BASE + 1; // will rehash when avg_dist > 2, see exceed(...) + } + + /** + * Clones specified core. Implemented as constructor to keep {@link #matrix} field final. + */ + Core(Core source) { + magic = source.magic; + shift = source.shift; + indexer = source.indexer; + matrix = source.matrix.clone(); + quality = source.quality; + payloadSize = source.payloadSize; + overallSize = source.overallSize; + modCount = source.modCount; + amortizedCost = source.amortizedCost; + } + + /** + * Returns increment to this core's {@link #quality} that should be added when new payload + * entry is added at position {@code index} with its initial position at {@code initialIndex}. + */ + private long qualityInc(int index, int initialIndex) { + return ((initialIndex - index) & (-1 >>> shift)) << QUALITY_BASE; + } + + /** + * Returns true if average distance to payload values exceeds + * 1 << (distanceShift - QUALITY_BASE), + * where only last {@link #QUALITY_BASE} bits of distanceShift parameter are used. + * Thus, the method can be directly applied using {@link #quality} itself as an + * {@code distanceShift} argument. + */ + private boolean exceed(long distanceShift) { + // mask total distance bits first + return (quality & (-1L << QUALITY_BASE)) > ((long) payloadSize << distanceShift); + } + + // compute quality tolerance for new instance, so that it rehashes only when it becomes much worse than now + private void computeTolerance() { + while (exceed(quality)) + quality++; + // increment to next tolerance if it is close to exceeding current tolerance + if ((quality & (-1L << QUALITY_BASE)) * 3 > ((long) payloadSize << quality) * 2) + quality++; + } + + /** + * Returns the cost of all put operations to place payload into this core. + * @see #amortizedCost + */ + private long unamortizedCost() { + return (quality >>> QUALITY_BASE) + payloadSize; + } + + private void putValuesIntoEmptyCore(V[] values) { + for (int i = values.length; --i >= 0;) { + V value = values[i]; // Atomic read. + if (value == null || value == REMOVED) + continue; + int index = getInitialIndexByValue(value); + int initialIndex = index; + while (matrix[index] != null) + index = (index - 1) & (-1 >>> shift); + matrix[index] = value; + quality += qualityInc(index, initialIndex); + payloadSize++; + overallSize++; + // Check if there are too many elements in the source. + // Error may happen either if source state is broken + // or if elements are added to source concurrently. + // Ignoring such error here will lead to a dead loop above. + if (overallSize > (THRESHOLD_UP >>> shift)) + throw new ConcurrentModificationException("Concurrent modification during rehash"); + } + } + + private Core rehashInternal(IndexerFunction indexer, int capacity) { + /* GENERAL DESCRIPTION OF REHASH ALGORITHM: + 1. Try to rehash at most 4 times. Twice at regular capacity, once at 2x capacity, and once at 4x capacity. + 2. First attempt can keep old magic if previous distance is good enough, other attempts use random magic. + 3. If the first attempt immediately satisfies perfect limits then return. + 4. If we have to make additional attempts, then the best result is picked. + In this case result is considered acceptable if it satisfies consequently worse limits. + 5. After four attempts the best result is returned even if it is unacceptable by the above rules. + */ + capacity = Math.min(Math.max(capacity, payloadSize), MAX_CAPACITY); + long totalCost = amortizedCost + unamortizedCost(); + Core result = new Core<>(indexer, capacity, exceed(QUALITY_BASE + 1) ? nextMagic(magic, capacity) : magic); + result.putValuesIntoEmptyCore(matrix); + totalCost += result.unamortizedCost(); + if (result.exceed(QUALITY_BASE + 1)) // only if quality is not very good + for (int k = 0; k < 3; k++) { + Core other = new Core<>(indexer, capacity, nextMagic(magic, capacity)); + other.putValuesIntoEmptyCore(matrix); + totalCost += other.unamortizedCost(); + if (other.quality < result.quality) // lower quality is better + result = other; + if (!result.exceed(QUALITY_BASE + 2 + k)) + break; // break when we have acceptable quality + capacity = Math.min(capacity * 2, MAX_CAPACITY); + } + result.computeTolerance(); + // update result stats + result.modCount = modCount; + result.amortizedCost = totalCost - result.unamortizedCost(); + return result; + } + + Core rehash(IndexerFunction indexer, int capacity) { + long modCount = this.modCount; // Atomic read. + Core result = rehashInternal(indexer, capacity); + if (modCount != this.modCount) // Atomic read. + throw new ConcurrentModificationException("Concurrent modification during rehash"); + return result; + } + + boolean needRehash() { + return overallSize > (THRESHOLD_UP >>> shift) || exceed(quality); + } + + Core rehashIfNeeded(IndexerFunction indexer, int capacity) { + return needRehash() ? + rehash(indexer, capacity) : this; + } + + Core ensureCapacity(IndexerFunction indexer, int capacity) { + return capacity > (THRESHOLD_UP >>> shift) && shift > MIN_SHIFT ? + rehash(indexer, capacity) : this; + } + + Core trimToSize(IndexerFunction indexer) { + return payloadSize < (THRESHOLD_DOWN >>> shift) && shift < MAX_SHIFT ? + rehash(indexer, 0) : this; + } + + Core clear() { + if (this == EMPTY_CORE) + return this; + for (int i = matrix.length; --i >= 0;) + matrix[i] = null; + modCount += payloadSize; + amortizedCost += unamortizedCost(); + quality = QUALITY_BASE + 1; + payloadSize = 0; + overallSize = 0; + return this; + } + + int size() { + return payloadSize; // Atomic read. + } + + long getModCount() { + return modCount; // Atomic read. + } + + int getInitialIndexByValue(V value) { + return (indexer.hashCodeByValue(value) * magic) >>> shift; + } + + V getByValue(V value) { + int index = getInitialIndexByValue(value); + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByValue(value, testValue)) + return testValue; + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + V getByKey(K key) { + int index = (indexer.hashCodeByKey(key) * magic) >>> shift; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByKey(key, testValue)) + return testValue; + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + V getByKey(long key) { + int index = (indexer.hashCodeByKey(key) * magic) >>> shift; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByKey(key, testValue)) + return testValue; + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + @SuppressWarnings("unchecked") + V put(V value, boolean replaceOnly) { + // These are sanity checks - they can be removed once testing completed. + assert this != EMPTY_CORE : "Putting into EMPTY core."; + assert value != REMOVED : "Value is an internal special marker object."; + + if (value == null) + throw new NullPointerException("Value is null."); + int index = getInitialIndexByValue(value); + int initialIndex = index; + int removedIndex = -1; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByValue(value, testValue)) { + matrix[index] = value; + return testValue; + } + if (testValue == REMOVED && removedIndex < 0) + removedIndex = index; + index = (index - 1) & (-1 >>> shift); + } + if (replaceOnly) + return null; + if (removedIndex < 0) { + matrix[index] = value; + overallSize++; + } else + matrix[index = removedIndex] = value; + quality += qualityInc(index, initialIndex); + payloadSize++; + modCount++; + return null; + } + + V removeValue(V value) { + int index = getInitialIndexByValue(value); + int initialIndex = index; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByValue(value, testValue)) { + removeAt(index, initialIndex); + return testValue; + } + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + V removeKey(K key) { + int index = (indexer.hashCodeByKey(key) * magic) >>> shift; + int initialIndex = index; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByKey(key, testValue)) { + removeAt(index, initialIndex); + return testValue; + } + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + V removeKey(long key) { + int index = (indexer.hashCodeByKey(key) * magic) >>> shift; + int initialIndex = index; + V testValue; + while ((testValue = matrix[index]) != null) { // Atomic read. + if (testValue != REMOVED && indexer.matchesByKey(key, testValue)) { + removeAt(index, initialIndex); + return testValue; + } + index = (index - 1) & (-1 >>> shift); + } + return null; + } + + int getMaxIndex() { + return matrix.length - 1; + } + + V getAt(int index) { + return matrix[index]; + } + + @SuppressWarnings("unchecked") + void removeAt(int index, int initialIndex) { + matrix[index] = (V)REMOVED; + quality -= qualityInc(index, initialIndex); + payloadSize--; + if (matrix[(index - 1) & (-1 >>> shift)] == null) + while (matrix[index] == REMOVED) { + matrix[index] = null; + overallSize--; + index = (index + 1) & (-1 >>> shift); + } + modCount++; + // we paid twice -- first adding this element, then removing it + amortizedCost += 2 * ((qualityInc(index, initialIndex) >>> QUALITY_BASE) + 1); + } + + @SuppressWarnings("unchecked") + T[] toArray(T[] a) { + // If (a == null) then returned array shall be of exact length, otherwise it can be larger. + int size = payloadSize; // Atomic read. + Object[] result = a == null ? new Object[size] : a.length >= size ? a : + (Object[])Array.newInstance(a.getClass().getComponentType(), size); + int n = 0; + for (int i = matrix.length; --i >= 0;) { + Object value = matrix[i]; // Atomic read. + if (value == null || value == REMOVED) + continue; + if (n >= result.length) { + // More elements were added concurrently. Enlarge result array. + // Do not grow more than twice. + // Do not grow more than possible remaining elements (i + 1). + // Do not fail when (n == 0). + Object[] tmp = (Object[])Array.newInstance(result.getClass().getComponentType(), n + Math.min(n, i) + 1); + System.arraycopy(result, 0, tmp, 0, n); + result = tmp; + } + result[n++] = value; + } + if (n < result.length && a == null) { + // Shrink allocated array to exact size. + Object[] tmp = new Object[n]; + System.arraycopy(result, 0, tmp, 0, n); + result = tmp; + } + if (n < result.length) + result[n] = null; + return (T[])result; + } + + void writeObjectImpl(ObjectOutputStream out) throws IOException { + int n = payloadSize; // Atomic read. + // if (n == 0) then empty set, no elements written + // if (n > 0) then fixed set with exactly n elements written + // if (n < 0) then dynamic set with approximately (-n) elements plus marker null element written + out.writeInt(n); + for (int i = matrix.length; --i >= 0;) { + Object value = matrix[i]; // Atomic read. + if (value != null && value != REMOVED && n-- > 0) // Do not write more than n values anyway. + out.writeObject(value); + } + if (n != 0) + throw new IOException("Concurrent modification detected."); + } + + @SuppressWarnings("unchecked") + static Core readObjectImpl(IndexerFunction indexer, ObjectInputStream in) throws IOException, ClassNotFoundException { + int n = in.readInt(); + // if (n == 0) then empty set, no elements written + // if (n > 0) then fixed set with exactly n elements written + // if (n < 0) then dynamic set with approximately (-n) elements plus marker null element written + if (n == 0) + return (Core)EMPTY_CORE; + Core core = new Core<>(indexer, Math.abs(n), GOLDEN_RATIO); + if (n > 0) + for (int i = 0; i < n; i++) { + core = core.rehashIfNeeded(indexer, n); // to protect from bad magic + core.put((V)in.readObject(), false); + } + else + for (V value; (value = (V)in.readObject()) != null;) { + core = core.rehashIfNeeded(indexer, -n); // to protect from bad magic + core.put(value, false); + } + return core; + } + + IndexedSetStats getStats() { + return new IndexedSetStats(payloadSize, matrix.length, quality >>> QUALITY_BASE, amortizedCost + unamortizedCost(), modCount); + } + } + + // ========== Internal Implementation - Iterator ========== + + /** + * Asynchronous iterator over {@link IndexedSet}. + */ + private static final class IndexedIterator implements Iterator { + static final int VALUE_CONCURRENT = 0; + static final int VALUE_FAILFAST = 1; + static final int KEY_FAILFAST = 2; + static final int ENTRY_FAILFAST = 3; + + @SuppressWarnings("unchecked") + static final Iterator EMPTY_ITERATOR = new IndexedIterator(null, Core.EMPTY_CORE, VALUE_CONCURRENT); + + // 'private' fields and methods of IndexedIterator class shall be accessed only from within IndexedIterator class itself. + + private final IndexedSet set; + private final Core core; + private final int type; + + private long modCount; + + private V nextValue; + private int nextIndex; + private V lastValue; + private int lastIndex; + + IndexedIterator(IndexedSet set, Core core, int type) { + this.set = set; + this.core = core; + this.type = type; + modCount = core.getModCount(); + nextIndex = core.getMaxIndex() + 1; + fillNext(); + } + + private void fillNext() { + if (type != VALUE_CONCURRENT) + set.checkModification(core, modCount); + while (--nextIndex >= 0) { + nextValue = core.getAt(nextIndex); // Atomic read. + if (nextValue != null && nextValue != Core.REMOVED) + return; + } + nextValue = null; // No more elements - clear leftover state. + } + + @Override + public boolean hasNext() { + return nextValue != null; + } + + @Override + public Object next() { + if (nextValue == null) + throw new NoSuchElementException(); + lastValue = nextValue; + lastIndex = nextIndex; + fillNext(); + if (type == KEY_FAILFAST) + return set.getIndexerFunction().getObjectKey(lastValue); + if (type == ENTRY_FAILFAST) + return new IndexedEntry<>(set, lastValue); + return lastValue; + } + + @Override + public void remove() { + if (lastValue == null) + throw new IllegalStateException(); + set.removeIterated(core, modCount, type == VALUE_CONCURRENT, lastValue, lastIndex); + modCount = core.getModCount(); + lastValue = null; + } + } + + // ========== Internal Implementation - Entry ========== + + /** + * IndexedEntry class is a wrapper to convert indexed API to collections API. + */ + private static final class IndexedEntry implements Map.Entry { + // 'private' fields and methods of IndexedEntry class shall be accessed only from within IndexedEntry class itself. + + private final IndexedSet set; + private V value; + + IndexedEntry(IndexedSet set, V value) { + this.set = set; + this.value = value; + } + + @Override + public K getKey() { + return set.getIndexerFunction().getObjectKey(value); + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + if (value == null) + throw new NullPointerException("Value is null."); + V oldValue = this.value; + if (!set.getIndexerFunction().matchesByValue(value, oldValue)) + throw new IllegalArgumentException("New value does not match old value."); + set.put(this.value = value); + return oldValue; + } + + public boolean equals(Object obj) { + if (!(obj instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry)obj; + Object key = getKey(); + Object ekey = e.getKey(); + return (key == null ? ekey == null : key.equals(ekey)) && value.equals(e.getValue()); + } + + public int hashCode() { + K key = getKey(); + return (key == null ? 0 : key.hashCode()) ^ value.hashCode(); + } + + public String toString() { + return getKey() + "=" + value; + } + } + + // ========== Internal Implementation - Helper Static Constants and Methods ========== + + /* + * This section contains constants and methods to support matrix-based data structures. + * Such data structures and related algorithms are also known as "direct linear probe hashing". + * The code below is a copy-paste from com.devexperts.qd.impl.matrix.Hashing class. + */ + + static final int THRESHOLD_UP = (int)((1L << 32) * 5 / 9); + static final int THRESHOLD_DOWN = (int)((1L << 32) * 5 / 27); + + static final int THRESHOLD_ALLOC_UP = (int)((1L << 32) * 4 / 9); + static final int MAX_SHIFT = 29; + static final int MIN_SHIFT = 2; + static final int MAX_CAPACITY = THRESHOLD_ALLOC_UP >>> MIN_SHIFT; + + /** + * Calculates appropriate 'shift' for specified capacity. + */ + static int getShift(int capacity) { + int shift = MAX_SHIFT; + while ((THRESHOLD_ALLOC_UP >>> shift) < capacity && shift >= MIN_SHIFT) + shift--; + if (shift < MIN_SHIFT) + throw new IllegalArgumentException("Capacity is too large: " + capacity); + return shift; + } + + private static final int GOLDEN_RATIO = 0x9E3779B9; + private static final int MAGIC = 0xC96B5A35; + private static int magicSeed = (int)(System.currentTimeMillis() * Runtime.getRuntime().freeMemory()); + + /** + * Generates next MAGIC number with proper distribution and difference of bits. + */ + static int nextMagic(int prevMagic) { + // Generate next pseudo-random number with lowest bit set to '1'. + int magic = (magicSeed = magicSeed * MAGIC + 1) | 1; + // Enforce that any 4 bits are neither '0000' nor '1111'. + // Start earlier to enforce that highest 2 bits are neither '00' nor '11'. + for (int i = 31; --i >= 0;) { + int bits = (magic >> i) & 0x0F; + if (bits == 0 || bits == 0x0F) { + magic ^= 1 << i; + i -= 2; + } + } + // Recover cleared lowest bit. + if ((magic & 1) == 0) + magic ^= 3; // Convert '10' (the only possible case) into '01'. + // Enforce that any 8 bits have at least 1 difference from previous number. + for (int i = 25; --i >= 0;) { + if ((((magic ^ prevMagic) >> i) & 0xFF) == 0) { + // Reverse bit i+1 and enforce that bit i+2 differs from it. + // This may lead to 4-bit (but not longer) sequences of '0' or '1'. + magic ^= ((magic ^ (magic << 1)) & (4 << i)) ^ (2 << i); + i -= 6; + } + } + return magic; + } + + /** + * Generates next MAGIC number by selecting best one from several candidates. + * Number of checked candidates depends on specified capacity. + */ + static int nextMagic(int prevMagic, int capacity) { + int magic = nextMagic(prevMagic); + if (capacity < 32) + return magic; + double eval = evaluateContinuedFraction(magic); + int attempts = 30 - Integer.numberOfLeadingZeros(capacity); + for (int i = 0; i < attempts; i++) { + int m = nextMagic(prevMagic); + double e = evaluateContinuedFraction(m); + if (e > eval) { + magic = m; + eval = e; + } + } + return magic; + } + + /** + * Evaluates quality of specified MAGIC number as a minimal normalized distance + * to rational approximations generated by continued fraction. + * The larger distance - the better magic. + */ + static double evaluateContinuedFraction(int magic) { + // for explanation of continued fraction look at https://en.wikipedia.org/wiki/Continued_fraction + // 'x' is a fractional representation of magic scaled into a range (0, 1); both bounds exclusive + // 'rem' is a current remainder to compute next continued fraction coefficient; it's in a range [0, 1) + // 'a' is a current continued fraction coefficient - aka a[i]; it's always a natural number + // 'p/q' is a current rational approximation of 'x' with an error less than 1/q^2 + // (p1,q1) and (p2,q2) are previous (p,q) approximations - aka (p[i-1],q[i-1]) and (p[i-2],q[i-2]) + // 'grade' is a minimal (worst) distance from 'x' to it's approximations normalized by q^2 + double x = (double)(magic & 0xFFFFFFFFL) / (1L << 32); + double rem = x; + long p2 = 1; + long q2 = 0; + long p1 = 0; + long q1 = 1; + double grade = x; + for (int i = 1; i <= 20; i++) { + rem = 1 / rem; + long a = (long)rem; + rem -= a; + long p = a * p1 + p2; + long q = a * q1 + q2; + p2 = p1; + q2 = q1; + p1 = p; + q1 = q; + grade = Math.min(grade, Math.abs(x * q - p) * q); + if (grade < 1e-6 || rem < 1e-6 || q > (1 << 20)) + break; + } + return grade; + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSetStats.java b/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSetStats.java new file mode 100644 index 0000000000..801e091875 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/IndexedSetStats.java @@ -0,0 +1,69 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.Serializable; +import java.util.Locale; + +/** + * Provides access to statistics of the {@link IndexedSet} static structure. + * Statistics are tracked only during modification operations. + * It has self-explanatory {@link #toString()} method that can be used to periodically dump + * information about important caches that are based on the {@code IndexedSet}. + */ +public class IndexedSetStats implements Serializable { + private static final long serialVersionUID = 0; + + private final int payload_size; + private final int allocated_size; + private final long payload_distance; + private final long amortized_cost; + private final long mod_count; + + IndexedSetStats(int payload_size, int allocated_size, long payload_distance, long amortized_cost, long mod_count) { + this.payload_size = payload_size; + this.allocated_size = allocated_size; + this.payload_distance = payload_distance; + this.amortized_cost = amortized_cost; + this.mod_count = mod_count; + } + + public int getSize() { + return payload_size; + } + + public int getAllocatedSize() { + return allocated_size; + } + + public double getFillFactor() { + return (double)payload_size / allocated_size; + } + + public double getAverageDistance() { + return payload_distance == 0 ? 0 : (double)payload_distance / payload_size; + } + + public double getAmortizedCost() { + return amortized_cost == 0 ? 0 : (double)amortized_cost / payload_size; + } + + public long getModCount() { + return mod_count; + } + + @Override + public String toString() { + return String.format(Locale.US, "size %d, filled %.1f%%, avgdist %.3f, amortized %.3f, mods %d", + getSize(), getFillFactor() * 100, getAverageDistance(), getAmortizedCost(), getModCount()); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/Indexer.java b/java_console/logging-api/src/main/java/com/devexperts/util/Indexer.java new file mode 100644 index 0000000000..ab792a3578 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/Indexer.java @@ -0,0 +1,110 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.Serializable; + +/** + * A strategy that distinguishes and identifies elements in an {@link IndexedSet} and {@link IndexedMap}. + * + *

The Indexer is {@link Serializable}, so that all concrete subclasses + * shall be serializable in order to support serialization of indexed set and map.. + * + * @deprecated Use a functional interface {@link IndexerFunction} instead. + */ +@Deprecated +public abstract class Indexer implements IndexerFunction { + private static final long serialVersionUID = 0L; + + /** + * Default strategy that treats values as their own keys (key == value) and delegates to + * {@link Object#hashCode() Object.hashCode()} and {@link Object#equals(Object) Object.equals(Object)} + * methods as appropriate. This strategy does not support primitive keys. + * + *

This strategy basically turns {@link IndexedSet} into plain hash set of objects and {@link IndexedMap} + * into a self-reference mapping. + */ + @SuppressWarnings("rawtypes") + public static final Indexer DEFAULT = new DefaultIndexer(); + + // ========== Standard Subclasses ========== + + /** + * Default strategy that treats values as their own keys (key == value). + */ + @SuppressWarnings("rawtypes") + static final class DefaultIndexer extends Indexer { + private static final long serialVersionUID = 0; + + DefaultIndexer() {} + + @Override + public Object getObjectKey(Object value) { + return value; + } + + @SuppressWarnings("ReadResolveAndWriteReplaceProtected") + public Object readResolve() { + return Indexer.DEFAULT; + } + } + + static class DelegateIndexer extends Indexer { + private static final long serialVersionUID = 0L; + + private final IndexerFunction indexer; + + DelegateIndexer(IndexerFunction indexer) { + this.indexer = indexer; + } + + @Override + public K getObjectKey(V value) { + return indexer.getObjectKey(value); + } + + @Override + public int hashCodeByKey(K key) { + return indexer.hashCodeByKey(key); + } + + @Override + public boolean matchesByKey(K key, V value) { + return indexer.matchesByKey(key, value); + } + + @Override + public int hashCodeByValue(V value) { + return indexer.hashCodeByValue(value); + } + + @Override + public boolean matchesByValue(V newValue, V oldValue) { + return indexer.matchesByValue(newValue, oldValue); + } + + @Override + public long getNumberKey(V value) { + return indexer.getNumberKey(value); + } + + @Override + public int hashCodeByKey(long key) { + return indexer.hashCodeByKey(key); + } + + @Override + public boolean matchesByKey(long key, V value) { + return indexer.matchesByKey(key, value); + } + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/IndexerFunction.java b/java_console/logging-api/src/main/java/com/devexperts/util/IndexerFunction.java new file mode 100644 index 0000000000..fcf886619e --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/IndexerFunction.java @@ -0,0 +1,354 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.Serializable; + +/** + * A strategy that distinguishes and identifies elements in an {@link IndexedSet} and {@link IndexedMap}. + * The IndexerFunction defines 3 equivalent ways to identify elements: + *

    + *
  • by value - mandatory and primary method to identify element by itself + *
  • by object key - optional method that identifies elements using object key + *
  • by number key - optional method that identifies elements using number key + *
+ * + *

The IndexerFunction is not restricted to use explicit key concept for identification. + * Identity of an element may be defined by a number of attributes, specified in a value itself, + * in a template object, in a formal key object, or encoded in a number key. The IndexerFunction + * may use all these ways interchangeable to distinguish and identify elements. + * + *

Being a strategy, the IndexerFunction is required to be stateless, concurrent and thread-safe. + * + *

The IndexerFunction is a functional interface with a sole abstract method that shall be implemented + * in order to use identification using explicit object key - no other methods are required to be + * overridden in such simple cases. + * There are two other functional interfaces {@link IndexerFunction.IntKey} and {@link IndexerFunction.LongKey} + * which are similarly designed with sole abstract methods to simplify identification using explicit number keys. + * There is also a functional interface {@link IndexerFunction.IdentityKey} which is similarly designed + * with sole abstract method for cases when explicit object keys must be compared by reference rather than + * using their {@link Object#equals(Object) equals} method. + * + *

The IndexerFunction is {@link Serializable}, so that all concrete subclasses + * shall be serializable in order to support serialization of indexed set and map. + */ +@FunctionalInterface +public interface IndexerFunction extends Serializable { + + /** + * Default strategy that treats values as their own keys (key == value) and delegates to + * {@link Object#hashCode() Object.hashCode()} and {@link Object#equals(Object) Object.equals(Object)} + * methods as appropriate. This strategy does not support primitive keys. + * + *

This strategy basically turns {@link IndexedSet} into plain hash set of objects and {@link IndexedMap} + * into a self-reference mapping. + */ + @SuppressWarnings("rawtypes") + public static final IndexerFunction DEFAULT = new DefaultIndexerFunction(); + + // ========== Object Key Operations ========== + + /** + * Returns object key for specified value to be used for hashing and identification; + * called when explicit object key is needed or when other methods delegate operations as specified. + */ + public K getObjectKey(V value); + + /** + * Returns hash code for specified object key; called when performing operations using object keys. + * + *

This implementation delegates to + * (key == null ? 0 : key.{@link Object#hashCode() hashCode}()) expression. + */ + public default int hashCodeByKey(K key) { + return key == null ? 0 : key.hashCode(); + } + + /** + * Determines if specified object key matches specified value; called when performing operations using object keys. + * + *

This implementation delegates to + * (key == null ? {@link #getObjectKey(Object) getObjectKey}(value) == null : key.{@link Object#equals(Object) equals}({@link #getObjectKey(Object) getObjectKey}(value))) expression. + */ + public default boolean matchesByKey(K key, V value) { + return key == null ? getObjectKey(value) == null : key.equals(getObjectKey(value)); + } + + // ========== Value Operations ========== + + /** + * Returns hash code for specified value; called when performing value-based operations, including rehash. + * + *

This implementation delegates to + * {@link #hashCodeByKey(Object) hashCodeByKey}({@link #getObjectKey(Object) getObjectKey}(value)) expression. + */ + public default int hashCodeByValue(V value) { + return hashCodeByKey(getObjectKey(value)); + } + + /** + * Determines if specified new value matches specified old value; called when performing value-based operations. + * + *

This implementation delegates to + * {@link #matchesByKey(Object, Object) matchesByKey}({@link #getObjectKey(Object) getObjectKey}(newValue), oldValue) expression. + */ + public default boolean matchesByValue(V newValue, V oldValue) { + return matchesByKey(getObjectKey(newValue), oldValue); + } + + // ========== Number Key Operations (Optional) ========== + + /** + * Returns number key for specified value to be used for hashing and identification; + * called when explicit number key is needed or when other methods delegate operations as specified. + * + *

This implementation delegates to + * {@link #hashCodeByKey(long) hashCodeByKey}(((Number){@link #getObjectKey(Object) getObjectKey}(value)).{@link Number#longValue() longValue}()) expression. + */ + public default long getNumberKey(V value) { + return hashCodeByKey(((Number)getObjectKey(value)).longValue()); + } + + /** + * Returns hash code for specified number key; called when performing operations using long keys. + * + *

This implementation delegates to + * Long.{@link Long#hashCode(long) hashCode}(key) expression. + */ + public default int hashCodeByKey(long key) { + return Long.hashCode(key); + } + + /** + * Determines if specified number key matches specified value; called when performing operations using number keys. + * + *

This implementation delegates to + * (key == {@link #getNumberKey(Object) getNumberKey}(value)) expression. + */ + public default boolean matchesByKey(long key, V value) { + return key == getNumberKey(value); + } + + /** + * An {@link IndexerFunction} that distinguishes and identifies elements using int keys. + * + *

It assumes that elements are fully identifiable by plain numeric identifier and treats object keys as a mere wrappers. + * The hash function is computed by taking int key value. + */ + @FunctionalInterface + public interface IntKey extends IndexerFunction { + /** + * Returns number key for specified value to be used for hashing and identification. + */ + public int getIntKey(V value); + + /** + * Returns number key for specified value to be used for hashing and identification; + * called when explicit number key is needed or when other methods delegate operations as specified. + * + *

This implementation delegates to + * {@link #getIntKey(Object) getIntKey}(value) expression. + */ + @Override + public default long getNumberKey(V value) { + return getIntKey(value); + } + + /** + * Returns hash code for specified value; called when performing value-based operations, including rehash. + * + *

This implementation delegates to + * {@link #hashCodeByKey(long) hashCodeByKey}({@link #getIntKey(Object) getIntKey}(value)) expression. + */ + @Override + public default int hashCodeByValue(V value) { + return hashCodeByKey(getIntKey(value)); + } + + /** + * Determines if specified new value matches specified old value; called when performing value-based operations. + * + *

This implementation delegates to + * ({@link #getIntKey(Object) getIntKey}(newValue) == {@link #getIntKey(Object) getIntKey}(oldValue)) expression. + */ + @Override + public default boolean matchesByValue(V newValue, V oldValue) { + return getIntKey(newValue) == getIntKey(oldValue); + } + + /** + * Returns object key for specified value to be used for hashing and identification; + * called when explicit object key is needed or when other methods delegate operations as specified. + * + *

This implementation delegates to + * {@link #getIntKey(Object) getIntKey}(value) expression. + */ + @Override + public default Integer getObjectKey(V value) { + return getIntKey(value); + } + + /** + * Returns hash code for specified object key; called when performing operations using object keys. + * + *

This implementation delegates to + * {@link #hashCodeByKey(long) hashCodeByKey}(key.{@link Integer#intValue() intValue}()) expression. + */ + @Override + public default int hashCodeByKey(Integer key) { + return hashCodeByKey(key.intValue()); + } + + /** + * Determines if specified object key matches specified value; called when performing operations using object keys. + * + *

This implementation delegates to + * (key == {@link #getIntKey(Object) getIntKey}(value)) expression. + */ + @Override + public default boolean matchesByKey(Integer key, V value) { + return key == getIntKey(value); + } + } + + /** + * An {@link IndexerFunction} that distinguishes and identifies elements using long keys. + * + *

It assumes that elements are fully identifiable by plain numeric identifier and treats object keys as a mere wrappers. + * The hash function is computed using {@link Long#hashCode(long) Long.hashCode}(key) expression. + */ + @FunctionalInterface + public interface LongKey extends IndexerFunction { + /** + * Returns number key for specified value to be used for hashing and identification; + * called when explicit number key is needed or when other methods delegate operations as specified. + */ + @Override + public long getNumberKey(V value); + + /** + * Returns hash code for specified value; called when performing value-based operations, including rehash. + * + *

This implementation delegates to + * {@link #hashCodeByKey(long) hashCodeByKey}({@link #getNumberKey(Object) getNumberKey}(value)) expression. + */ + @Override + public default int hashCodeByValue(V value) { + return hashCodeByKey(getNumberKey(value)); + } + + /** + * Determines if specified new value matches specified old value; called when performing value-based operations. + * + *

This implementation delegates to + * ({@link #getNumberKey(Object) getNumberKey}(newValue) == {@link #getNumberKey(Object) getNumberKey}(oldValue)) expression. + */ + @Override + public default boolean matchesByValue(V newValue, V oldValue) { + return getNumberKey(newValue) == getNumberKey(oldValue); + } + + /** + * Returns object key for specified value to be used for hashing and identification; + * called when explicit object key is needed or when other methods delegate operations as specified. + * + *

This implementation delegates to + * {@link #getNumberKey(Object) getNumberKey}(value) expression. + */ + @Override + public default Long getObjectKey(V value) { + return getNumberKey(value); + } + + /** + * Returns hash code for specified object key; called when performing operations using object keys. + * + *

This implementation delegates to + * {@link #hashCodeByKey(long) hashCodeByKey}(key.{@link Long#longValue() longValue}()) expression. + */ + @Override + public default int hashCodeByKey(Long key) { + return hashCodeByKey(key.longValue()); + } + + /** + * Determines if specified object key matches specified value; called when performing operations using object keys. + * + *

This implementation delegates to + * (key == {@link #getNumberKey(Object) getNumberKey}(value)) expression. + */ + @Override + public default boolean matchesByKey(Long key, V value) { + return key == getNumberKey(value); + } + } + + /** + * A specialized {@link IndexerFunction} that distinguishes and identifies elements using identity comparison of object keys. + * + *

It uses {@link System#identityHashCode(Object) System.identityHashCode(Object)} method instead of + * {@link Object#hashCode() Object.hashCode()} method to compute hashcode and reference comparison + * instead of {@link Object#equals(Object) Object.equals(Object)} method to determine identity. + * + *

In order to use this functional interface, cast a lambda expression to it when creating {@link IndexedSet}. For example,
+ * IndexedSet<Key, Value> set = IndexedSet.{@link IndexedSet#create(IndexerFunction) create}((IndexerFunction.IdentityKey<Key, Value>)Value::getKey); + */ + @FunctionalInterface + public interface IdentityKey extends IndexerFunction { + /** + * Returns hash code for specified object key; called when performing operations using object keys. + * + *

This implementation delegates to + * System.{@link System#identityHashCode(Object) identityHashCode}(key) expression. + */ + @Override + public default int hashCodeByKey(K key) { + return System.identityHashCode(key); + } + + /** + * Determines if specified object key matches specified value; called when performing operations using object keys. + * + *

This implementation delegates to + * (key == {@link #getObjectKey(Object) getObjectKey}(value)) expression. + */ + @Override + public default boolean matchesByKey(K key, V value) { + return key == getObjectKey(value); + } + } + + /** + * Default strategy that treats values as their own keys (key == value). + */ + @SuppressWarnings("rawtypes") + static final class DefaultIndexerFunction implements IndexerFunction { + private static final long serialVersionUID = 0; + + DefaultIndexerFunction() {} + + @Override + public Object getObjectKey(Object value) { + return value; + } + + // Substitute DefaultIndexer implementation for backward compatibility + private Object writeReplace() { + return Indexer.DEFAULT; + } + + private Object readResolve() { + return IndexerFunction.DEFAULT; + } + } + +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/IntComparator.java b/java_console/logging-api/src/main/java/com/devexperts/util/IntComparator.java new file mode 100644 index 0000000000..e473ba8cd9 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/IntComparator.java @@ -0,0 +1,233 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Objects; +import java.util.function.IntFunction; +import java.util.function.IntToDoubleFunction; +import java.util.function.IntToLongFunction; +import java.util.function.IntUnaryOperator; + +/** + * A comparison function, which imposes a total ordering on some collection of ints. + * Comparators can be passed to a sort method (such as {@link QuickSort#sort(int[], IntComparator) QuickSort.sort}) + * to allow precise control over the sort order. + * + *

The purpose of this function is to allow non-trivial ordering of ints which depend on some external data. + * For example when ints are some identifiers (pseudo-references) of actual data. + */ +@SuppressWarnings("UnusedDeclaration") +public interface IntComparator { + /** + * Compares its two arguments for order. Returns a negative integer, zero, or a positive integer + * as the first argument is less than, equal to, or greater than the second. + * + * @param i1 the first int to be compared. + * @param i2 the second int to be compared. + * @return a negative integer, zero, or a positive integer as the first argument is + * less than, equal to, or greater than the second. + */ + public int compare(int i1, int i2); + + /** + * Returns a comparator that imposes the reverse ordering of this comparator. + * + * @return a comparator that imposes the reverse ordering of this comparator. + */ + public default IntComparator reversed() { + return (IntComparator & Serializable) (i1, i2) -> compare(i2, i1); + } + + /** + * Returns a lexicographic-order comparator with another comparator. + * If this comparator considers two elements equal, i.e. {@code compare(i1, i2) == 0}, + * then other comparator is used to determine the order. + * + *

The returned comparator is serializable if the specified comparator is also serializable. + * + * @param other the other comparator to be used when this comparator compares two ints that are equal. + * @return a lexicographic-order comparator composed of this comparator and then the other comparator. + * @throws NullPointerException if the argument is null. + */ + public default IntComparator thenComparing(IntComparator other) { + Objects.requireNonNull(other); + return (IntComparator & Serializable) (i1, i2) -> { + int res = compare(i1, i2); + return res != 0 ? res : other.compare(i1, i2); + }; + } + + /** + * Returns a lexicographic-order comparator with a function that extracts + * a sort key to be compared with the given sort key comparator. + * + *

This default implementation delegates to + * {@link #thenComparing(IntComparator) thenComparing}({@link #comparing(IntFunction, Comparator) comparing}(keyExtractor, keyComparator)) expression. + * + * @param the type of the sort key. + * @param keyExtractor the function used to extract the sort key. + * @param keyComparator the comparator used to compare the sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted sort key using the specified sort key comparator. + * @throws NullPointerException if either argument is null. + */ + public default IntComparator thenComparing(IntFunction keyExtractor, Comparator keyComparator) { + return thenComparing(comparing(keyExtractor, keyComparator)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a comparable sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(IntComparator) thenComparing}({@link #comparing(IntFunction) comparing}(keyExtractor)) expression. + * + * @param the type of the comparable sort key. + * @param keyExtractor the function used to extract the comparable sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted comparable sort key. + * @throws NullPointerException if the argument is null. + */ + public default > IntComparator thenComparing(IntFunction keyExtractor) { + return thenComparing(comparing(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts an int sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(IntComparator) thenComparing}({@link #comparingInt(IntUnaryOperator) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the int sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted int sort key. + * @throws NullPointerException if the argument is null. + */ + public default IntComparator thenComparingInt(IntUnaryOperator keyExtractor) { + return thenComparing(comparingInt(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a long sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(IntComparator) thenComparing}({@link #comparingLong(IntToLongFunction) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the long sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted long sort key. + * @throws NullPointerException if the argument is null. + */ + public default IntComparator thenComparingLong(IntToLongFunction keyExtractor) { + return thenComparing(comparingLong(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a double sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(IntComparator) thenComparing}({@link #comparingDouble(IntToDoubleFunction) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the double sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing and extracted double sort key. + * @throws NullPointerException if the argument is null. + */ + public default IntComparator thenComparingDouble(IntToDoubleFunction keyExtractor) { + return thenComparing(comparingDouble(keyExtractor)); + } + + /** + * Accepts a function that extracts a sort key and a sort key comparator, and returns + * a comparator that compares by an extracted sort key using the specified sort key comparator. + * + *

The returned comparator is serializable if the specified function and comparator are both serializable. + * + * @param the type of the sort key. + * @param keyExtractor the function used to extract the sort key. + * @param keyComparator the comparator used to compare the sort key. + * @return a comparator that compares by an extracted sort key using the specified sort key comparator. + * @throws NullPointerException if either argument is null. + */ + public static IntComparator comparing(IntFunction keyExtractor, Comparator keyComparator) { + Objects.requireNonNull(keyExtractor); + Objects.requireNonNull(keyComparator); + return (IntComparator & Serializable) + (i1, i2) -> keyComparator.compare(keyExtractor.apply(i1), keyExtractor.apply(i2)); + } + + /** + * Accepts a function that extracts a comparable sort, and returns + * a comparator that compares by an extracted comparable sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param the type of the comparable sort key. + * @param keyExtractor the function used to extract the comparable sort key. + * @return a comparator that compares by an extracted comparable sort key. + * @throws NullPointerException if the argument is null. + */ + public static > IntComparator comparing(IntFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (IntComparator & Serializable) + (i1, i2) -> keyExtractor.apply(i1).compareTo(keyExtractor.apply(i2)); + } + + /** + * Accepts a function that extracts an int sort key, and returns + * a comparator that compares by an extracted int sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the int sort key. + * @return a comparator that compares by an extracted int sort key. + * @throws NullPointerException if the argument is null. + */ + public static IntComparator comparingInt(IntUnaryOperator keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (IntComparator & Serializable) + (i1, i2) -> Integer.compare(keyExtractor.applyAsInt(i1), keyExtractor.applyAsInt(i2)); + } + + /** + * Accepts a function that extracts a long sort key, and returns + * a comparator that compares by an extracted long sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the long sort key. + * @return a comparator that compares by an extracted long sort key. + * @throws NullPointerException if the argument is null. + */ + public static IntComparator comparingLong(IntToLongFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (IntComparator & Serializable) + (i1, i2) -> Long.compare(keyExtractor.applyAsLong(i1), keyExtractor.applyAsLong(i2)); + } + + /** + * Accepts a function that extracts a double sort key, and returns + * a comparator that compares by an extracted double sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the double sort key. + * @return a comparator that compares by an extracted double sort key. + * @throws NullPointerException if the argument is null. + */ + public static IntComparator comparingDouble(IntToDoubleFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (IntComparator & Serializable) + (i1, i2) -> Double.compare(keyExtractor.applyAsDouble(i1), keyExtractor.applyAsDouble(i2)); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/LongComparator.java b/java_console/logging-api/src/main/java/com/devexperts/util/LongComparator.java new file mode 100644 index 0000000000..1323f8e643 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/LongComparator.java @@ -0,0 +1,233 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Objects; +import java.util.function.LongFunction; +import java.util.function.LongToDoubleFunction; +import java.util.function.LongToIntFunction; +import java.util.function.LongUnaryOperator; + +/** + * A comparison function, which imposes a total ordering on some collection of longs. + * Comparators can be passed to a sort method (such as {@link QuickSort#sort(long[], LongComparator) QuickSort.sort}) + * to allow precise control over the sort order. + * + *

The purpose of this function is to allow non-trivial ordering of longs which depend on some external data. + * For example when longs are some identifiers (pseudo-references) of actual data. + */ +@SuppressWarnings("UnusedDeclaration") +public interface LongComparator { + /** + * Compares its two arguments for order. Returns a negative integer, zero, or a positive integer + * as the first argument is less than, equal to, or greater than the second. + * + * @param i1 the first long to be compared. + * @param i2 the second long to be compared. + * @return a negative integer, zero, or a positive integer as the first argument is + * less than, equal to, or greater than the second. + */ + public int compare(long i1, long i2); + + /** + * Returns a comparator that imposes the reverse ordering of this comparator. + * + * @return a comparator that imposes the reverse ordering of this comparator. + */ + public default LongComparator reversed() { + return (LongComparator & Serializable) (i1, i2) -> compare(i2, i1); + } + + /** + * Returns a lexicographic-order comparator with another comparator. + * If this comparator considers two elements equal, i.e. {@code compare(i1, i2) == 0}, + * then other comparator is used to determine the order. + * + *

The returned comparator is serializable if the specified comparator is also serializable. + * + * @param other the other comparator to be used when this comparator compares two longs that are equal. + * @return a lexicographic-order comparator composed of this comparator and then the other comparator. + * @throws NullPointerException if the argument is null. + */ + public default LongComparator thenComparing(LongComparator other) { + Objects.requireNonNull(other); + return (LongComparator & Serializable) (i1, i2) -> { + int res = compare(i1, i2); + return res != 0 ? res : other.compare(i1, i2); + }; + } + + /** + * Returns a lexicographic-order comparator with a function that extracts + * a sort key to be compared with the given sort key comparator. + * + *

This default implementation delegates to + * {@link #thenComparing(LongComparator) thenComparing}({@link #comparing(LongFunction, Comparator) comparing}(keyExtractor, keyComparator)) expression. + * + * @param the type of the sort key. + * @param keyExtractor the function used to extract the sort key. + * @param keyComparator the comparator used to compare the sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted sort key using the specified sort key comparator. + * @throws NullPointerException if either argument is null. + */ + public default LongComparator thenComparing(LongFunction keyExtractor, Comparator keyComparator) { + return thenComparing(comparing(keyExtractor, keyComparator)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a comparable sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(LongComparator) thenComparing}({@link #comparing(LongFunction) comparing}(keyExtractor)) expression. + * + * @param the type of the comparable sort key. + * @param keyExtractor the function used to extract the comparable sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted comparable sort key. + * @throws NullPointerException if the argument is null. + */ + public default > LongComparator thenComparing(LongFunction keyExtractor) { + return thenComparing(comparing(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts an int sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(LongComparator) thenComparing}({@link #comparingInt(LongToIntFunction) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the int sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted int sort key. + * @throws NullPointerException if the argument is null. + */ + public default LongComparator thenComparingInt(LongToIntFunction keyExtractor) { + return thenComparing(comparingInt(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a long sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(LongComparator) thenComparing}({@link #comparingLong(LongUnaryOperator) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the long sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing an extracted long sort key. + * @throws NullPointerException if the argument is null. + */ + public default LongComparator thenComparingLong(LongUnaryOperator keyExtractor) { + return thenComparing(comparingLong(keyExtractor)); + } + + /** + * Returns a lexicographic-order comparator with a function that extracts a double sort key. + * + *

This default implementation delegates to + * {@link #thenComparing(LongComparator) thenComparing}({@link #comparingDouble(LongToDoubleFunction) comparing}(keyExtractor)) expression. + * + * @param keyExtractor the function used to extract the double sort key. + * @return a lexicographic-order comparator composed of this comparator + * and then comparing and extracted double sort key. + * @throws NullPointerException if the argument is null. + */ + public default LongComparator thenComparingDouble(LongToDoubleFunction keyExtractor) { + return thenComparing(comparingDouble(keyExtractor)); + } + + /** + * Accepts a function that extracts a sort key and a sort key comparator, and returns + * a comparator that compares by an extracted sort key using the specified sort key comparator. + * + *

The returned comparator is serializable if the specified function and comparator are both serializable. + * + * @param the type of the sort key. + * @param keyExtractor the function used to extract the sort key. + * @param keyComparator the comparator used to compare the sort key. + * @return a comparator that compares by an extracted sort key using the specified sort key comparator. + * @throws NullPointerException if either argument is null. + */ + public static LongComparator comparing(LongFunction keyExtractor, Comparator keyComparator) { + Objects.requireNonNull(keyExtractor); + Objects.requireNonNull(keyComparator); + return (LongComparator & Serializable) + (i1, i2) -> keyComparator.compare(keyExtractor.apply(i1), keyExtractor.apply(i2)); + } + + /** + * Accepts a function that extracts a comparable sort, and returns + * a comparator that compares by an extracted comparable sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param the type of the comparable sort key. + * @param keyExtractor the function used to extract the comparable sort key. + * @return a comparator that compares by an extracted comparable sort key. + * @throws NullPointerException if the argument is null. + */ + public static > LongComparator comparing(LongFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (LongComparator & Serializable) + (i1, i2) -> keyExtractor.apply(i1).compareTo(keyExtractor.apply(i2)); + } + + /** + * Accepts a function that extracts an int sort key, and returns + * a comparator that compares by an extracted int sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the int sort key. + * @return a comparator that compares by an extracted int sort key. + * @throws NullPointerException if the argument is null. + */ + public static LongComparator comparingInt(LongToIntFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (LongComparator & Serializable) + (i1, i2) -> Integer.compare(keyExtractor.applyAsInt(i1), keyExtractor.applyAsInt(i2)); + } + + /** + * Accepts a function that extracts a long sort key, and returns + * a comparator that compares by an extracted long sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the long sort key. + * @return a comparator that compares by an extracted long sort key. + * @throws NullPointerException if the argument is null. + */ + public static LongComparator comparingLong(LongUnaryOperator keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (LongComparator & Serializable) + (i1, i2) -> Long.compare(keyExtractor.applyAsLong(i1), keyExtractor.applyAsLong(i2)); + } + + /** + * Accepts a function that extracts a double sort key, and returns + * a comparator that compares by an extracted double sort key. + * + *

The returned comparator is serializable if the specified function is also serializable. + * + * @param keyExtractor the function used to extract the double sort key. + * @return a comparator that compares by an extracted double sort key. + * @throws NullPointerException if the argument is null. + */ + public static LongComparator comparingDouble(LongToDoubleFunction keyExtractor) { + Objects.requireNonNull(keyExtractor); + return (LongComparator & Serializable) + (i1, i2) -> Double.compare(keyExtractor.applyAsDouble(i1), keyExtractor.applyAsDouble(i2)); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/QuickSort.java b/java_console/logging-api/src/main/java/com/devexperts/util/QuickSort.java new file mode 100644 index 0000000000..6532cd1df0 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/QuickSort.java @@ -0,0 +1,690 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.util.Comparator; +import java.util.List; + +/** + * This class implements modified version of Quick Sort algorithm. + * This implementation offers O(n*log(n)) performance on many data sets + * that cause other quick sort algorithms to degrade to quadratic performance. + * + *

The notable differences of this Quick Sort from other sorting algorithms are: + *

    + *
  • it is unstable - it can re-arrange equal elements in any order; + *
  • it is robust - it can withstand unstable ordering of elements, + * for example if ordering changes during sorting; + *
  • it is garbage-free - it does not allocate any temporary objects. + *
+ * + *

In the case of unstable ordering the result of this algorithm is not necessarily fully sorted, + * but it is guaranteed to complete in a finite amount of time and without exceptions. + * The result in this case would be partially sorted to the best of algorithm's ability. + */ +public class QuickSort { + + /** + * Sorts the specified list into ascending order according + * to the {@linkplain Comparable natural ordering} of its elements. + * All elements in the list must implement the {@link Comparable} interface. + * Furthermore, all elements in the list must be mutually comparable. + * + * @param the class of the objects in the list. + * @param list the list to be sorted. + * @throws ClassCastException if the list contains elements that are not mutually comparable. + */ + public static > void sort(List list) { + quickSort(list, 0, list.size() - 1, null); + } + + /** + * Sorts the specified range of the specified list into ascending order according to the natural ordering of its elements. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in this range must implement the {@link Comparable} interface. + * Furthermore, all elements in this range must be mutually comparable. + * + * @param the class of the objects in the list. + * @param list the list to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + * @throws ClassCastException if the list contains elements that are not mutually comparable. + */ + public static > void sort(List list, int fromIndex, int toIndex) { + rangeCheck(list.size(), fromIndex, toIndex); + quickSort(list, fromIndex, toIndex - 1, null); + } + + /** + * Sorts the specified list according to the order induced by the specified comparator. + * All elements in the list must be mutually comparable using the specified comparator. + * + * @param the class of the objects in the list. + * @param list the list to be sorted. + * @param c the comparator to determine the order of the list. A {@code null} value indicates + * that the elements' natural ordering should be used. + * @throws ClassCastException if the list contains elements that are not mutually comparable + * using the specified comparator. + */ + public static void sort(List list, Comparator c) { + quickSort(list, 0, list.size() - 1, c); + } + + /** + * Sorts the specified range of the specified list according to the order induced by the specified comparator. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in the range must be mutually comparable by the specified comparator. + * + * @param the class of the objects in the list. + * @param list the list to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @param c the comparator to determine the order of the list. A {@code null} value indicates + * that the elements' natural ordering should be used. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + * @throws ClassCastException if the list contains elements that are not mutually comparable + * using the specified comparator. + */ + public static void sort(List list, int fromIndex, int toIndex, Comparator c) { + rangeCheck(list.size(), fromIndex, toIndex); + quickSort(list, fromIndex, toIndex - 1, c); + } + + /** + * Sorts the specified array of objects into ascending order according + * to the {@linkplain Comparable natural ordering} of its elements. + * All elements in the array must implement the {@link Comparable} interface. + * Furthermore, all elements in the array must be mutually comparable. + * + * @param a the array to be sorted. + * @throws ClassCastException if the array contains elements that are not mutually comparable. + */ + public static void sort(Object[] a) { + quickSort(a, 0, a.length - 1, null); + } + + /** + * Sorts the specified range of the specified array of objects into ascending order + * according to the {@linkplain Comparable natural ordering} of its elements. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in this range must implement the {@link Comparable} interface. + * Furthermore, all elements in this range must be mutually comparable. + * + * @param a the array to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + * @throws ClassCastException if the array contains elements that are not mutually comparable. + */ + public static void sort(Object[] a, int fromIndex, int toIndex) { + rangeCheck(a.length, fromIndex, toIndex); + quickSort(a, fromIndex, toIndex - 1, null); + } + + /** + * Sorts the specified array of objects according to the order induced by the specified comparator. + * All elements in the array must be mutually comparable by the specified comparator. + * + * @param the class of the objects to be sorted. + * @param a the array to be sorted. + * @param c the comparator to determine the order of the array. A {@code null} value indicates + * that the elements' {@linkplain Comparable natural ordering} should be used. + * @throws ClassCastException if the array contains elements that are not mutually comparable + * using the specified comparator. + */ + public static void sort(T[] a, Comparator c) { + quickSort(a, 0, a.length - 1, c); + } + + /** + * Sorts the specified range of the specified array of objects according to the order induced by the specified comparator. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in the range must be mutually comparable by the specified comparator. + * + * @param the class of the objects to be sorted. + * @param a the array to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @param c the comparator to determine the order of the array. A {@code null} value indicates + * that the elements' {@linkplain Comparable natural ordering} should be used. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + * @throws ClassCastException if the array contains elements that are not mutually comparable + * using the specified comparator. + */ + public static void sort(T[] a, int fromIndex, int toIndex, Comparator c) { + rangeCheck(a.length, fromIndex, toIndex); + quickSort(a, fromIndex, toIndex - 1, c); + } + + /** + * Sorts the specified array of ints according to the order induced by the specified comparator. + * All elements in the array must be mutually comparable by the specified comparator. + * + * @param a the array to be sorted. + * @param c the comparator to determine the order of the array. + */ + public static void sort(int[] a, IntComparator c) { + quickSort(a, 0, a.length - 1, c); + } + + /** + * Sorts the specified range of the specified array of ints according to the order induced by the specified comparator. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in the range must be mutually comparable by the specified comparator. + * + * @param a the array to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @param c the comparator to determine the order of the array. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + */ + public static void sort(int[] a, int fromIndex, int toIndex, IntComparator c) { + rangeCheck(a.length, fromIndex, toIndex); + quickSort(a, fromIndex, toIndex - 1, c); + } + + /** + * Sorts the specified array of longs according to the order induced by the specified comparator. + * All elements in the array must be mutually comparable by the specified comparator. + * + * @param a the array to be sorted. + * @param c the comparator to determine the order of the array. + */ + public static void sort(long[] a, LongComparator c) { + quickSort(a, 0, a.length - 1, c); + } + + /** + * Sorts the specified range of the specified array of longs according to the order induced by the specified comparator. + * The range to be sorted extends from index {@code fromIndex}, inclusive, to index {@code toIndex}, exclusive. + * All elements in the range must be mutually comparable by the specified comparator. + * + * @param a the array to be sorted. + * @param fromIndex the index of the first element (inclusive) to be sorted. + * @param toIndex the index of the last element (exclusive) to be sorted. + * @param c the comparator to determine the order of the array. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + * @throws IndexOutOfBoundsException if {@code fromIndex < 0} or {@code toIndex > a.length}. + */ + public static void sort(long[] a, int fromIndex, int toIndex, LongComparator c) { + rangeCheck(a.length, fromIndex, toIndex); + quickSort(a, fromIndex, toIndex - 1, c); + } + + // ========== Quick Sort of x[lo..hi] (inclusive) ========== + + private static final int BINARY_INSERT_LIST = 20; + private static final int BINARY_INSERT_ARRAY = 40; + private static final int MOM_START = 400; + private static final int MOM_BASE = 15; + static { + //noinspection ConstantConditions,PointlessBooleanExpression + if (BINARY_INSERT_LIST < 4 || BINARY_INSERT_ARRAY < 4 || MOM_START < 25 || MOM_BASE < 5) + throw new AssertionError("invalid sort constants"); + } + + private static void quickSort(List x, int lo, int hi, Comparator comparator) { + // Quick sort large ranges in a loop to retain stack depth at log(n). + while (hi - lo > BINARY_INSERT_LIST) { + T pivot; + int loScan; + int hiScan; + if (hi - lo > MOM_START) { + // Range is large - perform median-of-medians search of good pivot. + pivot = x.get(medianOfMedians(comparator, x, momStep(lo, hi), lo, hi)); + loScan = lo; + hiScan = hi; + } else { + // Range is small - perform median-of-five search of good pivot. + pivot = x.get(medianOfFive(comparator, x, lo, lo + 1, (lo + hi) >>> 1, hi - 1, hi)); + // Median-of-five rearranges elements around pivot - skip comparisons of 4 outer elements. + loScan = lo + 2; + hiScan = hi - 2; + } + // Excessive checks (loScan <= hiScan) protect from IndexOutOfBoundsException due to unstable ordering. + while (loScan <= hiScan) { + while (loScan <= hiScan && compare(x.get(loScan), pivot, comparator) < 0) + loScan++; + while (loScan <= hiScan && compare(x.get(hiScan), pivot, comparator) > 0) + hiScan--; + if (loScan > hiScan) + break; + T tmp = x.get(loScan); + x.set(loScan, x.get(hiScan)); + x.set(hiScan, tmp); + loScan++; + hiScan--; + } + // Do recursion into smaller range, then do larger range ourselves. + if (hiScan - lo < hi - loScan) { + quickSort(x, lo, hiScan, comparator); + // Protect from degenerate partition when (loScan == lo) due to unstable ordering. + lo = Math.max(loScan, lo + 1); + } else { + quickSort(x, loScan, hi, comparator); + // Protect from degenerate partition when (hiScan == hi) due to unstable ordering. + hi = Math.min(hiScan, hi - 1); + } + } + // Binary insertion sort the remaining small range. + binaryInsertionSort(x, lo, hi, comparator); + } + + private static void quickSort(T[] x, int lo, int hi, Comparator comparator) { + while (hi - lo > BINARY_INSERT_ARRAY) { + T pivot; + int loScan; + int hiScan; + if (hi - lo > MOM_START) { + pivot = x[medianOfMedians(comparator, x, momStep(lo, hi), lo, hi)]; + loScan = lo; + hiScan = hi; + } else { + pivot = x[medianOfFive(comparator, x, lo, lo + 1, (lo + hi) >>> 1, hi - 1, hi)]; + loScan = lo + 2; + hiScan = hi - 2; + } + while (loScan <= hiScan) { + while (loScan <= hiScan && compare(x[loScan], pivot, comparator) < 0) + loScan++; + while (loScan <= hiScan && compare(x[hiScan], pivot, comparator) > 0) + hiScan--; + if (loScan > hiScan) + break; + T tmp = x[loScan]; + x[loScan] = x[hiScan]; + x[hiScan] = tmp; + loScan++; + hiScan--; + } + if (hiScan - lo < hi - loScan) { + quickSort(x, lo, hiScan, comparator); + lo = Math.max(loScan, lo + 1); + } else { + quickSort(x, loScan, hi, comparator); + hi = Math.min(hiScan, hi - 1); + } + } + binaryInsertionSort(x, lo, hi, comparator); + } + + private static void quickSort(int[] x, int lo, int hi, IntComparator comparator) { + while (hi - lo > BINARY_INSERT_ARRAY) { + int pivot; + int loScan; + int hiScan; + if (hi - lo > MOM_START) { + pivot = x[medianOfMedians(comparator, x, momStep(lo, hi), lo, hi)]; + loScan = lo; + hiScan = hi; + } else { + pivot = x[medianOfFive(comparator, x, lo, lo + 1, (lo + hi) >>> 1, hi - 1, hi)]; + loScan = lo + 2; + hiScan = hi - 2; + } + while (loScan <= hiScan) { + while (loScan <= hiScan && compare(x[loScan], pivot, comparator) < 0) + loScan++; + while (loScan <= hiScan && compare(x[hiScan], pivot, comparator) > 0) + hiScan--; + if (loScan > hiScan) + break; + int tmp = x[loScan]; + x[loScan] = x[hiScan]; + x[hiScan] = tmp; + loScan++; + hiScan--; + } + if (hiScan - lo < hi - loScan) { + quickSort(x, lo, hiScan, comparator); + lo = Math.max(loScan, lo + 1); + } else { + quickSort(x, loScan, hi, comparator); + hi = Math.min(hiScan, hi - 1); + } + } + binaryInsertionSort(x, lo, hi, comparator); + } + + private static void quickSort(long[] x, int lo, int hi, LongComparator comparator) { + while (hi - lo > BINARY_INSERT_ARRAY) { + long pivot; + int loScan; + int hiScan; + if (hi - lo > MOM_START) { + pivot = x[medianOfMedians(comparator, x, momStep(lo, hi), lo, hi)]; + loScan = lo; + hiScan = hi; + } else { + pivot = x[medianOfFive(comparator, x, lo, lo + 1, (lo + hi) >>> 1, hi - 1, hi)]; + loScan = lo + 2; + hiScan = hi - 2; + } + while (loScan <= hiScan) { + while (loScan <= hiScan && compare(x[loScan], pivot, comparator) < 0) + loScan++; + while (loScan <= hiScan && compare(x[hiScan], pivot, comparator) > 0) + hiScan--; + if (loScan > hiScan) + break; + long tmp = x[loScan]; + x[loScan] = x[hiScan]; + x[hiScan] = tmp; + loScan++; + hiScan--; + } + if (hiScan - lo < hi - loScan) { + quickSort(x, lo, hiScan, comparator); + lo = Math.max(loScan, lo + 1); + } else { + quickSort(x, loScan, hi, comparator); + hi = Math.min(hiScan, hi - 1); + } + } + binaryInsertionSort(x, lo, hi, comparator); + } + + // ========== Binary Insertion Sort of x[lo..hi] (inclusive) ========== + + private static void binaryInsertionSort(List x, int lo, int hi, Comparator comparator) { + for (int i = lo; ++i <= hi;) { + T pivot = x.get(i); + int left = lo; + for (int right = i; left < right;) { + int mid = (left + right) >>> 1; + if (compare(pivot, x.get(mid), comparator) < 0) + right = mid; + else + left = mid + 1; + } + if (left < i) { + for (int k = i; k > left; k--) + x.set(k, x.get(k - 1)); + x.set(left, pivot); + } + } + } + + private static void binaryInsertionSort(T[] x, int lo, int hi, Comparator comparator) { + for (int i = lo; ++i <= hi;) { + T pivot = x[i]; + int left = lo; + for (int right = i; left < right;) { + int mid = (left + right) >>> 1; + if (compare(pivot, x[mid], comparator) < 0) + right = mid; + else + left = mid + 1; + } + if (left < i) { + System.arraycopy(x, left, x, left + 1, i - left); + x[left] = pivot; + } + } + } + + private static void binaryInsertionSort(int[] x, int lo, int hi, IntComparator comparator) { + for (int i = lo; ++i <= hi;) { + int pivot = x[i]; + int left = lo; + for (int right = i; left < right;) { + int mid = (left + right) >>> 1; + if (compare(pivot, x[mid], comparator) < 0) + right = mid; + else + left = mid + 1; + } + if (left < i) { + System.arraycopy(x, left, x, left + 1, i - left); + x[left] = pivot; + } + } + } + + private static void binaryInsertionSort(long[] x, int lo, int hi, LongComparator comparator) { + for (int i = lo; ++i <= hi;) { + long pivot = x[i]; + int left = lo; + for (int right = i; left < right;) { + int mid = (left + right) >>> 1; + if (compare(pivot, x[mid], comparator) < 0) + right = mid; + else + left = mid + 1; + } + if (left < i) { + System.arraycopy(x, left, x, left + 1, i - left); + x[left] = pivot; + } + } + } + + // ========== Median Of Medians ========== + // Finds median of medians using quinary tree and median of five in each node. + // Expected number of used elements is pow(5, 1 + ceil(log(1 + size / MOM_START, MOM_BASE))). + // All used elements are spaced evenly (as much as possible) using "step" step. + + private static int momStep(int lo, int hi) { + int mult = 5; + for (int k = (int)((hi - lo + 1L) / MOM_START); k > 0; k /= MOM_BASE) + mult *= 5; + while (hi - lo < mult - 1 && mult > 5) + mult /= 5; + return (hi - lo) / (mult - 1); + } + + private static int medianOfMedians(Comparator comparator, List x, int step, int lo, int hi) { + int ns = (hi - lo - step * 4) / 5; + if (ns < step * 4) + return medianOfFive(comparator, x, lo, lo + step, (lo + hi) >>> 1, hi - step, hi); + int bs = ns + step; + return medianOfFive(comparator, x, + medianOfMedians(comparator, x, step, lo, lo + ns), + medianOfMedians(comparator, x, step, lo + bs, lo + bs + ns), + medianOfMedians(comparator, x, step, lo + bs + bs, hi - bs - bs), + medianOfMedians(comparator, x, step, hi - bs - ns, hi - bs), + medianOfMedians(comparator, x, step, hi - ns, hi) + ); + } + + private static int medianOfMedians(Comparator comparator, T[] x, int step, int lo, int hi) { + int ns = (hi - lo - step * 4) / 5; + if (ns < step * 4) + return medianOfFive(comparator, x, lo, lo + step, (lo + hi) >>> 1, hi - step, hi); + int bs = ns + step; + return medianOfFive(comparator, x, + medianOfMedians(comparator, x, step, lo, lo + ns), + medianOfMedians(comparator, x, step, lo + bs, lo + bs + ns), + medianOfMedians(comparator, x, step, lo + bs + bs, hi - bs - bs), + medianOfMedians(comparator, x, step, hi - bs - ns, hi - bs), + medianOfMedians(comparator, x, step, hi - ns, hi) + ); + } + + private static int medianOfMedians(IntComparator comparator, int[] x, int step, int lo, int hi) { + int ns = (hi - lo - step * 4) / 5; + if (ns < step * 4) + return medianOfFive(comparator, x, lo, lo + step, (lo + hi) >>> 1, hi - step, hi); + int bs = ns + step; + return medianOfFive(comparator, x, + medianOfMedians(comparator, x, step, lo, lo + ns), + medianOfMedians(comparator, x, step, lo + bs, lo + bs + ns), + medianOfMedians(comparator, x, step, lo + bs + bs, hi - bs - bs), + medianOfMedians(comparator, x, step, hi - bs - ns, hi - bs), + medianOfMedians(comparator, x, step, hi - ns, hi) + ); + } + + private static int medianOfMedians(LongComparator comparator, long[] x, int step, int lo, int hi) { + int ns = (hi - lo - step * 4) / 5; + if (ns < step * 4) + return medianOfFive(comparator, x, lo, lo + step, (lo + hi) >>> 1, hi - step, hi); + int bs = ns + step; + return medianOfFive(comparator, x, + medianOfMedians(comparator, x, step, lo, lo + ns), + medianOfMedians(comparator, x, step, lo + bs, lo + bs + ns), + medianOfMedians(comparator, x, step, lo + bs + bs, hi - bs - bs), + medianOfMedians(comparator, x, step, hi - bs - ns, hi - bs), + medianOfMedians(comparator, x, step, hi - ns, hi) + ); + } + + // ========== Median Of Five ========== + // Finds median of 5 elements using 6 comparisons. See first method for algorithm explanation. + // All methods do reorder their input around median, thus performing partial sorting. + // This side effect is used by quick sort algorithms to skip comparisons of 4 outer elements. + // This side effect is useless for median of medians algorithms, but by using same methods we save on bytecode. + + private static int medianOfFive(Comparator comparator, List x, int ai, int bi, int ci, int di, int ei) { + T a = x.get(ai); + T b = x.get(bi); + T c = x.get(ci); + T d = x.get(di); + T e = x.get(ei); + T t; + // (a, b, c, d, e) - sort (a, b) + if (compare(a, b, comparator) > 0) { t = a; a = b; b = t; } + // (a < b, c, d, e) - sort (d, e) + if (compare(d, e, comparator) > 0) { t = d; d = e; e = t; } + // (a < b, c, d < e) - sort pairs (a < b, d < e) by lowest of (a, d) + if (compare(a, d, comparator) > 0) { t = a; a = d; d = t; t = b; b = e; e = t; } + // (a < b, c, a < d < e) - now [a] < [b, d, e], put it aside + // [a] < [b, d, e] (b, c, d < e) - sort (b, c) + if (compare(b, c, comparator) > 0) { t = b; b = c; c = t; } + // [a] < [c, d, e] (b < c, d < e) - sort pairs (b < c, d < e) by lowest of (b, d) + if (compare(b, d, comparator) > 0) { t = b; b = d; d = t; t = c; c = e; e = t; } + // [a] < [c, d, e] (b < c, b < d < e) - now [b] < [c, d, e], put it aside + // [a, b] < [c, d, e] (c, d < e) - sort (c, d) + if (compare(c, d, comparator) > 0) { t = c; c = d; d = t; } + // [a, b] < [c, d, e] (c < d, c < e) - now [c] < [d, e], rewrite + // [a, b] < [c] < [d, e] - [c] is a median + x.set(ai, a); + x.set(bi, b); + x.set(ci, c); + x.set(di, d); + x.set(ei, e); + return ci; + } + + private static int medianOfFive(Comparator comparator, T[] x, int ai, int bi, int ci, int di, int ei) { + T a = x[ai]; + T b = x[bi]; + T c = x[ci]; + T d = x[di]; + T e = x[ei]; + T t; + if (compare(a, b, comparator) > 0) { t = a; a = b; b = t; } + if (compare(d, e, comparator) > 0) { t = d; d = e; e = t; } + if (compare(a, d, comparator) > 0) { t = a; a = d; d = t; t = b; b = e; e = t; } + if (compare(b, c, comparator) > 0) { t = b; b = c; c = t; } + if (compare(b, d, comparator) > 0) { t = b; b = d; d = t; t = c; c = e; e = t; } + if (compare(c, d, comparator) > 0) { t = c; c = d; d = t; } + x[ai] = a; + x[bi] = b; + x[ci] = c; + x[di] = d; + x[ei] = e; + return ci; + } + + private static int medianOfFive(IntComparator comparator, int[] x, int ai, int bi, int ci, int di, int ei) { + int a = x[ai]; + int b = x[bi]; + int c = x[ci]; + int d = x[di]; + int e = x[ei]; + int t; + if (compare(a, b, comparator) > 0) { t = a; a = b; b = t; } + if (compare(d, e, comparator) > 0) { t = d; d = e; e = t; } + if (compare(a, d, comparator) > 0) { t = a; a = d; d = t; t = b; b = e; e = t; } + if (compare(b, c, comparator) > 0) { t = b; b = c; c = t; } + if (compare(b, d, comparator) > 0) { t = b; b = d; d = t; t = c; c = e; e = t; } + if (compare(c, d, comparator) > 0) { t = c; c = d; d = t; } + x[ai] = a; + x[bi] = b; + x[ci] = c; + x[di] = d; + x[ei] = e; + return ci; + } + + private static int medianOfFive(LongComparator comparator, long[] x, int ai, int bi, int ci, int di, int ei) { + long a = x[ai]; + long b = x[bi]; + long c = x[ci]; + long d = x[di]; + long e = x[ei]; + long t; + if (compare(a, b, comparator) > 0) { t = a; a = b; b = t; } + if (compare(d, e, comparator) > 0) { t = d; d = e; e = t; } + if (compare(a, d, comparator) > 0) { t = a; a = d; d = t; t = b; b = e; e = t; } + if (compare(b, c, comparator) > 0) { t = b; b = c; c = t; } + if (compare(b, d, comparator) > 0) { t = b; b = d; d = t; t = c; c = e; e = t; } + if (compare(c, d, comparator) > 0) { t = c; c = d; d = t; } + x[ai] = a; + x[bi] = b; + x[ci] = c; + x[di] = d; + x[ei] = e; + return ci; + } + + // ========== Utility Code ========== + + /** + * Compares specified objects using either specified comparator or their natural ordering. + */ + @SuppressWarnings("unchecked") + private static int compare(Object o1, Object o2, Comparator c) { + // Boost performance and protect from degenerate partition due to unstable ordering. + if (o1 == o2) + return 0; + return c != null ? c.compare(o1, o2) : ((Comparable)o1).compareTo(o2); + } + + private static int compare(int i1, int i2, IntComparator c) { + // Boost performance and protect from degenerate partition due to unstable ordering. + if (i1 == i2) + return 0; + return c.compare(i1, i2); + } + + private static int compare(long i1, long i2, LongComparator c) { + // Boost performance and protect from degenerate partition due to unstable ordering. + if (i1 == i2) + return 0; + return c.compare(i1, i2); + } + + /** + * Checks that fromIndex and toIndex are in range and throws appropriate exception if they aren't. + */ + private static void rangeCheck(int length, int fromIndex, int toIndex) { + if (fromIndex > toIndex) + throw new IllegalArgumentException("fromIndex " + fromIndex + " > toIndex " + toIndex); + if (fromIndex < 0) + throw new IndexOutOfBoundsException("fromIndex " + fromIndex + " < 0"); + if (toIndex > length) + throw new IndexOutOfBoundsException("toIndex " + toIndex + " > length " + length); + } + + /** + * Private constructor to prevent instantiation. + */ + private QuickSort() {} +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/SynchronizedIndexedSet.java b/java_console/logging-api/src/main/java/com/devexperts/util/SynchronizedIndexedSet.java new file mode 100644 index 0000000000..992b488f7a --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/SynchronizedIndexedSet.java @@ -0,0 +1,452 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collector; + +/** + * A synchronized thread-safe version of {@link IndexedSet} class. + * It provides following benefits over standard {@link IndexedSet}: + * + *

    + *
  • concurrent asynchronous read access + *
  • synchronized thread-safe write access + *
  • all iterators are concurrent + *
+ * + *

Note that SynchronizedIndexedSet can be wrapped by {@link IndexedMap} + * to create what can be considered a SynchronizedIndexedMap. + */ +public class SynchronizedIndexedSet extends IndexedSet { + private static final long serialVersionUID = 0; + + // ========== static factory methods =========== + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT}. + */ + public static SynchronizedIndexedSet create() { + return new SynchronizedIndexedSet<>(); + } + + /** + * Creates new empty set with default identity indexer. + */ + public static SynchronizedIndexedSet createIdentity() { + return new SynchronizedIndexedSet<>((IndexerFunction.IdentityKey)(v -> v)); + } + + /** + * Creates new empty set with specified indexer. + */ + public static SynchronizedIndexedSet create(IndexerFunction indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified identity indexer. + */ + public static SynchronizedIndexedSet createIdentity(IndexerFunction.IdentityKey indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified int indexer. + */ + public static SynchronizedIndexedSet createInt(IndexerFunction.IntKey indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified long indexer. + */ + public static SynchronizedIndexedSet createLong(IndexerFunction.LongKey indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction.IntKey indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #createLong(IndexerFunction.LongKey) createLong(indexer)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction.LongKey indexer) { + return new SynchronizedIndexedSet<>(indexer); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction indexer, int initialCapacity) { + return new SynchronizedIndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction.IntKey indexer, int initialCapacity) { + return new SynchronizedIndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #createLong(IndexerFunction.LongKey) createLong(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction.LongKey indexer, int initialCapacity) { + return new SynchronizedIndexedSet<>(indexer, initialCapacity); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction indexer, Collection c) { + return new SynchronizedIndexedSet<>(indexer, c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #createInt(IndexerFunction.IntKey) createInt(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public static SynchronizedIndexedSet create(IndexerFunction.IntKey indexer, Collection c) { + return new SynchronizedIndexedSet<>(indexer, c); + } + + /** + * Creates a new set with default indexer containing specified elements. + */ + @SafeVarargs + public static SynchronizedIndexedSet of(V... objs) { + return new SynchronizedIndexedSet<>(Arrays.asList(objs)); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with default indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + @SuppressWarnings("unchecked") + public static Collector> collector() { + return collector((IndexerFunction)IndexerFunction.DEFAULT); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with default identity indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorIdentity() { + return collector((IndexerFunction.IdentityKey)(v -> v)); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collector(IndexerFunction indexer) { + return Collector.of(() -> create(indexer), IndexedSet::add, + (left, right) -> { left.addAll(right); return left; }, + Collector.Characteristics.CONCURRENT, Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified identity indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorIdentity(IndexerFunction.IdentityKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified int indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorInt(IndexerFunction.IntKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified long indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + */ + public static Collector> collectorLong(IndexerFunction.LongKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + * + * @deprecated Use {@link #collectorInt(IndexerFunction.IntKey) collectorInt(indexer)} + */ + @Deprecated + public static Collector> collector(IndexerFunction.IntKey indexer) { + return collector((IndexerFunction)indexer); + } + + /** + * Returns a {@code Collector} that accumulates the input elements into a new {@code SynchronizedIndexedSet} with specified indexer. + * This is an {@link Collector.Characteristics#CONCURRENT concurrent} and {@link Collector.Characteristics#UNORDERED unordered} Collector. + * + * @deprecated Use {@link #collectorLong(IndexerFunction.LongKey) collectorLong(indexer)} + */ + @Deprecated + public static Collector> collector(IndexerFunction.LongKey indexer) { + return collector((IndexerFunction)indexer); + } + + // ========== Construction and Sizing Operations ========== + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT}. + */ + public SynchronizedIndexedSet() { + super(); + } + + /** + * Creates new empty set with default indexer {@link IndexerFunction#DEFAULT} and specified initial capacity. + */ + public SynchronizedIndexedSet(int initialCapacity) { + super(initialCapacity); + } + + /** + * Creates new empty set with specified indexer. + */ + protected SynchronizedIndexedSet(IndexerFunction indexer) { + super(indexer); + } + + /** + * Creates new empty set with specified indexer. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)} + */ + @Deprecated + public SynchronizedIndexedSet(Indexer indexer) { + super(indexer); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + */ + protected SynchronizedIndexedSet(IndexerFunction indexer, int initialCapacity) { + super(indexer, initialCapacity); + } + + /** + * Creates new empty set with specified indexer and specified initial capacity. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withCapacity(int) withCapacity(initialCapacity)} + */ + @Deprecated + public SynchronizedIndexedSet(Indexer indexer, int initialCapacity) { + super(indexer, initialCapacity); + } + + /** + * Creates a new set containing the elements in the specified collection. + * If specified collection is an {@link IndexedSet}, then new indexed set uses same indexer, + * otherwise it uses default indexer {@link IndexerFunction#DEFAULT}. + */ + public SynchronizedIndexedSet(Collection c) { + super(c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + */ + protected SynchronizedIndexedSet(IndexerFunction indexer, Collection c) { + super(indexer, c); + } + + /** + * Creates a new set with specified indexer containing the elements in the specified collection. + * + * @deprecated Use {@link #create(IndexerFunction) create(indexer)}.{@link #withElements(Collection) withElements(c)} + */ + @Deprecated + public SynchronizedIndexedSet(Indexer indexer, Collection c) { + super(indexer, c); + } + + /** + * Returns a shallow copy of this set - the values themselves are not cloned. + */ + @Override + public synchronized SynchronizedIndexedSet clone() { + return (SynchronizedIndexedSet)super.clone(); + } + + /** + * Increases the capacity of this set instance, if necessary, to ensure that it + * can hold at least the number of elements specified by the capacity argument. + *

+ * Returns this set instance for convenience. + */ + @Override + public synchronized SynchronizedIndexedSet withCapacity(int capacity) { + return (SynchronizedIndexedSet)super.withCapacity(capacity); + } + + /** + * Adds all of the elements in the specified collection into this set. + *

+ * Returns this set instance for convenience. + */ + @Override + public synchronized SynchronizedIndexedSet withElements(Collection c) { + return (SynchronizedIndexedSet)super.withElements(c); + } + + /** + * Increases the capacity of this set instance, if necessary, to ensure that it + * can hold at least the number of elements specified by the capacity argument. + */ + @Override + public synchronized void ensureCapacity(int capacity) { + super.ensureCapacity(capacity); + } + + /** + * Trims the capacity of this set instance to be the set's current size. + * An application can use this operation to minimize the storage of this set instance. + */ + @Override + public synchronized void trimToSize() { + super.trimToSize(); + } + + /** + * Removes all elements from this set. + */ + @Override + public synchronized void clear() { + super.clear(); + } + + // ========== Query Operations ========== + + /** + * Returns static structure statistics of this set. + */ + @Override + public synchronized IndexedSetStats getStats() { + // This method is synchronized to provide consistent view of several cross-linked variables. + // It should not pose any contention risk anyway. + return super.getStats(); + } + + // ========== Modification Operations ========== + + /** + * Puts specified element into this set and returns previous element that matches specified one. + */ + @Override + public synchronized V put(V value) { + return super.put(value); + } + + /** + * Puts specified element into this set if it is absent and + * returns current element in the set that matches specified one. + * This is equivalent to + *

+	 *   if (set.containsValue(value)) {
+	 *     return set.getByValue(value);
+	 *   } else {
+	 *     set.put(value);
+	 *     return value;
+	 *   }
+	 * 
+ * except that the action is performed atomically if it is properly synchronized. + *

+ * Note, that unlike {@link ConcurrentMap#putIfAbsent}, + * this method returns specified value (not null) if the value was absent. + */ + @Override + public synchronized V putIfAbsentAndGet(V value) { + return super.putIfAbsentAndGet(value); + } + + /** + * Removes the element from this set which matches specified value if it is present + * and returns removed element or null if none were found. + */ + @Override + public synchronized V removeValue(V value) { + return super.removeValue(value); + } + + /** + * Removes the element from this set which matches specified key if it is present + * and returns removed element or null if none were found. + */ + @Override + public synchronized V removeKey(K key) { + return super.removeKey(key); + } + + /** + * Removes the element from this set which matches specified key if it is present + * and returns removed element or null if none were found. + */ + @Override + public synchronized V removeKey(long key) { + return super.removeKey(key); + } + + // ========== Internal Implementation - Helper Instance Methods ========== + + @Override + void checkModification(Object checkCore, long checkModCount) { + // Do nothing - all iterators are concurrent. + } + + @Override + synchronized void removeIterated(Object checkCore, long checkModCount, boolean concurrent, V lastValue, int lastIndex) { + super.removeIterated(checkCore, checkModCount, true, lastValue, lastIndex); + } + + @Override + synchronized void writeCore(ObjectOutputStream out) throws IOException { + // This method is synchronized to provide consistent serialization. + // It should not pose any contention risk anyway. + super.writeCore(out); + } +} diff --git a/java_console/logging-api/src/main/java/com/devexperts/util/TimeUtil.java b/java_console/logging-api/src/main/java/com/devexperts/util/TimeUtil.java new file mode 100644 index 0000000000..d293117499 --- /dev/null +++ b/java_console/logging-api/src/main/java/com/devexperts/util/TimeUtil.java @@ -0,0 +1,65 @@ +/* + * !++ + * QDS - Quick Data Signalling Library + * !- + * Copyright (C) 2002 - 2020 Devexperts LLC + * !- + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, You can obtain one at + * http://mozilla.org/MPL/2.0/. + * !__ + */ +package com.devexperts.util; + +/** + * A collection of static utility methods for manipulation of Java long time. + * @see System#currentTimeMillis() + */ +public class TimeUtil { + private TimeUtil() {} // do not create this class + + /** + * Number of milliseconds in a second. + */ + public static final long SECOND = 1000; + + /** + * Number of milliseconds in a minute. + */ + public static final long MINUTE = 60 * SECOND; + + /** + * Number of milliseconds in an hour. + */ + public static final long HOUR = 60 * MINUTE; + + /** + * Number of milliseconds in an day. + */ + public static final long DAY = 24 * HOUR; + + /** + * Returns correct number of seconds with proper handling negative values and overflows. + * Idea is that number of milliseconds shall be within [0..999] interval + * so that the following equation always holds + * {@code getSecondsFromTime(timeMillis) * 1000L + getMillisFromTime(timeMillis) == timeMillis} + * as as long the time in seconds fits into int. + * @see #getMillisFromTime(long) + */ + public static int getSecondsFromTime(long timeMillis) { + return timeMillis >= 0 ? (int)Math.min(timeMillis / SECOND, Integer.MAX_VALUE) : + (int)Math.max((timeMillis + 1) / SECOND - 1, Integer.MIN_VALUE); + } + + /** + * Returns correct number of milliseconds with proper handling negative values. + * Idea is that number of milliseconds shall be within [0..999] interval + * so that the following equation always holds + * {@code getSecondsFromTime(timeMillis) * 1000L + getMillisFromTime(timeMillis) == timeMillis} + * as as long the time in seconds fits into int. + * @see #getSecondsFromTime(long) + */ + public static int getMillisFromTime(long timeMillis) { + return (int)Math.floorMod(timeMillis, SECOND); + } +}