Skip to content

Instantly share code, notes, and snippets.

@Pyknic
Created July 27, 2018 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pyknic/5983fbc8fd777738a510df34fdfcf1e6 to your computer and use it in GitHub Desktop.
Save Pyknic/5983fbc8fd777738a510df34fdfcf1e6 to your computer and use it in GitHub Desktop.
package com.speedment.extra.slf4j;
import com.speedment.common.logger.*;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static java.lang.String.format;
import static java.util.Collections.newSetFromMap;
import static java.util.Objects.requireNonNull;
/**
* @author Emil Forslund
* @since 1.0.0
*/
public final class Slf4jLogger implements Logger {
private Level level;
private LoggerFormatter formatter; // Not used by this implementation, since Slf4j formats the logs
private org.slf4j.Logger inner; // All operations are delegated to this instance.
private final String name;
private final Set<LoggerEventListener> listeners;
Slf4jLogger(org.slf4j.Logger inner, LoggerFormatter formatter, Level level) {
this.inner = requireNonNull(inner);
this.name = requireNonNull(inner.getName());
this.formatter = requireNonNull(formatter);
this.level = requireNonNull(level);
this.listeners = newSetFromMap(new ConcurrentHashMap<>());
}
@Override
public Level getLevel() {
return level;
}
@Override
public void setLevel(Level level) {
this.level = requireNonNull(level);
}
@Override
public void setFormatter(LoggerFormatter formatter) {
this.formatter = requireNonNull(formatter);
}
@Override
public LoggerFormatter getFormatter() {
return formatter;
}
@Override
public void addListener(LoggerEventListener listener) {
listeners.add(requireNonNull(listener));
}
@Override
public void removeListener(LoggerEventListener listener) {
listeners.remove(requireNonNull(listener));
}
// Trace
@Override
public void trace(String message) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace(message);
notifyListeners(Level.TRACE, message);
}
}
@Override
public void trace(Throwable throwable) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace("", throwable);
notifyListeners(Level.TRACE, throwable);
}
}
@Override
public void trace(String format, Object arg) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace(format, arg);
notifyListeners(Level.TRACE, format(format, arg));
}
}
@Override
public void trace(String format, Object arg1, Object arg2) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace(format, arg1, arg2);
notifyListeners(Level.TRACE, format(format, arg1, arg2));
}
}
@Override
public void trace(String format, Object arg1, Object arg2, Object arg3) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace(format, arg1, arg2, arg3);
notifyListeners(Level.TRACE, format(format, arg1, arg2, arg3));
}
}
@Override
public void trace(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.TRACE, format, first, args);
notifyListeners(Level.TRACE, format(format, concat(first, args)));
}
}
@Override
public void trace(Throwable throwable, String message) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
inner.trace(message, throwable);
notifyListeners(Level.TRACE, message, throwable);
}
}
@Override
public void trace(Throwable throwable, String format, Object arg) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.trace(msg, throwable);
notifyListeners(Level.TRACE, msg, throwable);
}
}
@Override
public void trace(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.trace(msg, throwable);
notifyListeners(Level.TRACE, msg, throwable);
}
}
@Override
public void trace(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.trace(msg, throwable);
notifyListeners(Level.TRACE, msg, throwable);
}
}
@Override
public void trace(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.TRACE.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.trace(msg, throwable);
notifyListeners(Level.TRACE, msg, throwable);
}
}
// Debug
@Override
public void debug(String message) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug(message);
notifyListeners(Level.DEBUG, message);
}
}
@Override
public void debug(Throwable throwable) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug("", throwable);
notifyListeners(Level.DEBUG, throwable);
}
}
@Override
public void debug(String format, Object arg) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug(format, arg);
notifyListeners(Level.DEBUG, format(format, arg));
}
}
@Override
public void debug(String format, Object arg1, Object arg2) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug(format, arg1, arg2);
notifyListeners(Level.DEBUG, format(format, arg1, arg2));
}
}
@Override
public void debug(String format, Object arg1, Object arg2, Object arg3) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug(format, arg1, arg2, arg3);
notifyListeners(Level.DEBUG, format(format, arg1, arg2, arg3));
}
}
@Override
public void debug(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.DEBUG, format, first, args);
notifyListeners(Level.DEBUG, format(format, concat(first, args)));
}
}
@Override
public void debug(Throwable throwable, String message) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
inner.debug(message, throwable);
notifyListeners(Level.DEBUG, message, throwable);
}
}
@Override
public void debug(Throwable throwable, String format, Object arg) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.debug(msg, throwable);
notifyListeners(Level.DEBUG, msg, throwable);
}
}
@Override
public void debug(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.debug(msg, throwable);
notifyListeners(Level.DEBUG, msg, throwable);
}
}
@Override
public void debug(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.debug(msg, throwable);
notifyListeners(Level.DEBUG, msg, throwable);
}
}
@Override
public void debug(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.DEBUG.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.debug(msg, throwable);
notifyListeners(Level.DEBUG, msg, throwable);
}
}
// Info
@Override
public void info(String message) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info(message);
notifyListeners(Level.INFO, message);
}
}
@Override
public void info(Throwable throwable) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info("", throwable);
notifyListeners(Level.INFO, throwable);
}
}
@Override
public void info(String format, Object arg) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info(format, arg);
notifyListeners(Level.INFO, format(format, arg));
}
}
@Override
public void info(String format, Object arg1, Object arg2) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info(format, arg1, arg2);
notifyListeners(Level.INFO, format(format, arg1, arg2));
}
}
@Override
public void info(String format, Object arg1, Object arg2, Object arg3) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info(format, arg1, arg2, arg3);
notifyListeners(Level.INFO, format(format, arg1, arg2, arg3));
}
}
@Override
public void info(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.INFO.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.INFO, format, first, args);
notifyListeners(Level.INFO, format(format, concat(first, args)));
}
}
@Override
public void info(Throwable throwable, String message) {
if (Level.INFO.isEqualOrHigherThan(level)) {
inner.info(message, throwable);
notifyListeners(Level.INFO, message, throwable);
}
}
@Override
public void info(Throwable throwable, String format, Object arg) {
if (Level.INFO.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.info(msg, throwable);
notifyListeners(Level.INFO, msg, throwable);
}
}
@Override
public void info(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.INFO.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.info(msg, throwable);
notifyListeners(Level.INFO, msg, throwable);
}
}
@Override
public void info(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.INFO.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.info(msg, throwable);
notifyListeners(Level.INFO, msg, throwable);
}
}
@Override
public void info(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.INFO.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.info(msg, throwable);
notifyListeners(Level.INFO, msg, throwable);
}
}
// Warn
@Override
public void warn(String message) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn(message);
notifyListeners(Level.WARN, message);
}
}
@Override
public void warn(Throwable throwable) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn("", throwable);
notifyListeners(Level.WARN, throwable);
}
}
@Override
public void warn(String format, Object arg) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn(format, arg);
notifyListeners(Level.WARN, format(format, arg));
}
}
@Override
public void warn(String format, Object arg1, Object arg2) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn(format, arg1, arg2);
notifyListeners(Level.WARN, format(format, arg1, arg2));
}
}
@Override
public void warn(String format, Object arg1, Object arg2, Object arg3) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn(format, arg1, arg2, arg3);
notifyListeners(Level.WARN, format(format, arg1, arg2, arg3));
}
}
@Override
public void warn(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.WARN.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.WARN, format, first, args);
notifyListeners(Level.WARN, format(format, concat(first, args)));
}
}
@Override
public void warn(Throwable throwable, String message) {
if (Level.WARN.isEqualOrHigherThan(level)) {
inner.warn(message, throwable);
notifyListeners(Level.WARN, message, throwable);
}
}
@Override
public void warn(Throwable throwable, String format, Object arg) {
if (Level.WARN.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.warn(msg, throwable);
notifyListeners(Level.WARN, msg, throwable);
}
}
@Override
public void warn(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.WARN.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.warn(msg, throwable);
notifyListeners(Level.WARN, msg, throwable);
}
}
@Override
public void warn(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.WARN.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.warn(msg, throwable);
notifyListeners(Level.WARN, msg, throwable);
}
}
@Override
public void warn(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.WARN.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.warn(msg, throwable);
notifyListeners(Level.WARN, msg, throwable);
}
}
// Error
@Override
public void error(String message) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error(message);
notifyListeners(Level.ERROR, message);
}
}
@Override
public void error(Throwable throwable) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error("", throwable);
notifyListeners(Level.ERROR, throwable);
}
}
@Override
public void error(String format, Object arg) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error(format, arg);
notifyListeners(Level.ERROR, format(format, arg));
}
}
@Override
public void error(String format, Object arg1, Object arg2) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error(format, arg1, arg2);
notifyListeners(Level.ERROR, format(format, arg1, arg2));
}
}
@Override
public void error(String format, Object arg1, Object arg2, Object arg3) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error(format, arg1, arg2, arg3);
notifyListeners(Level.ERROR, format(format, arg1, arg2, arg3));
}
}
@Override
public void error(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.ERROR, format, first, args);
notifyListeners(Level.ERROR, format(format, concat(first, args)));
}
}
@Override
public void error(Throwable throwable, String message) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
inner.error(message, throwable);
notifyListeners(Level.ERROR, message, throwable);
}
}
@Override
public void error(Throwable throwable, String format, Object arg) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.error(msg, throwable);
notifyListeners(Level.ERROR, msg, throwable);
}
}
@Override
public void error(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.error(msg, throwable);
notifyListeners(Level.ERROR, msg, throwable);
}
}
@Override
public void error(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.error(msg, throwable);
notifyListeners(Level.ERROR, msg, throwable);
}
}
@Override
public void error(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.ERROR.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.error(msg, throwable);
notifyListeners(Level.ERROR, msg, throwable);
}
}
// Fatal
@Override
public void fatal(String message) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error(message);
notifyListeners(Level.FATAL, message);
}
}
@Override
public void fatal(Throwable throwable) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error("", throwable);
notifyListeners(Level.FATAL, throwable);
}
}
@Override
public void fatal(String format, Object arg) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error(format, arg);
notifyListeners(Level.FATAL, format(format, arg));
}
}
@Override
public void fatal(String format, Object arg1, Object arg2) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error(format, arg1, arg2);
notifyListeners(Level.FATAL, format(format, arg1, arg2));
}
}
@Override
public void fatal(String format, Object arg1, Object arg2, Object arg3) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error(format, arg1, arg2, arg3);
notifyListeners(Level.FATAL, format(format, arg1, arg2, arg3));
}
}
@Override
public void fatal(String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
final Object[] first = new Object[] {arg1, arg2, arg3};
log(Level.FATAL, format, first, args);
notifyListeners(Level.FATAL, format(format, concat(first, args)));
}
}
@Override
public void fatal(Throwable throwable, String message) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
inner.error(message, throwable);
notifyListeners(Level.FATAL, message, throwable);
}
}
@Override
public void fatal(Throwable throwable, String format, Object arg) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
final String msg = format(format, arg);
inner.error(msg, throwable);
notifyListeners(Level.FATAL, msg, throwable);
}
}
@Override
public void fatal(Throwable throwable, String format, Object arg1, Object arg2) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2);
inner.error(msg, throwable);
notifyListeners(Level.FATAL, msg, throwable);
}
}
@Override
public void fatal(Throwable throwable, String format, Object arg1, Object arg2, Object arg3) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
final String msg = format(format, arg1, arg2, arg3);
inner.error(msg, throwable);
notifyListeners(Level.FATAL, msg, throwable);
}
}
@Override
public void fatal(Throwable throwable, String format, Object arg1, Object arg2, Object arg3, Object... args) {
if (Level.FATAL.isEqualOrHigherThan(level)) {
final String msg = format(format, concat(new Object[] {arg1, arg2, arg3}, args));
inner.error(msg, throwable);
notifyListeners(Level.FATAL, msg, throwable);
}
}
private void log(Level msgLevel, String format, Object[] first, Object[] then) {
final Object[] array = concat(first, then);
switch (msgLevel) {
case TRACE: inner.trace(format, array); break;
case DEBUG: inner.debug(format, array); break;
case INFO: inner.info(format, array); break;
case WARN: inner.warn(format, array); break;
case ERROR: inner.error(format, array); break;
case FATAL: inner.error(format, array); break;
default: throw new UnsupportedOperationException();
}
}
private void notifyListeners(Level level, String message) {
final LoggerEvent ev = new SimpleLoggerEvent(level, name, message);
listeners.forEach(listener -> listener.accept(ev));
}
private void notifyListeners(Level level, Throwable thrw) {
notifyListeners(level, thrw.getClass().getSimpleName() + ": " + thrw.getMessage());
}
private void notifyListeners(Level level, String message, Throwable thrw) {
notifyListeners(level, thrw.getClass().getSimpleName() + ": " + message);
}
private static Object[] concat(Object[] first, Object[] then) {
final Object[] array = new Object[first.length + then.length];
System.arraycopy(first, 0, array, 0, first.length);
System.arraycopy(then, 0, array, first.length, then.length);
return array;
}
private final static class SimpleLoggerEvent implements LoggerEvent {
private final Level level;
private final String name, message;
private SimpleLoggerEvent(Level level, String name, String message) {
this.level = requireNonNull(level);
this.name = requireNonNull(name);
this.message = requireNonNull(message);
}
@Override
public Level getLevel() {
return level;
}
@Override
public String getName() {
return name;
}
@Override
public String getMessage() {
return message;
}
@Override
public String toString() {
return format("{level=%s, name='%s', message='%s'}", level, name, message);
}
}
}
package com.speedment.extra.slf4j;
import com.speedment.common.logger.*;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static com.speedment.common.logger.internal.util.NullUtil.requireNonNulls;
import static java.util.Collections.newSetFromMap;
import static java.util.Objects.requireNonNull;
/**
* @author Emil Forslund
* @since 1.0.0
*/
public final class Slf4jLoggerFactory implements LoggerFactory {
private LoggerFormatter formatter;
private Level level;
private final Map<String, Logger> loggers;
private final Set<LoggerEventListener> listeners;
public Slf4jLoggerFactory() {
this.loggers = new ConcurrentHashMap<>();
this.formatter = new Slf4jLoggerFormatter();
this.level = Level.defaultLevel();
this.listeners = newSetFromMap(new ConcurrentHashMap<>());
}
@Override
public Logger create(Class<?> binding) {
return prepare(binding, org.slf4j.LoggerFactory.getLogger(binding));
}
@Override
public Logger create(String binding) {
return prepare(null, org.slf4j.LoggerFactory.getLogger(binding));
}
@Override
public Class<? extends Logger> loggerClass() {
return Slf4jLogger.class;
}
@Override
public void setFormatter(LoggerFormatter formatter) {
this.formatter = requireNonNull(formatter);
}
@Override
public LoggerFormatter getFormatter() {
return formatter;
}
@Override
public void addListener(LoggerEventListener listener) {
if (listeners.add(listener)) {
forEachLogger(log -> log.addListener(listener));
}
}
@Override
public void removeListener(LoggerEventListener listener) {
if (listeners.remove(listener)) {
forEachLogger(log -> log.removeListener(listener));
}
}
@Override
public Stream<Map.Entry<String, Logger>> loggers() {
return loggers.entrySet().stream();
}
@Override
public Stream<LoggerEventListener> listeners() {
return listeners.stream();
}
@Override
public void setLevel(String path, Level level) {
requireNonNulls(path, level);
loggers().filter(e -> e.getKey().startsWith(path))
.map(Map.Entry::getValue)
.forEach((Logger l) -> l.setLevel(level)
);
}
@Override
public void setLevel(Class<?> binding, Level level) {
requireNonNulls(binding, level);
setLevel(makeNameFrom(binding), level);
}
private Logger prepare(Class<?> clazz, org.slf4j.Logger inner) {
// clazz is nullable
final Logger log = new Slf4jLogger(inner, formatter, level);
listeners.forEach(log::addListener);
loggers.put(clazz == null ? inner.getName() : makeNameFrom(clazz), log);
return log;
}
private void forEachLogger(Consumer<Logger> action) {
loggers().map(Map.Entry::getValue).forEach(action);
}
private String makeNameFrom(Class<?> binding) {
requireNonNull(binding);
final String[] tokens = binding.getName().split("\\.");
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < tokens.length; i++) {
if (i == tokens.length - 1) {
sb.append(tokens[i]);
} else {
sb.append(tokens[i].charAt(0)).append('.');
}
}
return sb.toString();
}
private Logger acquireLogger(String binding) {
return loggers.computeIfAbsent(binding, this::create);
}
}
package com.speedment.extra.slf4j;
import com.speedment.common.logger.Level;
import com.speedment.common.logger.LoggerFormatter;
/**
* @author Emil Forslund
* @since 1.0.0
*/
public final class Slf4jLoggerFormatter implements LoggerFormatter {
@Override
public String apply(Level level, String name, String message) {
return message;
}
}
package com.yourcompany;
import com.speedment.common.logger.LoggerManager;
import com.speedment.extra.slf4j.Slf4jLoggerFactory;
import org.springframework.context.annotation.Configuration;
/**
* @author Emil Forslund
* @since 1.0.0
*/
@Configuration
public class SpeedmentToSlf4jAdapter {
static {
LoggerManager.setFactory(new Slf4jLoggerFactory());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment