diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 4c50bd365..7932af228 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021 Contributors to the Eclipse Foundation +# Copyright (c) 2021, 2022 Contributors to the Eclipse Foundation # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0 which is available at @@ -42,4 +42,4 @@ jobs: - name: Verify run: | cd api - mvn -B -U -C -V clean verify org.glassfish.copyright:glassfish-copyright-maven-plugin:check -Poss-release,staging -Dgpg.skip=true + mvn -B -U -C -V clean verify org.glassfish.copyright:glassfish-copyright-maven-plugin:check -Poss-release,staging -Dcopyright.ignoreyear=true -Dgpg.skip=true diff --git a/Jenkinsfile b/Jenkinsfile index 4dff4489d..bb36f83dd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,7 @@ spec: - "james.local" containers: - name: mail-ci - image: jakartaee/cts-javamail-base:0.1 + image: jakartaee/cts-javamail-base:0.3 command: - cat tty: true @@ -49,11 +49,17 @@ spec: } parameters { string(name: 'JAF_BUNDLE_URL', - defaultValue: 'https://repo1.maven.org/maven2/com/sun/activation/jakarta.activation/2.0.0-RC3/jakarta.activation-2.0.0-RC3.jar', + defaultValue: 'https://repo1.maven.org/maven2/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar', + description: 'URL required for downloading JAF API jar' ) + string(name: 'ANGUS_JAF_BUNDLE_URL', + defaultValue: 'https://repo1.maven.org/maven2/org/eclipse/angus/angus-activation/1.0.0/angus-activation-1.0.0.jar', description: 'URL required for downloading JAF implementation jar' ) string(name: 'MAIL_TCK_BUNDLE_URL', defaultValue: 'https://ci.eclipse.org/mail/job/mail-tck/job/2.0.0/lastSuccessfulBuild/artifact/bundles/mail-tck-2.0.0-rc1.zip', description: 'URL required for downloading Jakarta Mail TCK zip' ) + string(name: 'ANGUS_MAIL_BUNDLE_URL', + defaultValue: 'https://repo1.maven.org/maven2/org/eclipse/angus/angus-mail/1.0.0/angus-mail-1.0.0.jar', + description: 'URL required for downloading Angus Mail jar' ) } environment { ANT_OPTS = "-Djavax.xml.accessExternalStylesheet=all -Djavax.xml.accessExternalSchema=all -Djavax.xml.accessExternalDTD=file,http" @@ -71,8 +77,8 @@ spec: cd .. bash -x ${WORKSPACE}/docker/build_jakartamail.sh """ - archiveArtifacts artifacts: 'mail/target/*.jar' - stash includes: 'mail/target/*.jar', name: 'mail-bundles' + archiveArtifacts artifacts: '**/target/*.jar' + stash includes: '**/target/*.jar', name: 'mail-bundles' } } } diff --git a/api/pom.xml b/api/pom.xml index 2a5493e0a..0da151288 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -30,7 +30,7 @@ 4.0.0 jakarta.mail jakarta.mail-api - 2.1.0-SNAPSHOT + 2.1.1-SNAPSHOT Jakarta Mail API ${project.name} ${spec.version} Specification API @@ -78,7 +78,7 @@ Low ${project.basedir}/../spotbugs-exclude.xml - 4.3.0 + 4.4.2.2 @@ -86,7 +86,12 @@ jakarta.activation jakarta.activation-api - 2.1.0-RC1 + 2.1.0 + + + org.eclipse.angus + angus-activation + 1.0.0-M2 junit @@ -106,6 +111,11 @@ junit test + + org.eclipse.angus + angus-activation + test + @@ -143,7 +153,6 @@ ${spotbugs.skip} ${spotbugs.threshold} - true ${spotbugs.exclude} true @@ -176,7 +185,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.3.0 + 3.3.1 org.apache.maven.plugins @@ -350,6 +359,10 @@ ${project.name} ${project.organization.name} ${buildNumber} + + !org.glassfish.hk2.osgiresourcelocator, + * + @@ -379,6 +392,7 @@ maven-javadoc-plugin 11 + true false true true @@ -404,6 +418,7 @@ Use is subject to 2 false + false false diff --git a/api/src/main/java/jakarta/mail/BodyPart.java b/api/src/main/java/jakarta/mail/BodyPart.java index a9eec9c18..ce83846a6 100644 --- a/api/src/main/java/jakarta/mail/BodyPart.java +++ b/api/src/main/java/jakarta/mail/BodyPart.java @@ -16,6 +16,8 @@ package jakarta.mail; +import jakarta.mail.util.StreamProvider; + /** * This class models a Part that is contained within a Multipart. * This is an abstract class. Subclasses provide actual implementations.

