diff --git a/doc/src/main/resources/docs/CHANGES.txt b/doc/src/main/resources/docs/CHANGES.txt
index 23a784e7..e3e8b28f 100644
--- a/doc/src/main/resources/docs/CHANGES.txt
+++ b/doc/src/main/resources/docs/CHANGES.txt
@@ -16,6 +16,7 @@ The following bugs have been fixed in the 2.0.3 release.
107: java.io.UnsupportedEncodingException: en_US.iso885915 if charset is "en_US.iso885915"
110: WildFly support for MailHandler
116: MailHandler LogManger support for mail entries
+123: MailHandler should catch ServiceConfigurationError
CHANGES IN THE 2.0.2 RELEASE
----------------------------
diff --git a/mailhandler/src/main/java/org/eclipse/angus/mail/util/logging/MailHandler.java b/mailhandler/src/main/java/org/eclipse/angus/mail/util/logging/MailHandler.java
index aeced388..915e137c 100644
--- a/mailhandler/src/main/java/org/eclipse/angus/mail/util/logging/MailHandler.java
+++ b/mailhandler/src/main/java/org/eclipse/angus/mail/util/logging/MailHandler.java
@@ -68,6 +68,7 @@
import java.util.Objects;
import java.util.Properties;
import java.util.ResourceBundle;
+import java.util.ServiceConfigurationError;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
@@ -408,6 +409,10 @@ public class MailHandler extends Handler {
* Min byte size for header data. Used for initial arrays sizing.
*/
private static final int MIN_HEADER_SIZE = 1024;
+ /**
+ * Default capacity for the log record storage.
+ */
+ private static final int DEFAULT_CAPACITY = 1000;
/**
* Cache the off value.
*/
@@ -444,11 +449,16 @@ public class MailHandler extends Handler {
* This must be less than the PUBLISH state.
*/
private static final Integer MUTEX_REPORT = -4;
+ /**
+ * The used for service configuration error reporting.
+ * This must be less than the REPORT state and less than linkage.
+ */
+ private static final Integer MUTEX_SERVICE = -8;
/**
* The used for linkage error reporting.
* This must be less than the REPORT state.
*/
- private static final Integer MUTEX_LINKAGE = -8;
+ private static final Integer MUTEX_LINKAGE = -16;
/**
* Used to turn off security checks.
*/
@@ -461,7 +471,7 @@ public class MailHandler extends Handler {
/**
* Holds all of the email server properties.
*/
- private Properties mailProps;
+ private Properties mailProps = new Properties();
/**
* Holds the authenticator required to login to the email server.
*/
@@ -491,6 +501,7 @@ public class MailHandler extends Handler {
* The maximum number of log records to format per email.
* Used to roughly bound the size of an email.
* Every time the capacity is reached, the handler will push.
+ * Capacity is zero while the handler is being constructed.
* The capacity will be negative if this handler is closed.
* Negative values are used to ensure all records are pushed.
*/
@@ -517,7 +528,7 @@ public class MailHandler extends Handler {
* This is only required if an email must be sent prior to shutdown
* or before the buffer is full.
*/
- private Level pushLevel;
+ private Level pushLevel = Level.OFF;
/**
* Holds the push filter for trigger conditions requiring an early push.
* Only gets called if the given log record is greater than or equal
@@ -530,10 +541,11 @@ public class MailHandler extends Handler {
*/
private volatile Filter filter;
/**
- * Holds the level for this handler.
+ * Holds the level for this handler. Default value must be OFF.
* There is no way to un-seal the super handler.
+ * @see #init(java.util.Properties)
*/
- private volatile Level logLevel = Level.ALL;
+ private volatile Level logLevel = Level.OFF;
/**
* Holds the filters for each attachment. Filters are optional for
* each attachment. This is declared volatile because this is treated as
@@ -541,14 +553,14 @@ public class MailHandler extends Handler {
* positive.
*/
@SuppressWarnings("VolatileArrayField")
- private volatile Filter[] attachmentFilters;
+ private volatile Filter[] attachmentFilters = emptyFilterArray();
/**
* Holds the encoding name for this handler.
* There is no way to un-seal the super handler.
*/
private String encoding;
/**
- * Holds the entry and body filter for this handler.
+ * Holds the body formatter for this handler.
* There is no way to un-seal the super handler.
*/
private Formatter formatter;
@@ -558,14 +570,14 @@ public class MailHandler extends Handler {
* getHead, format, and getTail methods are only called if one or more
* log records pass through the attachment filters.
*/
- private Formatter[] attachmentFormatters;
+ private Formatter[] attachmentFormatters = emptyFormatterArray();
/**
* Holds the formatters that create the file name for each attachment.
* Each formatter must produce a non null and non empty name.
* The final file name will be the concatenation of one getHead call, plus
* all of the format calls, plus one getTail call.
*/
- private Formatter[] attachmentNames;
+ private Formatter[] attachmentNames = emptyFormatterArray();
/**
* Used to override the content type for the body and set the content type
* for each attachment.
@@ -575,6 +587,7 @@ public class MailHandler extends Handler {
* Holds the error manager for this handler.
* There is no way to un-seal the super handler.
*/
+ @SuppressWarnings("this-escape")
private volatile ErrorManager errorManager = defaultErrorManager();
/**
@@ -584,6 +597,7 @@ public class MailHandler extends Handler {
* @throws SecurityException if a security manager exists and the
* caller does not have LoggingPermission("control").
*/
+ @SuppressWarnings("this-escape")
public MailHandler() {
init((Properties) null);
}
@@ -593,10 +607,12 @@ public MailHandler() {
* LogManager configuration properties but overrides the
* LogManager capacity with the given capacity.
*
- * @param capacity of the internal buffer.
+ * @param capacity of the internal buffer. If less than one the default of
+ * 1000 is used.
* @throws SecurityException if a security manager exists and the
* caller does not have LoggingPermission("control").
*/
+ @SuppressWarnings("this-escape")
public MailHandler(final int capacity) {
init((Properties) null);
setCapacity0(capacity);
@@ -613,6 +629,7 @@ public MailHandler(final int capacity) {
* @throws SecurityException if a security manager exists and the
* caller does not have LoggingPermission("control").
*/
+ @SuppressWarnings("this-escape")
public MailHandler(Properties props) {
init(props); //Must pass null or original object
}
@@ -632,6 +649,9 @@ public MailHandler(Properties props) {
*/
@Override
public boolean isLoggable(final LogRecord record) {
+ //For a this-escape, level will be off.
+ //Unless subclass is known, the filter will be null
+ //and the attachment filters will be an empty array.
if (record == null) { //JDK-8233979
return false;
}
@@ -685,6 +705,8 @@ public void publish(final LogRecord record) {
}
} catch (final LinkageError JDK8152515) {
reportLinkageError(JDK8152515, ErrorManager.WRITE_FAILURE);
+ } catch (final ServiceConfigurationError sce) {
+ reportConfigurationError(sce, ErrorManager.WRITE_FAILURE);
} finally {
releaseMutex();
}
@@ -703,6 +725,9 @@ private void publish0(final LogRecord record) {
Message msg;
boolean priority;
synchronized (this) {
+ //No need to check for sealed as long as the init method ensures
+ //that data.length and capacity are both zero until end of init().
+ //size is always zero on construction.
if (size == data.length && size < capacity) {
grow();
}
@@ -911,8 +936,8 @@ public void flush() {
*/
@Override
public void close() {
+ checkAccess();
try {
- checkAccess(); //Ensure setLevel works before clearing the buffer.
Message msg = null;
synchronized (this) {
try {
@@ -930,10 +955,10 @@ public void close() {
this.capacity = -this.capacity;
}
+ //Only need room for one record after closed
//Ensure not inside a push.
if (size == 0 && data.length != 1) {
- this.data = new LogRecord[1];
- this.matched = new int[this.data.length];
+ initLogRecords(1);
}
}
}
@@ -943,6 +968,8 @@ public void close() {
}
} catch (final LinkageError JDK8152515) {
reportLinkageError(JDK8152515, ErrorManager.CLOSE_FAILURE);
+ } catch (final ServiceConfigurationError sce) {
+ reportConfigurationError(sce, ErrorManager.CLOSE_FAILURE);
}
}
@@ -954,8 +981,10 @@ public void close() {
* @see #setLevel(java.util.logging.Level)
* @since Angus Mail 2.0.3
*/
- public boolean isEnabled() {
- return this.logLevel.intValue() != offValue; //Volatile read
+ public synchronized boolean isEnabled() {
+ //For a this-escape, capacity will be zero and level will be off.
+ //No need to check that construction completed.
+ return capacity > 0 && this.logLevel.intValue() != offValue;
}
/**
@@ -971,30 +1000,19 @@ public boolean isEnabled() {
* @see #isEnabled()
* @since Angus Mail 2.0.3
*/
- public void setEnabled(final boolean enabled) {
+ public synchronized void setEnabled(final boolean enabled) {
checkAccess();
- setEnabled0(enabled);
- }
-
- /**
- * Used to enable or disable this handler.
- *
- * Pushes any buffered records to the email server as normal priority.
- * The internal buffer is then cleared.
- *
- * @param enabled true to enable and false to disable.
- * @since Angus Mail 2.0.3
- */
- private synchronized void setEnabled0(final boolean enabled) {
if (this.capacity > 0) { //handler is open
- this.push(false, ErrorManager.FLUSH_FAILURE);
+ if (this.size != 0) {
+ push(false, ErrorManager.FLUSH_FAILURE);
+ }
if (enabled) {
- if (disabledLevel != null) { //was disabled
+ if (this.disabledLevel != null) { //was disabled
this.logLevel = this.disabledLevel;
this.disabledLevel = null;
}
} else {
- if (disabledLevel == null) {
+ if (this.disabledLevel == null) {
this.disabledLevel = this.logLevel;
this.logLevel = Level.OFF;
}
@@ -1041,6 +1059,8 @@ public void setLevel(final Level newLevel) {
*/
@Override
public Level getLevel() {
+ //For a this-escape, this value will be OFF.
+ //No need to check that construction completed.
return logLevel; //Volatile access.
}
@@ -1134,6 +1154,8 @@ public void setFilter(final Filter newFilter) {
*/
@Override
public synchronized String getEncoding() {
+ //For a this-escape, this value will be null.
+ //No need to check that construction completed.
return this.encoding;
}
@@ -1304,8 +1326,8 @@ public final synchronized void setComparator(Comparator super LogRecord> c) {
* @return the capacity.
*/
public final synchronized int getCapacity() {
- assert capacity != Integer.MIN_VALUE && capacity != 0 : capacity;
- return Math.abs(capacity);
+ assert capacity != Integer.MIN_VALUE : capacity;
+ return capacity != 0 ? Math.abs(capacity) : DEFAULT_CAPACITY;
}
/**
@@ -1314,15 +1336,16 @@ public final synchronized int getCapacity() {
* Pushes any buffered records to the email server as normal priority.
* The internal buffer is then cleared.
*
- * @param newCapacity the max number of records.
+ * @param newCapacity the max number of records. The default capacity of
+ * 1000 is used if the given capacity is less than one.
* @throws SecurityException if a security manager exists and the caller
* does not have LoggingPermission("control").
- * @throws IllegalArgumentException is the new capacity is less than one.
* @throws IllegalStateException if called from inside a push.
* @see #flush()
* @since Angus Mail 2.0.3
*/
public final synchronized void setCapacity(int newCapacity) {
+ checkAccess();
setCapacity0(newCapacity);
}
@@ -1429,6 +1452,7 @@ private void setAuthenticator0(final Authenticator auth) {
* @throws IllegalStateException if called from inside a push.
*/
public final void setMailProperties(Properties props) {
+ checkAccess();
if (props == null) {
final String p = getClass().getName();
props = parseProperties(
@@ -1450,13 +1474,18 @@ public final void setMailProperties(Properties props) {
* @since Angus Mail 2.0.3
*/
private Properties copyOf(Properties props) {
- Properties copy = (Properties) props.clone(); //Allow subclass
- return Objects.requireNonNull(copy); //Broken subclass
+ //Allow subclasses however, check that clone contract was followed in
+ //that a non-null Properties object was returned.
+ //No need to perform reflexive test or exact class matching tests as
+ //that doesn't really cause any unexpected failures.
+ Properties copy = (Properties) props.clone();
+ return Objects.requireNonNull(copy,
+ props.getClass().getName());
}
/**
- * A private hook to handle overrides when the public method is declared
- * non final. See public method for details.
+ * A private hook to set and validate properties.
+ * See public method for details.
*
* @param props a safe properties object.
* @return true if verification key was present.
@@ -1464,7 +1493,6 @@ private Properties copyOf(Properties props) {
*/
private boolean setMailProperties0(Properties props) {
Objects.requireNonNull(props);
- checkAccess();
Session settings;
synchronized (this) {
if (isWriting) {
@@ -1531,6 +1559,7 @@ public final Properties getMailProperties() {
* @since Angus Mail 2.0.3
*/
public final void setMailEntries(String entries) {
+ checkAccess();
if (entries == null) {
final String p = getClass().getName();
entries = fromLogManager(p.concat(".mailEntries"));
@@ -1930,6 +1959,8 @@ public synchronized final void setSubjectFormatter(final Formatter format) {
*/
@Override
protected void reportError(String msg, Exception ex, int code) {
+ //This method is not protected from a this-escape.
+ //The error manager will be non-null in any case.
try {
if (msg != null) {
errorManager.error(Level.SEVERE.getName()
@@ -1937,19 +1968,31 @@ protected void reportError(String msg, Exception ex, int code) {
} else {
errorManager.error((String) null, ex, code);
}
- } catch (RuntimeException | LinkageError GLASSFISH_21258) {
+ } catch (final RuntimeException | LinkageError GLASSFISH_21258) {
+ if (ex != null && GLASSFISH_21258 != ex) {
+ GLASSFISH_21258.addSuppressed(ex);
+ }
reportLinkageError(GLASSFISH_21258, code);
+ } catch (final ServiceConfigurationError sce) {
+ if (ex != null) {
+ sce.addSuppressed(ex);
+ }
+ reportConfigurationError(sce, code);
}
}
/**
* Checks logging permissions if this handler has been sealed.
+ * Otherwise, this will check that this object was fully constructed.
+ *
* @throws SecurityException if a security manager exists and the caller
* does not have {@code LoggingPermission("control")}.
*/
private void checkAccess() {
- if (sealed) {
+ if (this.sealed) {
LogManagerProperties.checkLogManagerAccess();
+ } else {
+ throw new SecurityException("this-escape");
}
}
@@ -1976,7 +2019,8 @@ final String contentTypeOf(CharSequence chunk) {
assert in.markSupported() : in.getClass().getName();
return URLConnection.guessContentTypeFromStream(in);
} catch (final IOException IOE) {
- reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE);
+ reportError("Unable to guess content type",
+ IOE, ErrorManager.FORMAT_FAILURE);
}
}
return null; //text/plain
@@ -2095,8 +2139,46 @@ private void reportError(Message msg, Exception ex, int code) {
} catch (final Exception e) {
reportError(toMsgString(e), ex, code);
}
- } catch (final LinkageError GLASSFISH_21258) {
+ } catch (LinkageError GLASSFISH_21258) {
+ if (ex != null) {
+ GLASSFISH_21258.addSuppressed(ex);
+ }
reportLinkageError(GLASSFISH_21258, code);
+ } catch (ServiceConfigurationError sce) {
+ if (ex != null) {
+ sce.addSuppressed(ex);
+ }
+ reportConfigurationError(sce, code);
+ }
+ }
+
+ /**
+ * Report a ServiceConfigurationError to the error manager.
+ *
+ * @param t the service configuration error.
+ * @param code the error manager reason code.
+ * @since Angus Mail 2.0.3
+ */
+ private void reportConfigurationError(Throwable t, int code) {
+ final Integer idx = MUTEX.get();
+ if (idx == null || idx > MUTEX_SERVICE) {
+ MUTEX.set(MUTEX_SERVICE);
+ try {
+ reportError("Unable to load dependencies",
+ new IllegalStateException(t), code);
+ } catch (RuntimeException | ServiceConfigurationError
+ | LinkageError e) {
+ if (t != null && e != t) {
+ e.addSuppressed(t);
+ }
+ reportLinkageError(e, code);
+ } finally {
+ if (idx != null) {
+ MUTEX.set(idx);
+ } else {
+ MUTEX.remove();
+ }
+ }
}
}
@@ -2109,21 +2191,23 @@ private void reportError(Message msg, Exception ex, int code) {
*
* @param le the linkage error or a RuntimeException.
* @param code the ErrorManager code.
- * @throws NullPointerException if error is null.
* @since JavaMail 1.5.3
*/
private void reportLinkageError(final Throwable le, final int code) {
- if (le == null) {
- throw new NullPointerException(String.valueOf(code));
- }
-
+ assert le != null : code;
final Integer idx = MUTEX.get();
if (idx == null || idx > MUTEX_LINKAGE) {
MUTEX.set(MUTEX_LINKAGE);
try {
+ //Per the API docs this is not how uncaught exception handler
+ //should be used. However, the throwable that we are receiving
+ //here is happening only when the JVM is shutting down.
+ //This will only execute on unpatched systems.
+ //See tickets listed in API docs above.
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), le);
- } catch (RuntimeException | LinkageError ignore) {
+ } catch (RuntimeException | ServiceConfigurationError
+ | LinkageError ignore) {
} finally {
if (idx != null) {
MUTEX.set(idx);
@@ -2217,29 +2301,30 @@ private String contentWithEncoding(String type, String encoding) {
/**
* Sets the capacity for this handler.
*
- * @param newCapacity the max number of records.
- * @throws SecurityException if a security manager exists and the
- * caller does not have LoggingPermission("control").
+ * @param newCapacity the max number of records. Default capacity is used if
+ * the value is negative.
* @throws IllegalStateException if called from inside a push.
*/
private synchronized void setCapacity0(int newCapacity) {
- checkAccess();
- if (newCapacity <= 0) {
- newCapacity = 1000;
- }
-
if (isWriting) {
throw new IllegalStateException();
}
+ if (!this.sealed || this.capacity == 0) {
+ return;
+ }
+
+ if (newCapacity <= 0) {
+ newCapacity = DEFAULT_CAPACITY;
+ }
+
if (this.capacity < 0) { //If closed, remain closed.
this.capacity = -newCapacity;
} else {
- this.push(false, ErrorManager.FLUSH_FAILURE);
+ push(false, ErrorManager.FLUSH_FAILURE);
this.capacity = newCapacity;
- if (this.data != null && this.data.length > newCapacity) {
- this.data = Arrays.copyOf(data, newCapacity, LogRecord[].class);
- this.matched = Arrays.copyOf(matched, newCapacity);
+ if (this.data.length > newCapacity) {
+ initLogRecords(1);
}
}
}
@@ -2369,9 +2454,9 @@ private boolean alignAttachmentFilters(int expect) {
private void reset() {
assert Thread.holdsLock(this);
if (size < data.length) {
- Arrays.fill(data, 0, size, null);
+ Arrays.fill(data, 0, size, (LogRecord) null);
} else {
- Arrays.fill(data, null);
+ Arrays.fill(data, (LogRecord) null);
}
this.size = 0;
}
@@ -2387,8 +2472,11 @@ private void grow() {
newCapacity = capacity;
}
assert len != capacity : len;
- this.data = Arrays.copyOf(data, newCapacity, LogRecord[].class);
- this.matched = Arrays.copyOf(matched, newCapacity);
+ final LogRecord[] d = Arrays.copyOf(data, newCapacity, LogRecord[].class);
+ final int[] m = Arrays.copyOf(matched, newCapacity);
+ //Ensure both arrays are created before assigning.
+ this.data = d;
+ this.matched = m;
}
/**
@@ -2402,21 +2490,20 @@ private void grow() {
* @see #sealed
*/
private synchronized void init(final Properties props) {
- assert this.errorManager != null;
- final String p = getClass().getName();
- this.mailProps = new Properties(); //ensure non-null on exception
- final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER);
- try {
- this.contentTypes = FileTypeMap.getDefaultFileTypeMap();
- } finally {
- getAndSetContextClassLoader(ccl);
- }
+ //Ensure non-null even on exception.
+ //Zero value allows publish to not check for this-escape.
+ initLogRecords(0);
+ LogManagerProperties.checkLogManagerAccess();
+ final String p = getClass().getName();
//Assign any custom error manager first so it can detect all failures.
+ assert this.errorManager != null; //default set before custom object
initErrorManager(fromLogManager(p.concat(".errorManager")));
- initCapacity(fromLogManager(p.concat(".capacity")));
- initLevel(fromLogManager(p.concat(".level")));
- initEnabled(fromLogManager(p.concat(".enabled")));
+ int cap = parseCapacity(fromLogManager(p.concat(".capacity")));
+ Level lvl = parseLevel(fromLogManager(p.concat(".level")));
+ boolean enabled = parseEnabled(fromLogManager(p.concat(".enabled")));
+ initContentTypes();
+
initFilter(fromLogManager(p.concat(".filter")));
this.auth = newAuthenticator(fromLogManager(p.concat(".authenticator")));
@@ -2433,11 +2520,11 @@ private synchronized void init(final Properties props) {
initAttachmentFilters(fromLogManager(p.concat(".attachment.filters")));
initAttachmentNames(fromLogManager(p.concat(".attachment.names")));
- //Verification of all of the MailHandler properties starts here
- //That means setting new object members goes above this comment.
//Entries are always parsed to report any errors.
Properties entries = parseProperties(fromLogManager(p.concat(".mailEntries")));
- sealed = true;
+
+ //Any new handler object members should be set above this line
+ String verify = fromLogManager(p.concat(".verify"));
boolean verified;
if (props != null) {
//Given properties do not fallback to log manager.
@@ -2447,14 +2534,29 @@ private synchronized void init(final Properties props) {
//.mailEntries should fallback to log manager when verify key not present.
verified = setMailProperties0(entries);
} else {
- checkAccess();
verified = false;
}
- if (!verified && fromLogManager(p.concat(".verify")) != null) {
- verifySettings(initSession());
+ //Fallback to top level verify properties if needed.
+ if (!verified && verify != null) {
+ try {
+ verifySettings(initSession());
+ } catch (final RuntimeException re) {
+ reportError("Unable to verify", re, ErrorManager.OPEN_FAILURE);
+ } catch (final ServiceConfigurationError sce) {
+ reportConfigurationError(sce, ErrorManager.OPEN_FAILURE);
+ }
}
intern(); //Show verify warnings first.
+
+ //Mark the handler as fully constructed by setting these fields.
+ this.capacity = cap;
+ if (enabled) {
+ this.logLevel = lvl;
+ } else {
+ this.disabledLevel = lvl;
+ }
+ sealed = true;
}
/**
@@ -2471,26 +2573,18 @@ private void intern() {
Object canidate;
Object result;
final Map