diff --git a/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/internal/PortableBase64Test.groovy b/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/internal/PortableBase64Test.groovy new file mode 100644 index 00000000..bae5c5b1 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/internal/PortableBase64Test.groovy @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2016 Deutsche Telekom AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.internal + +import static org.hamcrest.CoreMatchers.* +import static org.hamcrest.MatcherAssert.assertThat +import static org.junit.Assert.fail + +import java.io.UnsupportedEncodingException + +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +/** + * @author Jochen Hiller - Initial contribution + * @author Miki Jankov - Adding new tests and fixing existing ones + */ +public class PortableBase64Test { + + private static boolean isJava8OrNewer = true + + @Before + public void testInitialize() { + try { + Class.forName("java.util.Base64", false, PortableBase64.class.getClassLoader()) + } catch (ClassNotFoundException ex) { + // not found, so we run on JavaSE 7 or older + isJava8OrNewer = false + } + PortableBase64.initialize() + } + + @Test + public void testGetStaticClasses() { + PortableBase64.Decoder decoder = PortableBase64.getDecoder() + assertThat(decoder, is(not(nullValue()))) + + PortableBase64.Encoder encoder = PortableBase64.getEncoder() + assertThat(encoder, is(not(nullValue()))) + } + + @Test + public void testDecode() { + // see https://tools.ietf.org/html/rfc4648#section-10 + FROM_BASE64("", "") + FROM_BASE64("Zg==", "f") + FROM_BASE64("Zm8=", "fo") + FROM_BASE64("Zm9v", "foo") + FROM_BASE64("Zm9vYg==", "foob") + FROM_BASE64("Zm9vYmE=", "fooba") + FROM_BASE64("Zm9vYmFy", "foobar") + } + + @Test + public void testEncode() { + // see https://tools.ietf.org/html/rfc4648#section-10 + TO_BASE64("", "") + TO_BASE64("f", "Zg==") + TO_BASE64("fo", "Zm8=") + TO_BASE64("foo", "Zm9v") + TO_BASE64("foob", "Zm9vYg==") + TO_BASE64("fooba", "Zm9vYmE=") + TO_BASE64("foobar", "Zm9vYmFy") + } + + @Test(expected = java.lang.IllegalArgumentException.class) + public void testDecodeInvalidCharacterDot() { + PortableBase64.getDecoder().decode("......") + } + + @Test(expected = java.lang.IllegalArgumentException.class) + public void testDecodeInvalidCharacterDash() { + PortableBase64.getDecoder().decode("---") + } + + @Test(expected = java.lang.IllegalStateException.class) + public void testEncodeWhenClassNotInitialized() { + PortableBase64.isInitialized = false + PortableBase64.getEncoder().encode("".bytes) + } + + @Test(expected = java.lang.IllegalStateException.class) + public void testDecodeWhenClassNotInitialized() { + PortableBase64.isInitialized = false + PortableBase64.getDecoder().decode("") + } + + /** JavaSE 7 does NOT throw an IllegalArgumentException. */ + @Test + public void testDecodeInvalidPaddingStart1() { + try { + PortableBase64.getDecoder().decode("=A==") + if (isJava8OrNewer) { + fail("IllegalArgumentException expected in JavaSE 8") + } + } catch (Exception ex) { + if (!isJava8OrNewer) { + fail("No exception expected in JavaSE 7") + } + } + } + + /** JavaSE 7 does NOT throw an IllegalArgumentException. */ + @Test + public void testDecodeInvalidPaddingStart2() { + try { + PortableBase64.getDecoder().decode("====") + if (isJava8OrNewer) { + fail("IllegalArgumentException expected in JavaSE 8") + } + } catch (Exception ex) { + if (!isJava8OrNewer) { + fail("No exception expected in JavaSE 7") + } + } + } + + /** JavaSE 7 does NOT throw an IllegalArgumentException. */ + @Test + public void testDecodeInvalidPaddingMiddle() { + try { + PortableBase64.getDecoder().decode("Zg=a") + if (isJava8OrNewer) { + fail("IllegalArgumentException expected in JavaSE 8") + } + } catch (Exception ex) { + if (!isJava8OrNewer) { + fail("No exception expected in JavaSE 7") + } + } + } + + /** + * TODO we can not easily compare this by native calls as this would + */ + @Test + public void testPerformancePortableBase64() { + long tStart = System.nanoTime() + int N = 10000000 + for (int i = 0; i < N; i++) { + PortableBase64.getEncoder().encode("foobar".getBytes()) + PortableBase64.getDecoder().decode("Zm9vYmFy") + } + long tEnd = System.nanoTime() + System.out.println("testPerformancePortableBase64 took " + (tEnd - tStart) / 1000 / 1000 + " ms for " + N + + " iterations on " + System.getProperty("java.version") + ".") + } + + @Test + public void testPerformanceJavaSE7() { + long tStart = System.nanoTime() + int N = 10000000 + for (int i = 0; i < N; i++) { + javax.xml.bind.DatatypeConverter.printBase64Binary("foobar".getBytes()) + javax.xml.bind.DatatypeConverter.parseBase64Binary("Zm9vYmFy") + } + long tEnd = System.nanoTime() + System.out.println("testPerformanceJavaSE7 " + (tEnd - tStart) / 1000 / 1000 + " ms for " + N + + " iterations on " + System.getProperty("java.version") + ".") + } + + /** + * If you want to run the test on JavaSE 8, remove @Ignore, enable the code below and import java.util.Base64 and + * compile for Java 8. + */ + @Test + @Ignore + public void testPerformanceJavaSE8() { + long tStart = System.nanoTime() + int N = 10000000 + for (int i = 0; i < N; i++) { + // enable this code to run performance tests on JavaSE 8. + // Base64.getEncoder().encode("foobar".getBytes()); + // Base64.getDecoder().decode("Zm9vYmFy"); + } + long tEnd = System.nanoTime() + System.out.println("testPerformanceJavaSE7 " + (tEnd - tStart) / 1000 / 1000 + " ms for " + N + + " iterations on " + System.getProperty("java.version") + ".") + } + + private void FROM_BASE64(String base64, String output) { + try { + PortableBase64.Decoder decoder = PortableBase64.getDecoder() + byte[] decodedAsByteArray = decoder.decode(base64) + String decodedAsString = new String(decodedAsByteArray, "UTF-8") + assertThat(output, is(equalTo(decodedAsString))) + } catch (UnsupportedEncodingException ex) { + fail("Should never happen") + } + } + + private void TO_BASE64(String input, String res) { + PortableBase64.Encoder encoder = PortableBase64.getEncoder() + String base64 = encoder.encode(input.getBytes()) + assertThat(res, is(equalTo(base64))) + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF index 8ab99477..c695785d 100644 --- a/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF +++ b/bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF @@ -31,7 +31,7 @@ Bundle-License: http://www.eclipse.org/legal/epl-v10.html Import-Package: com.google.common.base, com.google.common.collect, com.google.gson, - javax.xml.bind, + javax.xml.bind;resolution:=optional, org.apache.commons.lang, org.eclipse.smarthome.core.binding, org.eclipse.smarthome.core.binding.dto, diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/CoreActivator.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/CoreActivator.java index 4691caac..f2974895 100644 --- a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/CoreActivator.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/CoreActivator.java @@ -27,6 +27,7 @@ public class CoreActivator implements BundleActivator { @Override public void start(BundleContext bc) throws Exception { context = bc; + PortableBase64.initialize(); logger.debug("Core bundle has been started."); } diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/PortableBase64.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/PortableBase64.java new file mode 100644 index 00000000..7d20dbea --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/PortableBase64.java @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2016 Deutsche Telekom AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.core.internal; + +import java.lang.reflect.Method; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class PortableBase64 will provide Base64 encode and decode functionality in a portable way for JavaSE 7 and + * JavaSE 8. Its design is based on the new java.util.Base64 class from JavaSE 8. + * + * The implementation checks for Java version. It will call either an included class from javax.xml.bind or use the new + * Base64 class from JavaSE 8. This has been chosen to avoid a compile dependency to Java 8. + * + * For JavaSE 7 it will use the class javax.xml.bind.DatatypeConverter for + * Base64 functionality. It will get the encode and decode methods to be able to call this methods later via reflection. + * + * For JavaSE 8 it will use the built-in class java.util.Base64, get the instances for encoder and decoder and will use + * them for calling. + * + * @author Jochen Hiller - Initial contribution + * @author Miki Jankov - Adding error checks + * + */ +public class PortableBase64 { + + /** + * The encoder instance will be needed for JavaSE 8 as the encode method is an instance method. For JavaSE 7 this is + * null as it is a static method. + */ + private static Object encoderInstance; + + /** + * The encode method will expect a signature of: + * + *
+ * String encodeMethod(byte[] b); + *+ */ + private static Method encodeMethod; + + /** + * The decoder instance will be needed for JavaSE 8 as the decode method is an instance method. For JavaSE 7 this is + * null as it is a static method. + */ + private static Object decoderInstance; + + /** + * The decode method will expect a signature of: + * + *
+ * byte[] decodeMethod(String s); + *+ */ + private static Method decodeMethod; + + /** + * A flag to show if initialization was already performed + */ + private static volatile boolean isInitialized = false; + + // static helpers to initialize + + public static void initialize() { + if (isInitialized) { + logInfo("PortableBase64 class already initialized"); + return; + } + + // just try to get Java 8(or newer) class + boolean isJava8OrNewer = true; + try { + Class.forName("java.util.Base64", false, PortableBase64.class.getClassLoader()); + } catch (ClassNotFoundException ex) { + // not found, so we run on JavaSE 7 or older + isJava8OrNewer = false; + } + logInfo("PortableBase64 class is running on JavaSE " + (isJava8OrNewer ? ">=8" : "<=7")); + + try { + if (isJava8OrNewer) { + initializeJava8(); + } else { + initializeJava7(); + } + // make one test call for encode and decode to be sure that it is working + // see https://tools.ietf.org/html/rfc4648#section-10 for samples + String encodedAsString = (String) encodeMethod.invoke(encoderInstance, "foobar".getBytes("UTF-8")); + if (!"Zm9vYmFy".equals(encodedAsString)) { + throw new IllegalAccessError("encode does not work as expected"); + } + byte[] decodedAsByteArray = (byte[]) decodeMethod.invoke(decoderInstance, "Zm9vYmFy"); + String decodedAsString = new String(decodedAsByteArray, "UTF-8"); + if (!"foobar".equals(decodedAsString)) { + throw new IllegalAccessError("decode does not work as expected"); + } + PortableBase64.isInitialized = true; + } catch (Exception ex) { + logError( + "Could not initialize PortableBase64 class- Check your Java environment to run on Java 7 or 8 or later.", + ex); + encodeMethod = null; + decodeMethod = null; + // TODO fallback to an internal implementation? E.g. for JavaSE 6/5/4... + } + } + + /** + * Initialization for JavaSE 7 using javax.xml.bind.DatatypeConverter class. + */ + private static void initializeJava7() throws Exception { + // now we know that the class DataTypeConverter is available + Class> datatypeConverterClass = Class.forName("javax.xml.bind.DatatypeConverter", false, + PortableBase64.class.getClassLoader()); + // preserve static methods for later use + encodeMethod = datatypeConverterClass.getMethod("printBase64Binary", new Class[] { byte[].class }); + decodeMethod = datatypeConverterClass.getMethod("parseBase64Binary", new Class[] { String.class }); + } + + /** + * Initialization for JavaSE 8 using java.util.Base64 class. + */ + private static void initializeJava8() throws Exception { + Class> baseClass = Class.forName("java.util.Base64", false, PortableBase64.class.getClassLoader()); + + // search for inner classes + Class>[] innerClasses = baseClass.getDeclaredClasses(); + Class> encoderClass = null; + Class> decoderClass = null; + for (int i = 0; i < innerClasses.length; i++) { + Class> c = innerClasses[i]; + if (c.getName().equals("java.util.Base64$Encoder")) { + encoderClass = c; + } else if (c.getName().equals("java.util.Base64$Decoder")) { + decoderClass = c; + } else { + // ignore, we do not need + } + } + // check if we found the classes + if (encoderClass == null) { + throw new IllegalAccessError("Could not find encoderClass java.util.Base64$Encoder"); + } + if (decoderClass == null) { + throw new IllegalAccessError("Could not find decoderClass java.util.Base64$Decoder"); + } + + // preserve the instances of encoder and decoder + PortableBase64.encoderInstance = baseClass.getMethod("getEncoder", new Class[] {}).invoke(null, + (Object[]) null); + PortableBase64.decoderInstance = baseClass.getMethod("getDecoder", new Class[] {}).invoke(null, + (Object[]) null); + // preserve method on instances for later use + encodeMethod = encoderClass.getMethod("encodeToString", new Class[] { byte[].class }); + decodeMethod = decoderClass.getMethod("decode", new Class[] { String.class }); + } + + private static void logError(String msg, Exception ex) { + Logger l = LoggerFactory.getLogger(PortableBase64.class); + l.error(msg, ex); + } + + private static void logInfo(String msg) { + Logger l = LoggerFactory.getLogger(PortableBase64.class); + l.info(msg); + } + + // PortablBase64 implementation + + private static Encoder basicEncoder = new Encoder(); + private static Decoder basicDecoder = new Decoder(); + + public static Encoder getEncoder() { + return basicEncoder; + } + + public static Decoder getDecoder() { + return basicDecoder; + } + + public static class Encoder { + public String encode(byte[] base64) { + try { + if (!isInitialized) + throw new IllegalStateException("PortableBase64 is not initialized"); + Object res = encodeMethod.invoke(encoderInstance, base64); + return (String) res; + } catch (IllegalStateException ise) { + throw ise; + } catch (Exception ex) { + PortableBase64.logError("PortableBase64 - Could not encode", ex); + throw new IllegalArgumentException(ex.getMessage()); + } + } + } + + public static class Decoder { + public byte[] decode(String s) { + try { + if (!isInitialized) + throw new IllegalStateException("PortableBase64 is not initialized"); + Object res = decodeMethod.invoke(decoderInstance, s); + byte[] b = (byte[]) res; + // System.out.println("'" + new String(b) + "'"); + if ((b.length == 0) && (s.length() > 0)) { + throw new IllegalArgumentException("decode returned empty result"); + } + return b; + } catch (IllegalStateException ise) { + throw ise; + } catch (IllegalArgumentException ex) { + throw ex; + } catch (Exception ex) { + PortableBase64.logError("PortableBase64 - Could not decode", ex); + throw new IllegalArgumentException(ex.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/library/types/RawType.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/library/types/RawType.java index 844f8f80..dfa99ffe 100644 --- a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/library/types/RawType.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/library/types/RawType.java @@ -9,8 +9,7 @@ package org.eclipse.smarthome.core.library.types; import java.util.Arrays; -import javax.xml.bind.DatatypeConverter; - +import org.eclipse.smarthome.core.internal.PortableBase64; import org.eclipse.smarthome.core.types.PrimitiveType; import org.eclipse.smarthome.core.types.State; @@ -38,12 +37,12 @@ public class RawType implements PrimitiveType, State { } public static RawType valueOf(String value) { - return new RawType(DatatypeConverter.parseBase64Binary(value)); + return new RawType(PortableBase64.getDecoder().decode(value)); } @Override public String toString() { - return DatatypeConverter.printBase64Binary(bytes); + return PortableBase64.getEncoder().encode(bytes); } @Override