@@ -36,6 +38,13 @@ public abstract class BodyPart implements Part { */ protected Multipart parent; + /** + * Instance of stream provider. + * + * @since JavaMail 2.1 + */ + protected final StreamProvider streamProvider = StreamProvider.provider(); + /** * Creates a default {@code BodyPart}. */ diff --git a/api/src/main/java/jakarta/mail/Multipart.java b/api/src/main/java/jakarta/mail/Multipart.java index 8ba962c8d..1ca028c63 100644 --- a/api/src/main/java/jakarta/mail/Multipart.java +++ b/api/src/main/java/jakarta/mail/Multipart.java @@ -21,6 +21,7 @@ import java.io.OutputStream; import java.io.IOException; import jakarta.activation.DataSource; +import jakarta.mail.util.StreamProvider; /** * Multipart is a container that holds multiple body parts. Multipart @@ -60,6 +61,13 @@ public abstract class Multipart { */ protected Part parent; + /** + * Instance of stream provider. + * + * @since JavaMail 2.1 + */ + protected final StreamProvider streamProvider = StreamProvider.provider(); + /** * Default constructor. An empty Multipart object is created. */ diff --git a/api/src/main/java/jakarta/mail/Session.java b/api/src/main/java/jakarta/mail/Session.java index 3382dd237..5fe4d54b5 100644 --- a/api/src/main/java/jakarta/mail/Session.java +++ b/api/src/main/java/jakarta/mail/Session.java @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -203,8 +202,8 @@ public final class Session { // Support legacy @DefaultProvider private static final String DEFAULT_PROVIDER = "com.sun.mail.util.DefaultProvider"; - public static final StreamProvider STREAM_PROVIDER; + private final StreamProvider streamProvider; private final Properties props; private final Authenticator authenticator; private final Hashtable authTable @@ -246,13 +245,13 @@ public String run() { // ignore any exceptions } confDir = dir; - STREAM_PROVIDER = getStreamProvider(); } // Constructor is not public private Session(Properties props, Authenticator authenticator) { this.props = props; this.authenticator = authenticator; + this.streamProvider = StreamProvider.provider(); if (Boolean.valueOf(props.getProperty("mail.debug")).booleanValue()) debug = true; @@ -272,14 +271,15 @@ private Session(Properties props, Authenticator authenticator) { q = new EventQueue((Executor)props.get("mail.event.executor")); } - private static StreamProvider getStreamProvider() { - ServiceLoader sl = ServiceLoader.load(StreamProvider.class); - Iterator iter = sl.iterator(); - if (iter.hasNext()) { - return iter.next(); - } else { - throw new IllegalStateException("Not provider of " + StreamProvider.class.getName() + " was found"); - } + /** + * Get the stream provider instance of the session. + * + * @return the stream provider + * + * @since JavaMail 2.1 + */ + public StreamProvider getStreamProvider() { + return streamProvider; } private final synchronized void initLogger() { @@ -1036,7 +1036,7 @@ public void load(InputStream is) throws IOException { private void loadProvidersFromStream(InputStream is) throws IOException { if (is != null) { - LineInputStream lis = Session.STREAM_PROVIDER.inputLineStream(is, false); + LineInputStream lis = streamProvider.inputLineStream(is, false); String currLine; // load and process one line at a time using LineInputStream diff --git a/api/src/main/java/jakarta/mail/internet/InternetHeaders.java b/api/src/main/java/jakarta/mail/internet/InternetHeaders.java index 32e4f6b72..fb5b94840 100644 --- a/api/src/main/java/jakarta/mail/internet/InternetHeaders.java +++ b/api/src/main/java/jakarta/mail/internet/InternetHeaders.java @@ -16,22 +16,19 @@ package jakarta.mail.internet; -import jakarta.mail.Header; -import jakarta.mail.MessagingException; -import jakarta.mail.Session; -import jakarta.mail.util.LineInputStream; -import jakarta.mail.util.StreamProvider; - import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; +import jakarta.mail.Header; +import jakarta.mail.MessagingException; +import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.StreamProvider; + /** @@ -407,7 +404,7 @@ public void load(InputStream is, boolean allowutf8) // Read header lines until a blank line. It is valid // to have BodyParts with no header lines. String line; - LineInputStream lis = Session.STREAM_PROVIDER.inputLineStream(is, allowutf8); + LineInputStream lis = StreamProvider.provider().inputLineStream(is, allowutf8); String prevline = null; // the previous header line, as a string // a buffer to accumulate the header in, when we know it's needed StringBuilder lineBuffer = new StringBuilder(); diff --git a/api/src/main/java/jakarta/mail/internet/MimeBodyPart.java b/api/src/main/java/jakarta/mail/internet/MimeBodyPart.java index 8a4a955ff..567126129 100644 --- a/api/src/main/java/jakarta/mail/internet/MimeBodyPart.java +++ b/api/src/main/java/jakarta/mail/internet/MimeBodyPart.java @@ -1693,7 +1693,7 @@ static void writeTo(MimePart part, OutputStream os, String[] ignoreList, boolean } else { Map params = new HashMap<>(); params.put("allowutf8", allowutf8); - los = Session.STREAM_PROVIDER.outputLineStream(os, allowutf8); + los = StreamProvider.provider().outputLineStream(os, allowutf8); } // First, write out the header diff --git a/api/src/main/java/jakarta/mail/internet/MimeMessage.java b/api/src/main/java/jakarta/mail/internet/MimeMessage.java index 22bc03888..93842bb7f 100644 --- a/api/src/main/java/jakarta/mail/internet/MimeMessage.java +++ b/api/src/main/java/jakarta/mail/internet/MimeMessage.java @@ -258,7 +258,7 @@ public MimeMessage(MimeMessage source) throws MessagingException { strict = source.strict; source.writeTo(bos); bos.close(); - InputStream bis = Session.STREAM_PROVIDER.inputSharedByteArray(bos.toByteArray()); + InputStream bis = session.getStreamProvider().inputSharedByteArray(bos.toByteArray()); parse(bis); bis.close(); saved = true; @@ -1425,7 +1425,7 @@ protected InputStream getContentStream() throws MessagingException { if (contentStream != null) return ((SharedInputStream)contentStream).newStream(0, -1); if (content != null) { - return Session.STREAM_PROVIDER.inputSharedByteArray(content); + return session.getStreamProvider().inputSharedByteArray(content); } throw new MessagingException("No MimeMessage content"); } @@ -1932,7 +1932,7 @@ public void writeTo(OutputStream os, String[] ignoreList) // Else, the content is untouched, so we can just output it // First, write out the header Enumeration hdrLines = getNonMatchingHeaderLines(ignoreList); - LineOutputStream los = Session.STREAM_PROVIDER.outputLineStream(os, allowutf8Headers); + LineOutputStream los = session.getStreamProvider().outputLineStream(os, allowutf8Headers); while (hdrLines.hasMoreElements()) los.writeln(hdrLines.nextElement()); diff --git a/api/src/main/java/jakarta/mail/internet/MimeMultipart.java b/api/src/main/java/jakarta/mail/internet/MimeMultipart.java index 48ae3e01a..a602983ca 100644 --- a/api/src/main/java/jakarta/mail/internet/MimeMultipart.java +++ b/api/src/main/java/jakarta/mail/internet/MimeMultipart.java @@ -16,6 +16,14 @@ package jakarta.mail.internet; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + import jakarta.activation.DataSource; import jakarta.mail.BodyPart; import jakarta.mail.IllegalWriteException; @@ -25,18 +33,8 @@ import jakarta.mail.Multipart; import jakarta.mail.MultipartDataSource; import jakarta.mail.Session; -import jakarta.mail.internet.MimeUtility; import jakarta.mail.util.LineInputStream; import jakarta.mail.util.LineOutputStream; -import jakarta.mail.util.StreamProvider; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; @@ -524,7 +522,7 @@ public synchronized void writeTo(OutputStream os) String boundary = "--" + (new ContentType(contentType)).getParameter("boundary"); - LineOutputStream los = Session.STREAM_PROVIDER.outputLineStream(os, false); + LineOutputStream los = streamProvider.outputLineStream(os, false); // if there's a preamble, write it out if (preamble != null) { byte[] pb = MimeUtility.getBytes(preamble); @@ -605,7 +603,7 @@ protected synchronized void parse() throws MessagingException { try { // Skip and save the preamble - LineInputStream lin = Session.STREAM_PROVIDER.inputLineStream(in, false); + LineInputStream lin = streamProvider.inputLineStream(in, false); StringBuilder preamblesb = null; String line; while ((line = lin.readLine()) != null) { diff --git a/api/src/main/java/jakarta/mail/internet/MimeUtility.java b/api/src/main/java/jakarta/mail/internet/MimeUtility.java index 890f465fd..43fae2019 100644 --- a/api/src/main/java/jakarta/mail/internet/MimeUtility.java +++ b/api/src/main/java/jakarta/mail/internet/MimeUtility.java @@ -22,7 +22,6 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; @@ -38,8 +37,8 @@ import jakarta.activation.DataSource; import jakarta.mail.EncodingAware; import jakarta.mail.MessagingException; -import jakarta.mail.Session; import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.StreamProvider; import jakarta.mail.util.StreamProvider.EncoderTypes; /** @@ -366,17 +365,17 @@ public static String getEncoding(DataHandler dh) { public static InputStream decode(InputStream is, String encoding) throws MessagingException { if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) - return Session.STREAM_PROVIDER.inputBase64(is); + return StreamProvider.provider().inputBase64(is); else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.inputQP(is); + return StreamProvider.provider().inputQP(is); else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) - return Session.STREAM_PROVIDER.inputUU(is); + return StreamProvider.provider().inputUU(is); else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.inputBinary(is); + return StreamProvider.provider().inputBinary(is); else { if (!ignoreUnknownEncoding) throw new MessagingException("Unknown encoding: " + encoding); @@ -401,17 +400,17 @@ public static OutputStream encode(OutputStream os, String encoding) if (encoding == null) return os; else if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) - return Session.STREAM_PROVIDER.outputBase64(os); + return StreamProvider.provider().outputBase64(os); else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.outputQP(os); + return StreamProvider.provider().outputQP(os); else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) - return Session.STREAM_PROVIDER.outputUU(os, null); + return StreamProvider.provider().outputUU(os, null); else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.outputBinary(os); + return StreamProvider.provider().outputBinary(os); else throw new MessagingException("Unknown encoding: " +encoding); } @@ -439,17 +438,17 @@ public static OutputStream encode(OutputStream os, String encoding, if (encoding == null) return os; else if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) - return Session.STREAM_PROVIDER.outputBase64(os); + return StreamProvider.provider().outputBase64(os); else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.outputQP(os); + return StreamProvider.provider().outputQP(os); else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) - return Session.STREAM_PROVIDER.outputUU(os, filename); + return StreamProvider.provider().outputUU(os, filename); else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) - return Session.STREAM_PROVIDER.outputBinary(os); + return StreamProvider.provider().outputBinary(os); else throw new MessagingException("Unknown encoding: " +encoding); } @@ -822,9 +821,9 @@ private static void doEncode(String string, boolean b64, ByteArrayOutputStream os = new ByteArrayOutputStream(); OutputStream eos; // the encoder if (b64) { // "B" encoding - eos = Session.STREAM_PROVIDER.outputB(os); + eos = StreamProvider.provider().outputB(os); } else { // "Q" encoding - eos = Session.STREAM_PROVIDER.outputQ(os, encodingWord); + eos = StreamProvider.provider().outputQ(os, encodingWord); } try { // do the encoding @@ -911,9 +910,9 @@ public static String decodeWord(String eword) // Get the appropriate decoder InputStream is; if (encoding.equalsIgnoreCase("B")) - is = Session.STREAM_PROVIDER.inputBase64(bis); + is = StreamProvider.provider().inputBase64(bis); else if (encoding.equalsIgnoreCase("Q")) - is = Session.STREAM_PROVIDER.inputQ(bis); + is = StreamProvider.provider().inputQ(bis); else throw new UnsupportedEncodingException( "unknown encoding: " + encoding); @@ -1369,7 +1368,7 @@ static String getDefaultMIMECharset() { if (is != null) { try { - LineInputStream lineInput = Session.STREAM_PROVIDER.inputLineStream(is, false); + LineInputStream lineInput = StreamProvider.provider().inputLineStream(is, false); // Load the JDK-to-MIME charset mapping table loadMappings(lineInput, java2mime); diff --git a/api/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java b/api/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java index da8d2703d..a6330453a 100644 --- a/api/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java +++ b/api/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java @@ -16,15 +16,13 @@ package jakarta.mail.internet; -import jakarta.mail.MessagingException; -import jakarta.mail.Session; -import jakarta.mail.util.LineOutputStream; -import jakarta.mail.util.StreamProvider; - import java.io.IOException; import java.io.OutputStream; import java.util.Enumeration; +import jakarta.mail.MessagingException; +import jakarta.mail.util.LineOutputStream; + /** * A MimeBodyPart that handles data that has already been encoded. * This class is useful when constructing a message and attaching @@ -80,7 +78,7 @@ public void writeTo(OutputStream os) if (os instanceof LineOutputStream) { los = (LineOutputStream) os; } else { - los = Session.STREAM_PROVIDER.outputLineStream(os, false); + los = streamProvider.outputLineStream(os, false); } // First, write out the header diff --git a/api/src/main/java/jakarta/mail/util/FactoryFinder.java b/api/src/main/java/jakarta/mail/util/FactoryFinder.java new file mode 100644 index 000000000..7b90facdb --- /dev/null +++ b/api/src/main/java/jakarta/mail/util/FactoryFinder.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package jakarta.mail.util; + +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Iterator; +import java.util.ServiceLoader; + +class FactoryFinder { + + /** + * Finds the implementation {@code Class} object for the given + * factory type. + * The arguments supplied must be used in order + * This method is package private so that this code can be shared. + * + * @return the {@code Class} object of the specified message factory + * + * @param factoryClass factory abstract class or interface to be found + * @exception RuntimeException if there is an error + */ + static T find(Class factoryClass) throws RuntimeException { + + String factoryId = factoryClass.getName(); + + // Use the system property first + String className = fromSystemProperty(factoryId); + if (className != null) { + T result = newInstance(className); + if (result != null) { + return result; + } + } + + // standard services: java.util.ServiceLoader + T factory = factoryFromServiceLoader(factoryClass); + if (factory != null) { + return factory; + } + + // handling Glassfish/OSGi (platform specific default) + if (isOsgi()) { + T result = lookupUsingOSGiServiceLoader(factoryId); + if (result != null) { + return result; + } + } + throw new IllegalStateException("Not provider of " + factoryClass.getName() + " was found"); + } + + @SuppressWarnings({"unchecked"}) + private static T newInstance(String className) throws RuntimeException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + checkPackageAccess(className); + Class clazz = null; + try { + if (classLoader == null) { + clazz = (Class) Class.forName(className); + } else { + clazz = (Class) classLoader.loadClass(className); + } + return clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot instance " + className, e); + } + } + + private static String fromSystemProperty(String factoryId) { + String systemProp = getSystemProperty(factoryId); + return systemProp; + } + + private static String getSystemProperty(final String property) { + String value = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public String run() { + return System.getProperty(property); + } + }); + return value; + } + + private static final String OSGI_SERVICE_LOADER_CLASS_NAME = "org.glassfish.hk2.osgiresourcelocator.ServiceLoader"; + + private static boolean isOsgi() { + try { + Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); + return true; + } catch (ClassNotFoundException ignored) { + } + return false; + } + + @SuppressWarnings({"unchecked"}) + private static T lookupUsingOSGiServiceLoader(String factoryId) { + try { + // Use reflection to avoid having any dependency on HK2 ServiceLoader class + Class serviceClass = Class.forName(factoryId); + Class[] args = new Class[]{serviceClass}; + Class target = Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); + Method m = target.getMethod("lookupProviderInstances", Class.class); + Iterator iter = ((Iterable) m.invoke(null, (Object[]) args)).iterator(); + return iter.hasNext() ? (T) iter.next() : null; + } catch (Exception ignored) { + // log and continue + return null; + } + } + + private static T factoryFromServiceLoader(Class factory) { + try { + ServiceLoader sl = ServiceLoader.load(factory); + Iterator iter = sl.iterator(); + if (iter.hasNext()) { + return iter.next(); + } else { + return null; + } + } catch (Throwable t) { + // For example, ServiceConfigurationError can be thrown if the factory class is not declared with 'uses' in module-info + throw new IllegalStateException("Cannot load " + factory + " as ServiceLoader", t); + } + } + + private static void checkPackageAccess(String className) { + // make sure that the current thread has an access to the package of the given name. + SecurityManager s = System.getSecurityManager(); + if (s != null) { + int i = className.lastIndexOf('.'); + if (i != -1) { + s.checkPackageAccess(className.substring(0, i)); + } + } + } +} + diff --git a/api/src/main/java/jakarta/mail/util/StreamProvider.java b/api/src/main/java/jakarta/mail/util/StreamProvider.java index 078cd8e85..274168a2f 100644 --- a/api/src/main/java/jakarta/mail/util/StreamProvider.java +++ b/api/src/main/java/jakarta/mail/util/StreamProvider.java @@ -18,6 +18,8 @@ import java.io.InputStream; import java.io.OutputStream; +import java.util.Iterator; +import java.util.ServiceLoader; /** * Service lookup is used to find implementations of this interface. @@ -25,11 +27,14 @@ * It contains the methods to instance different encoders/decoders and * other streams required by the API. * + * @since JavaMail 2.1 */ public interface StreamProvider { /** * Enumeration with the different encoder types supported by the Mail API. + * + * @since JavaMail 2.1 */ public static enum EncoderTypes { @@ -159,4 +164,16 @@ public String getEncoder() { * @return the encoder */ OutputStream outputUU(OutputStream out, String filename); + + /** + * Creates a stream provider object. The provider is loaded using the + * {@link ServiceLoader#load(Class)} method. If there are no available + * service providers, this method throws an IllegalStateException. + * Users are recommended to cache the result of this method. + * + * @return a stream provider + */ + public static StreamProvider provider() { + return FactoryFinder.find(StreamProvider.class); + } } diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index c46f191eb..8fdf526f1 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -27,4 +27,6 @@ uses jakarta.mail.Provider; uses jakarta.mail.util.StreamProvider; + //reflective call to java.beans.Beans.instantiate + requires static java.desktop; } diff --git a/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java b/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java new file mode 100644 index 000000000..0bdb117dc --- /dev/null +++ b/api/src/test/java/jakarta/mail/util/FactoryFinderTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package jakarta.mail.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.ServiceConfigurationError; + +import org.junit.Test; + +public class FactoryFinderTest { + + @Test + public void specifiedInSystemProperty() { + System.setProperty(Class1.class.getName(), Class2.class.getName()); + Class1 impl = FactoryFinder.find(Class1.class); + assertEquals(Class2.class, impl.getClass()); + } + + @Test + public void specifiedInServiceLoader() { + StreamProvider impl = FactoryFinder.find(StreamProvider.class); + assertEquals(DummyStreamProvider.class, impl.getClass()); + } + + @Test + public void doesNotExist() { + try { + FactoryFinder.find(Class2.class); + fail("IllegalStateException is expected"); + } catch (IllegalStateException e) { + assertNull(e.getCause()); + } + try { + FactoryFinder.find(Class3.class); + fail("IllegalStateException is expected"); + } catch (IllegalStateException e) { + assertEquals("Not provider of " + Class3.class.getName() + " was found", e.getMessage()); + } + } + + public static class Class1 {} + public static class Class2 extends Class1 {} + public static class Class3 {} +} diff --git a/api/src/test/java/module-info.java b/api/src/test/java/module-info.java index 610496d6e..e3ca4441a 100644 --- a/api/src/test/java/module-info.java +++ b/api/src/test/java/module-info.java @@ -20,6 +20,14 @@ requires transitive jakarta.activation; requires junit; + exports jakarta.mail; + exports jakarta.mail.event; + exports jakarta.mail.internet; + exports jakarta.mail.search; + exports jakarta.mail.util; + uses jakarta.mail.Provider; uses jakarta.mail.util.StreamProvider; -} + uses jakarta.mail.util.FactoryFinderTest.Class2; + provides jakarta.mail.util.StreamProvider with jakarta.mail.util.DummyStreamProvider; +} \ No newline at end of file diff --git a/docker/build_jakartamail.sh b/docker/build_jakartamail.sh index 41fcba601..4af8803d6 100755 --- a/docker/build_jakartamail.sh +++ b/docker/build_jakartamail.sh @@ -1,6 +1,6 @@ #!/bin/bash -xe # -# Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2022 Oracle and/or its affiliates. All rights reserved. # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0, which is available at @@ -15,4 +15,6 @@ # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # Build + +cd ${WORKSPACE}/api mvn -B -V -Pstaging clean install diff --git a/docker/run_jakartamailtck.sh b/docker/run_jakartamailtck.sh index 3c659f96a..28594b38c 100755 --- a/docker/run_jakartamailtck.sh +++ b/docker/run_jakartamailtck.sh @@ -1,6 +1,6 @@ #!/bin/bash -xe # -# Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2022 Oracle and/or its affiliates. All rights reserved. # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0, which is available at @@ -16,14 +16,23 @@ WGET_PROPS="-q --no-cache" if [ -z "$JAF_BUNDLE_URL" ];then - export JAF_BUNDLE_URL=https://repo1.maven.org/maven2/com/sun/activation/jakarta.activation/2.0.0-RC3/jakarta.activation-2.0.0-RC3.jar + export JAF_BUNDLE_URL=https://repo1.maven.org/maven2/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar +fi +if [ -z "$ANGUS_JAF_BUNDLE_URL" ];then + export ANGUS_JAF_BUNDLE_URL=https://repo1.maven.org/maven2/org/eclipse/angus/angus-activation/1.0.0/angus-activation-1.0.0.jar fi if [ -z "$MAIL_TCK_BUNDLE_URL" ];then - export MAIL_TCK_BUNDLE_URL=https://ci.eclipse.org/mail/job/mail-tck/job/2.0.0/lastSuccessfulBuild/artifact/bundles/mail-tck-2.0.0-rc1.zip + export MAIL_TCK_BUNDLE_URL=https://ci.eclipse.org/mail/job/mail-tck/job/master/lastSuccessfulBuild/artifact/bundles/jakarta-mail-tck-2.0.1.zip +fi +if [ -z "$ANGUS_MAIL_BUNDLE_URL" ];then + export ANGUS_MAIL_BUNDLE_URL=https://repo1.maven.org/maven2/org/eclipse/angus/angus-mail/1.0.0/angus-mail-1.0.0.jar fi -wget $WGET_PROPS $JAF_BUNDLE_URL -O jakarta.activation.jar -wget $WGET_PROPS $MAIL_TCK_BUNDLE_URL -O mailtck.zip -cp ${WORKSPACE}/mail/target/jakarta.mail.jar ${WORKSPACE} +wget $WGET_PROPS $JAF_BUNDLE_URL -O jakarta.activation-api.jar +wget $WGET_PROPS $ANGUS_JAF_BUNDLE_URL -O angus-activation.jar +# There is no latest mail-tck bundle in Jenkins yet. It will use the bundle of ${WORKSPACE}/mailtck.zip. +#wget $WGET_PROPS $MAIL_TCK_BUNDLE_URL -O mailtck.zip +wget $WGET_PROPS $ANGUS_MAIL_BUNDLE_URL -O angus-mail.jar +cp ${WORKSPACE}/api/target/jakarta.mail-api-*.jar ${WORKSPACE}/jakarta.mail-api.jar unzip -q -o ${WORKSPACE}/mailtck.zip -d ${WORKSPACE} @@ -42,10 +51,28 @@ sed -i "s#^SMTP_DOMAIN=.*#SMTP_DOMAIN=james.local#g" "$TS_HOME/lib/ts.jte" sed -i "s#^SMTP_FROM=.*#SMTP_FROM=user01@james.local#g" "$TS_HOME/lib/ts.jte" sed -i "s#^SMTP_TO=.*#SMTP_TO=user01@james.local#g" "$TS_HOME/lib/ts.jte" + +sed -i "s#^TS_HOME=.*#TS_HOME=$TS_HOME#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVA_HOME=.*#JAVA_HOME=$JAVA_HOME#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JARPATH=.*#JARPATH=$WORKSPACE#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_SERVER=.*#JAVAMAIL_SERVER=localhost -pn 1143#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_PROTOCOL=.*#JAVAMAIL_PROTOCOL=imap#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_TRANSPORT_PROTOCOL=.*#JAVAMAIL_TRANSPORT_PROTOCOL=smtp#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_TRANSPORT_SERVER=.*#JAVAMAIL_TRANSPORT_SERVER=localhost -tpn 1025#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_USERNAME=.*#JAVAMAIL_USERNAME=$MAIL_USER#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^JAVAMAIL_PASSWORD=.*#JAVAMAIL_PASSWORD=1234#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^SMTP_DOMAIN=.*#SMTP_DOMAIN=james.local#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^SMTP_FROM=.*#SMTP_FROM=user01@james.local#g" "$TS_HOME/lib/ts.pluggability.jte" +sed -i "s#^SMTP_TO=.*#SMTP_TO=user01@james.local#g" "$TS_HOME/lib/ts.pluggability.jte" + mkdir -p ${HOME}/.m2 cd $TS_HOME/tests/mailboxes -export CLASSPATH=$TS_HOME/tests/mailboxes:$WORKSPACE/jakarta.mail.jar:$WORKSPACE/jakarta.activation.jar:$CLASSPATH +export CLASSPATH=$TS_HOME/tests/mailboxes:$WORKSPACE/jakarta.mail-api.jar:$WORKSPACE/angus-mail.jar:$WORKSPACE/jakarta.activation-api.jar:$WORKSPACE/angus-activation.jar:$CLASSPATH + +ls -lrta $WORKSPACE +echo $CLASSPATH + javac -cp $CLASSPATH fpopulate.java java -cp $CLASSPATH fpopulate -s test1 -d imap://user01%40james.local:1234@localhost:1143 @@ -53,13 +80,13 @@ which ant ant -version cd $WORKSPACE/mail-tck/ -ant -Dreport.dir=$WORKSPACE/JTreport/mail-tck -Dwork.dir=$WORKSPACE/JTwork/mail-tck run +ant -Dreport.dir=$WORKSPACE/JTreport/mail-tck -Dwork.dir=$WORKSPACE/JTwork/mail-tck run run.pluggability HOST=`hostname -f` echo "1 mail-tck $HOST" > $WORKSPACE/args.txt mkdir -p $WORKSPACE/results/junitreports/ - +JT_REPORT_DIR=$WORKSPACE/JTreport $JAVA_HOME/bin/java -Djunit.embed.sysout=true \ -jar ${WORKSPACE}/docker/JTReportParser/JTReportParser.jar \ $WORKSPACE/args.txt $WORKSPACE/JTreport $WORKSPACE/results/junitreports/ diff --git a/mailtck.zip b/mailtck.zip new file mode 100755 index 000000000..28e4d8af2 Binary files /dev/null and b/mailtck.zip differ