diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..2581fba --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,23 @@ +name: Java CI + +on: + push: + branches: [ master ] + pull_request: +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [8, 11, 16, 17] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v2.4.0 + with: + distribution: 'temurin' + java-version: ${{matrix.java}} + cache: 'maven' + - name: Build with Maven + run: mvn verify -B -e -V --file pom.xml -Dgpg.skip=true diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..fb9fbe9 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,23 @@ +name: Publish Pages +on: + push: + branches: + - master +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Build + run: | + sudo apt-get update && sudo apt-get install -y pandoc + mkdir ghpages + pandoc -o ghpages/index.html README.md + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@4.1.5 + with: + branch: gh-pages + folder: ghpages diff --git a/.gitignore b/.gitignore index 700d3af..05900eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ /.classpath /.project /.settings +/.idea +*.iml /target /src/main/java/com/softlayer/api/service /gen/target /gen/build.log /examples/target settings.xml +.DS_Store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa4eb57..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: java -jdk: - - oraclejdk8 - - oraclejdk7 - - openjdk7 - - openjdk6 -install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgpg.skip=true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b8b5301 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +Note that new releases update the API types and services and may change or remove classes, methods, and types without +notice. + +## [Unreleased] + +## [0.3.4] - 2021-12-17 + +### Changed +* Add deprecation annotations to types, properties, and methods. Deprecations may be removed in future changes to the + API. + +### Changed +* New service definitions. + +## [0.3.3] - 2021-09-15 + +### Changed +* Updated services and types. +* Updated dependencies. + +## [0.3.2] - 2021-01-20 + +### Added +* Added support for Bearer Authentication Token Support. + +```java +import com.softlayer.api.*; +ApiClient client = new RestApiClient().withBearerToken("qqqqwwwweeeaaassddd...."); +``` + +### Changed +* Updated services and types. + +## [0.3.1] - 2020-11-09 + +### Changed +* Updated services and types. + +## [0.3.0] - 2020-03-25 + +### Added +* Added a new `RestApiClient.BASE_SERVICE_URL` constant to use the client with the classic infrastructure private + network. + +### Changed +* A breaking change has been made. Coerce return types to whatever the API metadata says should be returned, even if + the type returned by the API does not match (#64). + +* Updated services and types. + +## [0.2.9] - 2020-01-21 + +### Changed +* Updated generated services and types. diff --git a/LICENSE b/LICENSE index 09c0bbd..213959f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 The SoftLayer Developer Network +Copyright (c) 2021 The SoftLayer Developer Network Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ce88df9..a771bf3 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # SoftLayer API Client for Java -[![Build Status](https://travis-ci.org/softlayer/softlayer-java.svg)](https://travis-ci.org/softlayer/softlayer-java) +[![Java CI](https://github.com/softlayer/softlayer-java/actions/workflows/maven.yml/badge.svg)](https://github.com/softlayer/softlayer-java/actions/workflows/maven.yml) +[![Maven Central](https://img.shields.io/maven-central/v/com.softlayer.api/softlayer-api-client)](https://search.maven.org/artifact/com.softlayer.api/softlayer-api-client) +[![Javadocs](https://www.javadoc.io/badge/com.softlayer.api/softlayer-api-client.svg)](https://www.javadoc.io/doc/com.softlayer.api/softlayer-api-client) ## Introduction -This library provides a JVM client for the [SoftLayer API](http://sldn.softlayer.com/article/SoftLayer-API-Overview). It -has code generated and compiled via Maven. The client can work with any Java 6+ runtime. It uses the code generation +This library provides a JVM client for the [SoftLayer API](https://sldn.softlayer.com/article/getting-started/). It +has code generated and compiled via Maven. The client can work with any Java 8+ runtime. It uses the code generation project in `gen/` to generate the service and type related code. Although likely to work in resource-constrained environments (i.e. Android, J2ME, etc), using this is not recommended; Use the -[REST](http://sldn.softlayer.com/article/REST) API instead. +[REST](https://sldn.softlayer.com/article/rest/) API instead. By default the HTTP client is the Java `HttpUrlConnection` and the JSON marshalling is done by -[Gson](https://code.google.com/p/google-gson/). Both of these pieces can be exchanged for alternative implementations +[Gson](https://github.com/google/gson). Both of these pieces can be exchanged for alternative implementations (see below). The `examples/` project has sample uses of the API. It can be executed from Maven while inside the `examples/` folder @@ -25,33 +27,71 @@ servers and may charge your account. ## Using -To add the project you your Maven project, add the dependency: +Add the library as a dependency using your favorite build tooling. + +Note that the published client library is built upon the state of the API at the time of the version's release. +It will contain the generated artifacts as of that time only. +See "Building" for more information on how to regenerate the artifacts to get regular +additions to the SoftLayer API. + +### Maven ```xml com.softlayer.api softlayer-api-client - 0.2.2 + 0.3.4 ``` -Note, the client published to Maven is built upon version change of this project. It will contain the generated -artifacts as of that time only. See "Building" for more information on how to regenerate the artifacts to get regular -additions to the SoftLayer API. +### Gradle + +```groovy +implementation 'com.softlayer.api:softlayer-api-client:0.3.4' +``` + +### Kotlin + +```kotlin +compile("com.softlayer.api:softlayer-api-client:0.3.4") +``` ### Creating a Client All clients are instances of `ApiClient`. Currently there is only one implementation, the `RestApiClient`. Simply instantiate it and provide your credentials: + +#### Username and API Key +For using a Classic Infrastructure or IBM Cloud API key. When using the IBM Cloud Api key, your username is the literal string `apikey`, more information about that can be found on the SLDN [Authenticating to the SoftLayer API](https://sldn.softlayer.com/article/authenticating-softlayer-api/#cloud-api) article. + +:warning: Make sure to avoid hard coding your username and API key when using the client! Always pull credentials from the environment, secure config, or other source. + ```java import com.softlayer.api.*; -ApiClient client = new RestApiClient().withCredentials("my user", "my api key"); +ApiClient client = new RestApiClient().withCredentials(myUser, myApiKey); +``` + +#### Access Token +Information on how to get a temporary api token can be found on the SLDN +[Authenticating to the SoftLayer API](https://sldn.softlayer.com/article/authenticating-softlayer-api/#temp-token) +article. + +```java +import com.softlayer.api.*; +ApiClient client = new RestApiClient().withBearerToken(myBearerToken); ``` If the end point isn't at the normal SoftLayer API, you can provide the prefix to the constructor of the -`RestApiClient`. By default it is set to `https://api.softlayer.com/rest/v3.1/`. +`RestApiClient`. By default, it is set to the public API endpoint, `https://api.softlayer.com/rest/v3.1/`. + +If you are using the classic infrastructure private network, you can communicate with the API over that network by using the service URL instead: + +```java +ApiClient client = new RestApiClient(RestApiClient.BASE_SERVICE_URL) + .withCredentials(myUser, myApiKey); +``` ### Making API Calls @@ -132,7 +172,7 @@ for (Vlan vlan : service.getObject().getNetworkVlans()) { All values of a type can be masked upon. If a value represents a primitive or collection of primitives, the same mask it is called on is returned. Otherwise the mask of the other type is given. These translate into SoftLayer's -[string-based object mask format](http://sldn.softlayer.com/article/Object-Masks). A string or an instance of a mask +[string-based object mask format](https://sldn.softlayer.com/article/object-masks/). A string or an instance of a mask can be given directly by calling `setMask` on the service. Note, when object masks are added on a service object, they will be sent with every service call unless removed via `clearMask` or overwritten via `withNewMask` or `setMask`. @@ -230,6 +270,13 @@ The services are not guaranteed to be thread-safe on their own, so it is difficu style, the `ResponseHandlerWithHeaders` can be used instead of `ResponseHandler`. But the safest way is to only use a single service per thread. +### Differences from the API + +Due to restrictions on identifiers in Java, some properties, methods, classes, and packages will be named differently +from the naming used by the API. For example, an API property that starts with a number will be prepended with 'z'. +[Java keywords](https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.9) that appear in identifiers may +also be replaced. + ## Building This project is intentionally provided without all of the service code. Normal Maven `install` and `package` commands @@ -256,8 +303,8 @@ name of your implementation on a single line in a file in the JAR at ### JSON Marshalling -The default JSON marshaller that is used is [Gson](https://code.google.com/p/google-gson/). In order to create your own, -alternative implementation you must implement `com.softlayer.api.json.JsonMarshallerFactyory`. Once implemented, this +The default JSON marshaller that is used is [Gson](https://github.com/google/gson). In order to create your own, +alternative implementation you must implement `com.softlayer.api.json.JsonMarshallerFactory`. Once implemented, this can be explicitly set on the `RestApiClient` by calling `setJsonMarshallerFactory`. Instead of setting the factory manually, you can also leverage Java's `ServiceLoader` mechanism to have it used by default. This involves adding the fully qualified class name of your implementation on a single line in a file in the JAR at @@ -265,4 +312,4 @@ fully qualified class name of your implementation on a single line in a file in ## Copyright -This software is Copyright (c) 2015 The SoftLayer Developer Network. See the bundled LICENSE file for more information. +This software is Copyright (c) 2021 The SoftLayer Developer Network. See the bundled LICENSE file for more information. diff --git a/examples/pom.xml b/examples/pom.xml index 3d2d634..f90f564 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,23 +1,26 @@ 4.0.0 + + 3.3.9 + com.softlayer.api softlayer-api-client-examples jar - 0.2.2 + 0.3.4 softlayer-api-client-examples - http://sldn.softlayer.com + https://sldn.softlayer.com/ The MIT License (MIT) - http://opensource.org/licenses/MIT + https://opensource.org/licenses/MIT repo UTF-8 - 1.6 + 8 @@ -28,18 +31,15 @@ - org.apache.maven.plugins maven-resources-plugin - 2.6 + 3.1.0 org.codehaus.mojo exec-maven-plugin - 1.2.1 + 3.0.0 @@ -54,7 +54,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.0 + 3.8.1 ${java.version} ${java.version} diff --git a/examples/src/main/java/com/softlayer/api/example/AddSecurityGroupRule.java b/examples/src/main/java/com/softlayer/api/example/AddSecurityGroupRule.java new file mode 100644 index 0000000..c5d21e6 --- /dev/null +++ b/examples/src/main/java/com/softlayer/api/example/AddSecurityGroupRule.java @@ -0,0 +1,45 @@ +package com.softlayer.api.example; + +import com.softlayer.api.ApiClient; +import com.softlayer.api.service.network.SecurityGroup; +import com.softlayer.api.service.network.securitygroup.Rule; + +import java.util.ArrayList; +import java.util.List; + +/** Create a simple security group */ +public class AddSecurityGroupRule extends Example { + + @Override + public void run(ApiClient client) throws Exception { + SecurityGroup.Service service = SecurityGroup.service(client); + + // create a new security group + SecurityGroup sg = new SecurityGroup(); + sg.setName("javaTest"); + sg.setDescription("javaTestDescription"); + + // create that security group + SecurityGroup sgOut = service.createObject(sg); + System.out.format("Created security group with ID: %s\n", sgOut.getId()); + + // bind the service to the id of the newly created security group + service = sgOut.asService(client); + + // Create a security group rule + Rule rule = new Rule(); + rule.setDirection("ingress"); + rule.setProtocol("udp"); + + List newRules = new ArrayList<>(); + newRules.add(rule); + + // Now add the rule(s) to the security group + System.out.println("Adding rule(s) to security group"); + service.addRules(newRules); + } + + public static void main(String[] args) throws Exception { + new AddSecurityGroupRule().start(args); + } +} diff --git a/examples/src/main/java/com/softlayer/api/example/CreateSecurityGroup.java b/examples/src/main/java/com/softlayer/api/example/CreateSecurityGroup.java new file mode 100644 index 0000000..eadbb99 --- /dev/null +++ b/examples/src/main/java/com/softlayer/api/example/CreateSecurityGroup.java @@ -0,0 +1,27 @@ +package com.softlayer.api.example; + +import com.softlayer.api.ApiClient; +import com.softlayer.api.service.network.SecurityGroup; + +/** Create security group example. */ +public class CreateSecurityGroup extends Example { + + @Override + public void run(ApiClient client) throws Exception { + SecurityGroup.Service service = SecurityGroup.service(client); + + // Create a java object representing the new security group + SecurityGroup sg = new SecurityGroup(); + sg.setName("javaTest"); + sg.setDescription("javaTestDescription"); + + // Now call the security group service to create it + System.out.println("Make call to create security group"); + SecurityGroup sgOut = service.createObject(sg); + System.out.format("Created security group with name = %s\n", sgOut.getName()); + } + + public static void main(String[] args) throws Exception { + new CreateSecurityGroup().start(args); + } +} diff --git a/examples/src/main/java/com/softlayer/api/example/Example.java b/examples/src/main/java/com/softlayer/api/example/Example.java index db2d386..32566fc 100644 --- a/examples/src/main/java/com/softlayer/api/example/Example.java +++ b/examples/src/main/java/com/softlayer/api/example/Example.java @@ -17,7 +17,14 @@ public void start(String[] args) throws Exception { baseUrl += '/'; } - run(new RestApiClient(baseUrl).withCredentials(args[0], args[1])); + RestApiClient client; + // mvn -e -q compile exec:java -Dexec.args="QuickTest Bearer eyJraWQ..... + if (args[0].trim().equals("Bearer")) { + client = new RestApiClient(baseUrl).withBearerToken(args[1]); + } else { + client = new RestApiClient(baseUrl).withCredentials(args[0], args[1]); + } + run(client); } /** Run the example with the given client */ diff --git a/examples/src/main/java/com/softlayer/api/example/ListSecurityGroups.java b/examples/src/main/java/com/softlayer/api/example/ListSecurityGroups.java new file mode 100644 index 0000000..a7f4f4e --- /dev/null +++ b/examples/src/main/java/com/softlayer/api/example/ListSecurityGroups.java @@ -0,0 +1,31 @@ +package com.softlayer.api.example; + +import com.softlayer.api.ApiClient; +import com.softlayer.api.service.Account; +import com.softlayer.api.service.network.SecurityGroup; + +/** List all security groups for an account */ +public class ListSecurityGroups extends Example { + + @Override + public void run(ApiClient client) throws Exception { + // Get the Account service + Account.Service service = Account.service(client); + + // To get specific information on an account (security groups in this case) a mask is provided + service.withMask().securityGroups(); + + // Calling getObject will now use the mask + Account account = service.getObject(); + + System.out.format("\nFound %d security groups\n", account.getSecurityGroups().size()); + + for (SecurityGroup sg : account.getSecurityGroups()) { + System.out.format("id: %s name: %s \n", sg.getId(), sg.getName()); + } + } + + public static void main(String[] args) throws Exception { + new ListSecurityGroups().start(args); + } +} diff --git a/examples/src/main/java/com/softlayer/api/example/PaginationAsyncCallback.java b/examples/src/main/java/com/softlayer/api/example/PaginationAsyncCallback.java index f7d74cd..8848d90 100644 --- a/examples/src/main/java/com/softlayer/api/example/PaginationAsyncCallback.java +++ b/examples/src/main/java/com/softlayer/api/example/PaginationAsyncCallback.java @@ -1,6 +1,5 @@ package com.softlayer.api.example; -import java.util.Comparator; import java.util.List; import java.util.NavigableSet; import java.util.Set; @@ -28,12 +27,9 @@ public void run(ApiClient client) throws Exception { ((ThreadPooledHttpClientFactory) HttpClientFactory.getDefault()).setThreadPool(threadPool); // A thread-safe set is needed to hold the resulting packages ordered by name - final NavigableSet packages = new ConcurrentSkipListSet(new Comparator() { - @Override - public int compare(Package pkg1, Package pkg2) { - return pkg1.getName().compareToIgnoreCase(pkg2.getName()); - } - }); + final NavigableSet packages = new ConcurrentSkipListSet<>( + (pkg1, pkg2) -> pkg1.getName().compareToIgnoreCase(pkg2.getName()) + ); // To know how many calls have to be made to get all items, an initial call is required to get the // first set of data AND the total count diff --git a/examples/src/main/java/com/softlayer/api/example/PaginationAsyncPolling.java b/examples/src/main/java/com/softlayer/api/example/PaginationAsyncPolling.java index c80d20d..7187441 100644 --- a/examples/src/main/java/com/softlayer/api/example/PaginationAsyncPolling.java +++ b/examples/src/main/java/com/softlayer/api/example/PaginationAsyncPolling.java @@ -1,7 +1,6 @@ package com.softlayer.api.example; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.NavigableSet; import java.util.TreeSet; @@ -27,7 +26,7 @@ public void run(ApiClient client) throws Exception { ((ThreadPooledHttpClientFactory) HttpClientFactory.getDefault()).setThreadPool(threadPool); // Asynchronous responses are held so they can be waited on once all are submitted - List responses = new ArrayList(); + List responses = new ArrayList<>(); // To know how many calls have to be made to get all items, an initial call is required to get the // first set of data AND the total count @@ -53,12 +52,9 @@ public void run(ApiClient client) throws Exception { threadPool.shutdown(); // A set is needed to hold the resulting packages ordered by name - final NavigableSet packages = new TreeSet(new Comparator() { - @Override - public int compare(Package pkg1, Package pkg2) { - return pkg1.getName().compareToIgnoreCase(pkg2.getName()); - } - }); + final NavigableSet packages = new TreeSet<>( + (pkg1, pkg2) -> pkg1.getName().compareToIgnoreCase(pkg2.getName()) + ); // Unlike the callback approach, this approach guarantees they come in the order requested since a blocking // call to get() is in the request order diff --git a/examples/src/main/java/com/softlayer/api/example/QuickTest.java b/examples/src/main/java/com/softlayer/api/example/QuickTest.java new file mode 100644 index 0000000..24701da --- /dev/null +++ b/examples/src/main/java/com/softlayer/api/example/QuickTest.java @@ -0,0 +1,28 @@ +package com.softlayer.api.example; + +import com.softlayer.api.ApiClient; +import com.softlayer.api.RestApiClient; +import com.softlayer.api.service.Account; + + +/** A quick example for testing if authentication works. + +cd softlayer-java/examples +mvn -e -q compile exec:java -Dexec.args="QuickTest Bearer eyJraWQ..... +*/ +public class QuickTest extends Example { + + @Override + public void run(ApiClient client) throws Exception { + client.withLoggingEnabled(); + System.out.format("Authorization: %s\n", client.getCredentials()); + Account.Service service = Account.service(client); + + Account account = service.getObject(); + System.out.format("Account Name: %s\n", account.getCompanyName()); + } + + public static void main(String[] args) throws Exception { + new QuickTest().start(args); + } +} diff --git a/gen/pom.xml b/gen/pom.xml index 0b1b940..768a3a5 100644 --- a/gen/pom.xml +++ b/gen/pom.xml @@ -1,13 +1,16 @@ 4.0.0 + + 3.3.9 + com.softlayer.api softlayer-api-client-gen jar - 0.2.2 + 0.3.4 softlayer-api-client-gen - http://sldn.softlayer.com + https://sldn.softlayer.com/ The MIT License (MIT) @@ -17,7 +20,7 @@ UTF-8 - 1.6 + 8 com.softlayer.api.gen.Main @@ -29,12 +32,12 @@ com.google.code.gson gson - 2.3 + 2.8.9 junit junit - 4.11 + 4.13.2 test @@ -43,7 +46,7 @@ org.codehaus.mojo exec-maven-plugin - 1.2.1 + 3.0.0 @@ -58,12 +61,15 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 + 3.3.1 + + ${java.version} + org.apache.maven.plugins maven-compiler-plugin - 3.0 + 3.8.1 ${java.version} ${java.version} diff --git a/gen/src/main/java/com/softlayer/api/gen/ClassWriter.java b/gen/src/main/java/com/softlayer/api/gen/ClassWriter.java index b51c04b..2d4ef46 100644 --- a/gen/src/main/java/com/softlayer/api/gen/ClassWriter.java +++ b/gen/src/main/java/com/softlayer/api/gen/ClassWriter.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; @@ -21,7 +22,7 @@ public class ClassWriter extends JavaWriter { public static final String SLDN_URL_BASE_PATH = "http://sldn.softlayer.com/reference/"; - + public static final String TYPE_API_CLIENT = "com.softlayer.api.ApiClient"; public static final String TYPE_API_METHOD = "com.softlayer.api.annotation.ApiMethod"; public static final String TYPE_API_PROPERTY = "com.softlayer.api.annotation.ApiProperty"; @@ -59,13 +60,13 @@ public static void emitPackageInfo(File baseDir, List classes) throws } } - public static void emitType(File baseDir, TypeClass type) throws IOException { + public static void emitType(File baseDir, TypeClass type, Meta meta) throws IOException { File fileDir = new File(baseDir, type.packageName.replace('.', '/')); fileDir.mkdirs(); Writer writer = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(new File(fileDir, type.className + ".java")), "UTF-8")); + new FileOutputStream(new File(fileDir, type.className + ".java")), StandardCharsets.UTF_8)); try { - new ClassWriter(writer, type).emitType(); + new ClassWriter(writer, type, meta).emitType(); } finally { try { writer.close(); } catch (Exception e) { } } @@ -86,16 +87,18 @@ public static String getClassName(Meta.Type type) { } public final TypeClass type; + private final Meta meta; - public ClassWriter(Writer out, TypeClass type) { + public ClassWriter(Writer out, TypeClass type, Meta meta) { super(out); this.type = type; + this.meta = meta; setIndent(" "); } public ClassWriter emitAnnotationWithAttrs(String annotationType, Object... attributes) throws IOException { int i = 0; - Map attrMap = new HashMap(attributes.length / 2 + 1); + Map attrMap = new HashMap<>(attributes.length / 2 + 1); while (i < attributes.length) { String key = attributes[i++].toString(); attrMap.put(key, attributes[i++]); @@ -112,12 +115,12 @@ public ClassWriter emitMask() throws IOException { for (TypeClass.Property property : type.properties) { if (property.nonArrayJavaType.startsWith("com.")) { beginMethod(property.nonArrayJavaType + ".Mask", property.name, PUBLIC). - emitStatement("return withSubMask(%s, %s.class)", stringLiteral(property.name), + emitStatement("return withSubMask(%s, %s.class)", stringLiteral(property.meta.name), compressType(property.nonArrayJavaType + ".Mask")). endMethod().emitEmptyLine(); } else { beginMethod("Mask", property.name, PUBLIC). - emitStatement("withLocalProperty(%s)", stringLiteral(property.name)). + emitStatement("withLocalProperty(%s)", stringLiteral(property.meta.name)). emitStatement("return this"). endMethod().emitEmptyLine(); } @@ -131,8 +134,12 @@ public ClassWriter emitProperty(TypeClass.Property property) throws IOException if (property.meta.doc != null) { emitJavadoc(property.meta.doc.replace("\n", "
\n")); } + + if (property.meta.deprecated) { + emitAnnotation("Deprecated"); + } - Map params = new HashMap(2); + Map params = new HashMap<>(2); if (!property.name.equals(property.meta.name)) { params.put("value", stringLiteral(property.meta.name)); } @@ -264,18 +271,26 @@ public ClassWriter emitServiceMethod(TypeClass.Method method, boolean async) thr javadoc += "@see " + type.meta.name + "::" + method.meta.name + ""; emitJavadoc(javadoc); + + if (method.meta.deprecated) { + emitAnnotation("Deprecated"); + } - Map params = new HashMap(2); + Map params = new HashMap<>(2); if (!method.name.equals(method.meta.name)) { params.put("value", stringLiteral(method.meta.name)); } - if (!method.meta.isstatic) { + if (!method.meta.isstatic && !"SoftLayer_Resource_Metadata".equals(type.meta.name)) { params.put("instanceRequired", true); } emitAnnotation(TYPE_API_METHOD, params); } else { // Otherwise, just a javadoc link emitJavadoc("Async version of {@link Service#" + method.name + "}"); + + if (method.meta.deprecated) { + emitAnnotation("Deprecated"); + } } String[] parameters = new String[method.parameters.size() * 2]; @@ -293,6 +308,9 @@ public ClassWriter emitServiceMethod(TypeClass.Method method, boolean async) thr // Async has an extra callback method if (async) { + if (method.meta.deprecated) { + emitAnnotation("Deprecated"); + } parameters = Arrays.copyOf(parameters, parameters.length + 2); parameters[parameters.length - 2] = TYPE_RESPONSE_HANDLER + '<' + method.javaType + '>'; parameters[parameters.length - 1] = "callback"; @@ -321,6 +339,10 @@ public ClassWriter emitServiceMethod(TypeClass.Property property, boolean async) javadoc += "@see " + type.meta.name + "::" + name + ""; emitJavadoc(javadoc); + + if (property.meta.deprecated) { + emitAnnotation("Deprecated"); + } // Instance is only required if it's not an account property if ("SoftLayer_Account".equals(type.meta.name)) { @@ -331,6 +353,10 @@ public ClassWriter emitServiceMethod(TypeClass.Property property, boolean async) } else { // Otherwise, just a javadoc link emitJavadoc("Async version of {@link Service#" + name + "}"); + + if (property.meta.deprecated) { + emitAnnotation("Deprecated"); + } } String returnType = property.javaType; @@ -352,17 +378,12 @@ public ClassWriter emitType() throws IOException { emitPackage(type.packageName); emitTypeImports(); - - // Javadoc - String javadoc = type.meta.typeDoc != null ? type.meta.typeDoc : type.meta.serviceDoc; - if (javadoc == null) { - javadoc = ""; - } else { - javadoc = javadoc.replace("\n", "
\n") + "\n\n"; + + emitJavadoc(getTypeJavadoc()); + + if (type.meta.deprecated) { + emitAnnotation("Deprecated"); } - javadoc += "@see " + type.meta.name + ""; - emitJavadoc(javadoc); // Each type has a type attribute emitAnnotation("ApiType", stringLiteral(type.meta.name)); @@ -377,8 +398,24 @@ public ClassWriter emitType() throws IOException { // Now the service if (!type.meta.noservice) { - if (type.meta.properties.containsKey("id")) { - if (type.meta.properties.containsKey("globalIdentifier")) { + + // Check if the type or any of its' parent types have id or globalIdentifier properties + Boolean containsId = false; + Boolean containsGlobalIdentifier = false; + Meta.Type searchType = type.meta; + while (searchType != null) { + if (searchType.properties.containsKey("id")) { + containsId = true; + if (searchType.properties.containsKey("globalIdentifier")) { + containsGlobalIdentifier = true; + } + break; + } + searchType = meta.types.get(searchType.base); + } + + if (containsId) { + if (containsGlobalIdentifier) { beginMethod("Service", "asService", PUBLIC, TYPE_API_CLIENT, "client"). beginControlFlow("if (id != null)"). emitStatement("return service(client, id)"). @@ -395,11 +432,11 @@ public ClassWriter emitType() throws IOException { emitStatement("return client.createService(Service.class, null)"). endMethod().emitEmptyLine(); - if (type.meta.properties.containsKey("id")) { + if (containsId) { beginMethod("Service", "service", PUBLIC_STATIC, TYPE_API_CLIENT, "client", "Long", "id"). emitStatement("return client.createService(Service.class, id == null ? null : id.toString())"). endMethod().emitEmptyLine(); - if (type.meta.properties.containsKey("globalIdentifier")) { + if (containsGlobalIdentifier) { beginMethod("Service", "service", PUBLIC_STATIC, TYPE_API_CLIENT, "client", "String", "globalIdentifier"). emitStatement("return client.createService(Service.class, globalIdentifier)"). @@ -409,14 +446,26 @@ public ClassWriter emitType() throws IOException { emitService(); } - + emitMask().endType(); return this; } + + protected String getTypeJavadoc() { + String javadoc = type.meta.typeDoc != null ? type.meta.typeDoc : type.meta.serviceDoc; + if (javadoc == null) { + javadoc = ""; + } else { + javadoc = javadoc.replace("\n", "
\n") + "\n\n"; + } + javadoc += "@see " + type.meta.name + ""; + return javadoc; + } public ClassWriter emitTypeImports() throws IOException { - Map imports = new HashMap(type.imports); - + Map imports = new HashMap<>(type.imports); + imports.remove("Mask"); imports.remove(type.className); imports.put("ApiType", TYPE_API_TYPE); diff --git a/gen/src/main/java/com/softlayer/api/gen/Generator.java b/gen/src/main/java/com/softlayer/api/gen/Generator.java index 16da038..1ba77d5 100644 --- a/gen/src/main/java/com/softlayer/api/gen/Generator.java +++ b/gen/src/main/java/com/softlayer/api/gen/Generator.java @@ -3,6 +3,11 @@ import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -15,7 +20,13 @@ public class Generator { private final URL metadataUrl; private final Restriction whitelist; private final Restriction blacklist; - + + /** + * @param dir The directory to generate classes into. + * @param metadataUrl The metadata to generate from. + * @param whitelist + * @param blacklist + */ public Generator(File dir, URL metadataUrl, Restriction whitelist, Restriction blacklist) { this.dir = dir; this.metadataUrl = metadataUrl; @@ -32,10 +43,10 @@ public void buildClient() throws IOException { applyRestrictions(meta); log("Generating source code"); - List classes = new ArrayList(meta.types.size()); + List classes = new ArrayList<>(meta.types.size()); for (Meta.Type type : meta.types.values()) { TypeClass typeClass = new MetaConverter(BASE_PKG, meta, type).buildTypeClass(); - ClassWriter.emitType(dir, typeClass); + ClassWriter.emitType(dir, typeClass, meta); classes.add(typeClass); } ClassWriter.emitPackageInfo(dir, classes); @@ -77,16 +88,22 @@ protected void log(String contents) { System.out.println(contents); } - public void recursivelyDelete(File file) { - if (file.isDirectory()) { - for (File child : file.listFiles()) { - recursivelyDelete(child); + public void recursivelyDelete(File file) throws IOException { + if (!file.exists()) { + return; + } + Files.walkFileTree(file.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; } - if (!file.delete()) { - throw new RuntimeException("Unable to delete: " + file); + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; } - } else if (file.exists() && !file.delete()) { - throw new RuntimeException("Unable to delete: " + file); - } + }); } } diff --git a/gen/src/main/java/com/softlayer/api/gen/Main.java b/gen/src/main/java/com/softlayer/api/gen/Main.java index 117dfba..3fa541a 100644 --- a/gen/src/main/java/com/softlayer/api/gen/Main.java +++ b/gen/src/main/java/com/softlayer/api/gen/Main.java @@ -13,6 +13,9 @@ /** Entry point for the code generator */ public class Main { + + protected static final String METADATA_URL = "https://api.softlayer.com/metadata/v3.1/"; + protected static final String DEFAULT_SOURCE_PATH = "../src/main/java"; public static final String USAGE = "Arguments:\n\n" + @@ -20,7 +23,7 @@ public class Main { " --src DIR - Optional directory to generate source into. The com.softlayer.api.service package\n" + " underneath this directory will be cleaned before code is generated. If not given,\n" + " ../src/main/java is used\n" + - " --url URL - Optional metadata URL. If not given, http://api.softlayer.com/metadata is used.\n" + + " --url URL - Optional metadata URL. If not given, http://api.softlayer.com/metadata/v3.1/ is used.\n" + " --whitelist FILENAME - Optional set of types, properties, and methods to whitelist. It is one\n" + " entry per line and anything not entered will not be included in the generated client. Simply\n" + " give the type name, the property as type_name.propertyName, or the method as type_name::methodName.\n" + @@ -35,15 +38,15 @@ public static void main(String[] args) throws Exception { Restriction whitelist; Restriction blacklist; try { - List argList = new ArrayList(Arrays.asList(args)); + List argList = Arrays.asList(args); if (argList.contains("--help")) { System.out.println(USAGE); return; } String dirString = getArg("--src", argList); - dir = new File(dirString != null ? dirString : "../src/main/java"); + dir = new File(dirString != null ? dirString : DEFAULT_SOURCE_PATH); String urlString = getArg("--url", argList); - url = new URL(urlString != null ? urlString : "https://api.softlayer.com/metadata/v3.1"); + url = new URL(urlString != null ? urlString : METADATA_URL); whitelist = getRestriction(getArg("--whitelist", argList)); blacklist = getRestriction(getArg("--blacklist", argList)); if (whitelist != null && blacklist != null) { @@ -83,17 +86,13 @@ private static Restriction getRestriction(String filename) throws IOException { line = line.trim(); if (line.contains("::")) { String pieces[] = line.split("::", 2); - Set methods = restriction.methods.get(pieces[0]); - if (methods == null) { - methods = new HashSet(); - restriction.methods.put(pieces[0], methods); - } + Set methods = restriction.methods.computeIfAbsent(pieces[0], key -> new HashSet<>()); methods.add(pieces[1]); } else if (line.contains(".")) { String pieces[] = line.split(".", 2); Set properties = restriction.properties.get(pieces[0]); if (properties == null) { - properties = new HashSet(); + properties = new HashSet<>(); restriction.methods.put(pieces[0], properties); } properties.add(pieces[1]); diff --git a/gen/src/main/java/com/softlayer/api/gen/Meta.java b/gen/src/main/java/com/softlayer/api/gen/Meta.java index 6fb26ce..838af47 100644 --- a/gen/src/main/java/com/softlayer/api/gen/Meta.java +++ b/gen/src/main/java/com/softlayer/api/gen/Meta.java @@ -16,8 +16,17 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +/** + * Represents the structure of the metadata provided by the API. + */ public class Meta { - + + /** + * Reads a JSON object from the given metadata URL and generates a new Meta object containing all types. + * + * @param url The API metadata URL. + * @return Meta + */ public static Meta fromUrl(URL url) { InputStream stream = null; try { @@ -33,8 +42,10 @@ public PropertyForm read(JsonReader in) throws IOException { return PropertyForm.valueOf(in.nextString().toUpperCase()); } }).create(); - Map types = gson.fromJson(new InputStreamReader(stream), - new TypeToken>(){ }.getType()); + Map types = gson.fromJson( + new InputStreamReader(stream), + new TypeToken>(){ }.getType() + ); return new Meta(types); } catch (IOException e) { throw new RuntimeException(e); @@ -44,14 +55,19 @@ public PropertyForm read(JsonReader in) throws IOException { } } } - + public final Map types; - + public Meta(Map types) { this.types = types; } + /** + * Representation of a type in the metadata API. + */ public static class Type { + public static final String BASE_TYPE_NAME = "SoftLayer_Entity"; + public String name; public String base; public String typeDoc; @@ -59,22 +75,30 @@ public static class Type { public String serviceDoc; public Map methods = Collections.emptyMap(); public boolean noservice; + public boolean deprecated; } - + + /** + * Representation of a property in the metadata API. + */ public static class Property { public String name; public String type; public boolean typeArray; public PropertyForm form; public String doc; + public boolean deprecated; } - public static enum PropertyForm { + public enum PropertyForm { LOCAL, RELATIONAL, COUNT } - + + /** + * A representation of a method in the metadata API. + */ public static class Method { public String name; public String type; @@ -87,8 +111,12 @@ public static class Method { public boolean filterable; public boolean maskable; public List parameters = Collections.emptyList(); + public boolean deprecated; } - + + /** + * A representation of a parameter in the metadata API. + */ public static class Parameter { public String name; public String type; diff --git a/gen/src/main/java/com/softlayer/api/gen/MetaConverter.java b/gen/src/main/java/com/softlayer/api/gen/MetaConverter.java index 6a328be..f123dae 100644 --- a/gen/src/main/java/com/softlayer/api/gen/MetaConverter.java +++ b/gen/src/main/java/com/softlayer/api/gen/MetaConverter.java @@ -1,12 +1,8 @@ package com.softlayer.api.gen; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; public class MetaConverter { @@ -15,10 +11,11 @@ public class MetaConverter { protected static final Set invalidClassNames; static { - keywordReplacements = new HashMap(2); + keywordReplacements = new HashMap<>(2); keywordReplacements.put("package", "pkg"); keywordReplacements.put("private", "priv"); - keywords = new HashSet(Arrays.asList(new String[] { + keywordReplacements.put("native", "nat"); + keywords = new HashSet<>(Arrays.asList( "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", @@ -29,13 +26,13 @@ public class MetaConverter { "char", "final", "interface", "static", "void", "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while" - })); - invalidClassNames = new HashSet(Arrays.asList(new String[] { + )); + invalidClassNames = new HashSet<>(Arrays.asList( "Service" - })); + )); } - protected final Map imports = new HashMap(); + protected final Map imports = new HashMap<>(); protected final String basePackageName; protected final Meta meta; protected final Meta.Type type; @@ -45,10 +42,10 @@ public MetaConverter(String basePackageName, Meta meta, Meta.Type type) { this.basePackageName = basePackageName; this.meta = meta; this.type = type; - className = getClassName(type.name); + this.className = getClassName(type.name); } - public String getClassName(String typeName) { + protected String getClassName(String typeName) { String[] pieces = typeName.split("_"); // We want just the last, but add an extra piece if invalid. We don't go recursive // or check top-level when going back because it's a rare occurrence and we're safe @@ -57,7 +54,7 @@ public String getClassName(String typeName) { if (invalidClassNames.contains(name)) { name = pieces[pieces.length - 2] + name; } - return name; + return getValidJavaIdentifier(name); } public String getMethodOrPropertyName(String className, String name) { @@ -66,7 +63,7 @@ public String getMethodOrPropertyName(String className, String name) { name = Character.toLowerCase(className.charAt(0)) + className.substring(1) + Character.toUpperCase(name.charAt(0)) + name.substring(1); } - return name; + return getValidJavaIdentifier(name); } public String getPackageName(String typeName) { @@ -79,11 +76,27 @@ public String getPackageName(String typeName) { for (int i = 0; i < pieces.length - 1; i++) { String piece = pieces[i].toLowerCase(); String replacement = keywordReplacements.get(piece); - pkg.append('.').append(replacement != null ? replacement : piece); + piece = replacement != null ? replacement : piece; + pkg.append('.').append(getValidJavaIdentifier(piece)); } return pkg.toString(); } + /** + * Provides a valid java identifier from the given name. + * Currently only checks for a valid start character and adds 'z' + * if the name has an invalid character at the start. + * + * @param name The identifier name to use. + * @return The new name after validating. + */ + public String getValidJavaIdentifier(String name) { + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + name = "z" + name; + } + return name; + } + public TypeClass buildTypeClass() { imports.clear(); String packageName = getPackageName(type.name); @@ -91,7 +104,7 @@ public TypeClass buildTypeClass() { Meta.Type baseMeta = null; String baseService = null; Meta.Type baseServiceMeta = null; - if (type.base != null && !"SoftLayer_Entity".equals(type.name)) { + if (type.base != null && !Meta.Type.BASE_TYPE_NAME.equals(type.name)) { String baseClassName = getClassName(type.base); base = getPackageName(type.base) + '.' + baseClassName; imports.put(baseClassName, base); @@ -115,7 +128,7 @@ public TypeClass buildTypeClass() { } public List getProperties() { - List properties = new ArrayList(type.properties.size()); + List properties = new ArrayList<>(type.properties.size()); for (Meta.Property property : type.properties.values()) { String javaType = getJavaType(property.type, property.typeArray); if (javaType != null) { @@ -131,12 +144,12 @@ public List getProperties() { } public List getMethods(Meta.Type baseMeta) { - List methods = new ArrayList(type.methods.size()); + List methods = new ArrayList<>(type.methods.size()); for (Meta.Method method : type.methods.values()) { String javaType = getJavaType(method.type, method.typeArray); if (javaType != null) { boolean allParametersValid = true; - List parameters = new ArrayList(method.parameters.size()); + List parameters = new ArrayList<>(method.parameters.size()); for (Meta.Parameter parameter : method.parameters) { String paramJavaType = getJavaType(parameter.type, parameter.typeArray); if (paramJavaType == null) { @@ -189,33 +202,51 @@ public List getMethods(Meta.Type baseMeta) { public String getJavaType(String typeName, boolean array) { String javaType; + // Attempt primitives first - if ("base64Binary".equals(typeName)) { - javaType = "byte[]"; - } else if ("boolean".equals(typeName)) { - javaType = "Boolean"; - } else if ("dateTime".equals(typeName)) { - javaType = "java.util.GregorianCalendar"; - imports.put("GregorianCalendar", javaType); - } else if ("decimal".equals(typeName) || "float".equals(typeName)) { - javaType = "java.math.BigDecimal"; - imports.put("BigDecimal", javaType); - } else if ("enum".equals(typeName) || "json".equals(typeName) || "string".equals(typeName)) { - javaType = "String"; - } else if ("int".equals(typeName) || "integer".equals(typeName) || - "unsignedInt".equals(typeName) || "unsignedLong".equals(typeName)) { - javaType = "Long"; - } else if ("nonNegativeInteger".equals(typeName)) { - javaType = "java.math.BigInteger"; - imports.put("BigInteger", javaType); - } else if ("void".equals(typeName)) { - javaType = "Void"; - } else if (!meta.types.containsKey(typeName)) { - return null; - } else { - String className = getClassName(typeName); - javaType = getPackageName(typeName) + '.' + className; - imports.put(className, javaType); + switch (typeName) { + case "base64Binary": + javaType = "byte[]"; + break; + case "boolean": + javaType = "Boolean"; + break; + case "dateTime": + javaType = GregorianCalendar.class.getName(); + imports.put("GregorianCalendar", javaType); + break; + case "decimal": + case "float": + javaType = BigDecimal.class.getName(); + imports.put("BigDecimal", javaType); + break; + case "enum": + case "json": + case "string": + javaType = "String"; + break; + case "int": + case "integer": + case "unsignedInt": + case "unsignedLong": + javaType = "Long"; + break; + case "nonNegativeInteger": + javaType = BigInteger.class.getName(); + imports.put("BigInteger", javaType); + break; + case "void": + javaType = "Void"; + break; + default: + if (!meta.types.containsKey(typeName)) { + return null; + } else { + String className = getClassName(typeName); + javaType = getPackageName(typeName) + '.' + className; + imports.put(className, javaType); + } + break; } if (array) { imports.put("List", "java.util.List"); diff --git a/gen/src/main/java/com/softlayer/api/gen/Restriction.java b/gen/src/main/java/com/softlayer/api/gen/Restriction.java index 1483178..6fecd22 100644 --- a/gen/src/main/java/com/softlayer/api/gen/Restriction.java +++ b/gen/src/main/java/com/softlayer/api/gen/Restriction.java @@ -7,11 +7,11 @@ public class Restriction { /** If empty, there is no restriction on any type */ - public final Set types = new HashSet(); + public final Set types = new HashSet<>(); /** If empty, there is no restriction on any property */ - public final Map> properties = new HashMap>(); + public final Map> properties = new HashMap<>(); /** If empty, there is no restriction on any method */ - public final Map> methods = new HashMap>(); + public final Map> methods = new HashMap<>(); } diff --git a/pom.xml b/pom.xml index 2caf573..23bd176 100644 --- a/pom.xml +++ b/pom.xml @@ -1,27 +1,37 @@ 4.0.0 + + 3.3.9 + com.softlayer.api softlayer-api-client jar - 0.2.2 + 0.3.4 SoftLayer API Client for Java API client for accessing the SoftLayer API - http://sldn.softlayer.com + https://sldn.softlayer.com The MIT License (MIT) - http://opensource.org/licenses/MIT + https://opensource.org/licenses/MIT repo + + GitHub Issues + https://github.com/softlayer/softlayer-java/issues + + + Travis CI + https://travis-ci.org/softlayer/softlayer-java + - cretz - Chad Retz - cretz@softlayer.com - SoftLayer - http://softlayer.com + camporter + Cameron Porter + IBM + https://ibm.com/cloud owner developer @@ -33,18 +43,12 @@ scm:git:git@github.com:softlayer/softlayer-java.git scm:git:git@github.com:softlayer/softlayer-java.git git@github.com:softlayer/softlayer-java.git - 0.2.2 + 0.3.4 UTF-8 - 1.6 + 8 - - - jcenter - http://jcenter.bintray.com/ - - ossrh @@ -59,18 +63,18 @@ com.google.code.gson gson - 2.3 + 2.8.9 junit junit - 4.11 + 4.13.2 test org.mockito mockito-core - 1.10.4 + 4.2.0 test @@ -79,7 +83,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.0 + 3.8.1 ${java.version} ${java.version} @@ -88,7 +92,7 @@ org.apache.maven.plugins maven-source-plugin - 2.2.1 + 3.1.0 attach-sources @@ -101,7 +105,14 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 + 3.3.1 + + + + none + ${java.version} + --allow-script-in-comments + attach-javadocs @@ -114,7 +125,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 3.0.1 sign-artifacts @@ -128,7 +139,7 @@ maven-invoker-plugin - 1.8 + 3.2.2 generate-services @@ -147,6 +158,48 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + **/*com/softlayer/api/service/**/* + + + + + + prepare-agent + + + + report + prepare-package + + report + + + +
+ + + + org.codehaus.mojo + versions-maven-plugin + 2.8.1 + + + + dependency-updates-report + plugin-updates-report + property-updates-report + + + + + +
diff --git a/src/main/java/com/softlayer/api/ApiClient.java b/src/main/java/com/softlayer/api/ApiClient.java index 70cf4b3..a637995 100644 --- a/src/main/java/com/softlayer/api/ApiClient.java +++ b/src/main/java/com/softlayer/api/ApiClient.java @@ -1,5 +1,7 @@ package com.softlayer.api; +import com.softlayer.api.http.HttpCredentials; + /** Common interface for all API clients. {@link RestApiClient} is the preferred implementation */ public interface ApiClient { @@ -8,12 +10,33 @@ public interface ApiClient { * * @return This instance */ - public ApiClient withCredentials(String username, String apiKey); - + ApiClient withCredentials(String username, String apiKey); + + /** + * Uses a HTTP Bearer token for authentication instead of API key. + * + * @return This instance + */ + ApiClient withBearerToken(String token); + + /** + * Enables logging for client API calls + * + * @return This instance + */ + ApiClient withLoggingEnabled(); + + /** + * Returns the HTTP Authorization header + * + * @return This instance + */ + HttpCredentials getCredentials(); + /** * Get a service for the given sets of classes and optional ID. It is not recommended to call this * directly, but rather invoke the service method on the type class. * E.g. {@link com.softlayer.api.service.Account#service(ApiClient)}. */ - public S createService(Class serviceClass, String id); + S createService(Class serviceClass, String id); } diff --git a/src/main/java/com/softlayer/api/Mask.java b/src/main/java/com/softlayer/api/Mask.java index a62c3bb..9fae4d8 100644 --- a/src/main/java/com/softlayer/api/Mask.java +++ b/src/main/java/com/softlayer/api/Mask.java @@ -7,8 +7,8 @@ /** Object mask parameter. See http://sldn.softlayer.com/article/Object-Masks */ public class Mask { - private final Set localProperties = new HashSet(); - private final Map subMasks = new HashMap(); + private final Set localProperties = new HashSet<>(); + private final Map subMasks = new HashMap<>(); /** Clear out all previously masked objects and local properties */ public void clear() { @@ -29,7 +29,7 @@ protected T withSubMask(String name, Class maskClass) { T subMask = (T) subMasks.get(name); if (subMask == null) { try { - subMask = maskClass.newInstance(); + subMask = maskClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(); } diff --git a/src/main/java/com/softlayer/api/Maskable.java b/src/main/java/com/softlayer/api/Maskable.java index 81c1c7a..4a3093e 100644 --- a/src/main/java/com/softlayer/api/Maskable.java +++ b/src/main/java/com/softlayer/api/Maskable.java @@ -4,17 +4,17 @@ public interface Maskable { /** Overwrite the existing mask on this service with a new one and return it */ - public Mask withNewMask(); + Mask withNewMask(); /** Use the existing mask on this service or create it if not present */ - public Mask withMask(); + Mask withMask(); /** Set the mask to the given object */ - public void setMask(Mask mask); + void setMask(Mask mask); /** Set the mask to a string, formatted according to http://sldn.softlayer.com/article/Object-Masks */ - public void setMask(String mask); + void setMask(String mask); /** Removes the mask from the service */ - public void clearMask(); + void clearMask(); } diff --git a/src/main/java/com/softlayer/api/ResponseHandler.java b/src/main/java/com/softlayer/api/ResponseHandler.java index edb25f3..a807a35 100644 --- a/src/main/java/com/softlayer/api/ResponseHandler.java +++ b/src/main/java/com/softlayer/api/ResponseHandler.java @@ -4,8 +4,8 @@ public interface ResponseHandler { /** Called when the method errored. This is NOT called when onSuccess errors. */ - public void onError(Exception ex); + void onError(Exception ex); /** Called when the method succeeds. */ - public void onSuccess(T value); + void onSuccess(T value); } diff --git a/src/main/java/com/softlayer/api/RestApiClient.java b/src/main/java/com/softlayer/api/RestApiClient.java index c945442..43450c8 100644 --- a/src/main/java/com/softlayer/api/RestApiClient.java +++ b/src/main/java/com/softlayer/api/RestApiClient.java @@ -9,11 +9,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.URLEncoder; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Scanner; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -22,17 +18,29 @@ import com.softlayer.api.annotation.ApiMethod; import com.softlayer.api.annotation.ApiService; +import com.softlayer.api.http.HttpCredentials; import com.softlayer.api.http.HttpBasicAuthCredentials; +import com.softlayer.api.http.HttpBearerCredentials; import com.softlayer.api.http.HttpClient; import com.softlayer.api.http.HttpClientFactory; import com.softlayer.api.http.HttpResponse; import com.softlayer.api.json.JsonMarshallerFactory; import com.softlayer.api.service.Entity; -/** Implementation of API client for http://sldn.softlayer.com/article/REST */ +/** + * Implementation of API client for http://sldn.softlayer.com/article/REST + */ public class RestApiClient implements ApiClient { + /** + * The publically available API URL. + */ public static final String BASE_URL = "https://api.softlayer.com/rest/v3.1/"; + + /** + * The API URL that should be used when connecting via the softlayer/classic infrastructure private network. + */ + public static final String BASE_SERVICE_URL = "https://api.service.softlayer.com/rest/v3.1/"; static final String BASE_PKG = Entity.class.getPackage().getName(); @@ -41,17 +49,37 @@ public class RestApiClient implements ApiClient { static { HEADERS = Collections.singletonMap("SoftLayer-Include-Types", Collections.singletonList("true")); } - + + /** + * A list of service methods that do not have to be added to the REST URL. + * createObjects is supposed to work, but does not. + */ + private static final List IMPLICIT_SERVICE_METHODS = Arrays.asList( + "getObject", + "deleteObject", + "createObject", + "editObject", + "editObjects" + ); + private final String baseUrl; private HttpClientFactory httpClientFactory; private JsonMarshallerFactory jsonMarshallerFactory; private boolean loggingEnabled = false; - private HttpBasicAuthCredentials credentials; - + private HttpCredentials credentials; + + /** + * Create a Rest client that uses the publically available API. + */ public RestApiClient() { this(BASE_URL); } - + + /** + * Create a Rest client with a custom URL. + * + * @param baseUrl The custom URL the REST client will use. + */ public RestApiClient(String baseUrl) { // Add trailing slash if not present if (!baseUrl.endsWith("/")) { @@ -59,7 +87,7 @@ public RestApiClient(String baseUrl) { } this.baseUrl = baseUrl; } - + public String getBaseUrl() { return baseUrl; } @@ -88,6 +116,7 @@ public void setLoggingEnabled(boolean loggingEnabled) { this.loggingEnabled = loggingEnabled; } + @Override public RestApiClient withLoggingEnabled() { this.loggingEnabled = true; return this; @@ -115,7 +144,14 @@ public RestApiClient withCredentials(String username, String apiKey) { return this; } - public HttpBasicAuthCredentials getCredentials() { + @Override + public RestApiClient withBearerToken(String token) { + credentials = new HttpBearerCredentials(token); + return this; + } + + @Override + public HttpCredentials getCredentials() { return credentials; } @@ -125,17 +161,31 @@ protected void writeParameterHttpBody(Object[] params, OutputStream out) { } protected String getHttpMethodFromMethodName(String methodName) { - if ("deleteObject".equals(methodName)) { - return "DELETE"; - } else if ("createObject".equals(methodName) || "createObjects".equals(methodName)) { - return "POST"; - } else if ("editObject".equals(methodName) || "editObjects".equals(methodName)) { - return "PUT"; - } else { - return "GET"; + switch (methodName) { + case "deleteObject": + return "DELETE"; + case "createObject": + case "createObjects": + return "POST"; + case "editObject": + case "editObjects": + return "PUT"; + default: + return "GET"; } } - + + /** + * Get the full REST URL required to make a request. + * + * @param serviceName The name of the API service. + * @param methodName The name of the method on the service to call. + * @param id The identifier of the object to make a call to, + * otherwise null if not making a request to a specific object. + * @param resultLimit The number of results to limit the request to. + * @param maskString The mask, in string form, to use on the request. + * @return String + */ protected String getFullUrl(String serviceName, String methodName, String id, ResultLimit resultLimit, String maskString) { StringBuilder url = new StringBuilder(baseUrl + serviceName); @@ -146,11 +196,10 @@ protected String getFullUrl(String serviceName, String methodName, String id, // Some method names are not included, others can have the "get" stripped if (methodName.startsWith("get") && !"getObject".equals(methodName)) { url.append('/').append(methodName.substring(3)); - } else if (!"getObject".equals(methodName) && !"deleteObject".equals(methodName) && - !"createObject".equals(methodName) && !"createObjects".equals(methodName) && - !"editObject".equals(methodName) && !"editObjects".equals(methodName)) { + } else if (!IMPLICIT_SERVICE_METHODS.contains(methodName)) { url.append('/').append(methodName); } + url.append(".json"); if (resultLimit != null) { url.append("?resultLimit=").append(resultLimit.offset).append(',').append(resultLimit.limit); @@ -190,7 +239,7 @@ protected void logResponse(String url, int statusCode, String body) { @SuppressWarnings("unchecked") public S createService(Class serviceClass, String id) { return (S) Proxy.newProxyInstance(getClass().getClassLoader(), - new Class[] { serviceClass }, new ServiceProxy(serviceClass, id)); + new Class[] { serviceClass }, new ServiceProxy<>(serviceClass, id)); } class ServiceProxy implements InvocationHandler { @@ -289,12 +338,9 @@ public Object invokeService(Method method, final Object[] args) throws Throwable final HttpClient client = getHttpClientFactory().getHttpClient(credentials, httpMethod, url, HEADERS); // Invoke with response - HttpResponse response = client.invokeSync(new Callable() { - @Override - public Void call() throws Exception { - logRequestAndWriteBody(client, httpMethod, url, args); - return null; - } + HttpResponse response = client.invokeSync(() -> { + logRequestAndWriteBody(client, httpMethod, url, args); + return null; }); return logAndHandleResponse(response, url, method.getGenericReturnType()); @@ -326,13 +372,10 @@ public Object invokeServiceAsync(final Method asyncMethod, final Object[] args) final String url = getFullUrl(serviceClass.getAnnotation(ApiService.class).value(), methodName, methodId, resultLimit, mask == null ? maskString : mask.getMask()); final HttpClient client = getHttpClientFactory().getHttpClient(credentials, httpMethod, url, HEADERS); - - Callable setupBody = new Callable() { - @Override - public Void call() throws Exception { - logRequestAndWriteBody(client, httpMethod, url, trimmedArgs); - return null; - } + + Callable setupBody = () -> { + logRequestAndWriteBody(client, httpMethod, url, trimmedArgs); + return null; }; if (lastParamCallback) { @@ -418,20 +461,21 @@ public boolean isDone() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { boolean noParams = args == null || args.length == 0; + if ("asAsync".equals(method.getName()) && noParams) { - ServiceProxy asyncProxy = new ServiceProxy(serviceClass, id); + ServiceProxy asyncProxy = new ServiceProxy<>(serviceClass, id); asyncProxy.mask = mask; asyncProxy.maskString = maskString; asyncProxy.resultLimit = resultLimit; return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { method.getReturnType() }, asyncProxy); } else if ("withNewMask".equals(method.getName()) && noParams) { - mask = (Mask) method.getReturnType().newInstance(); + mask = (Mask) method.getReturnType().getDeclaredConstructor().newInstance(); maskString = null; return mask; } else if ("withMask".equals(method.getName()) && noParams) { if (mask == null) { - mask = (Mask) method.getReturnType().newInstance(); + mask = (Mask) method.getReturnType().getDeclaredConstructor().newInstance(); maskString = null; } return mask; diff --git a/src/main/java/com/softlayer/api/ResultLimitable.java b/src/main/java/com/softlayer/api/ResultLimitable.java index a7a80f1..6dd8719 100644 --- a/src/main/java/com/softlayer/api/ResultLimitable.java +++ b/src/main/java/com/softlayer/api/ResultLimitable.java @@ -3,10 +3,10 @@ /** Interface implemented by services to support pagination */ public interface ResultLimitable { - public ResultLimit getResultLimit(); + ResultLimit getResultLimit(); - public ResultLimit setResultLimit(ResultLimit limit); + ResultLimit setResultLimit(ResultLimit limit); /** The non-paginated total item count. This can be overwritten if a service is reused */ - public Integer getLastResponseTotalItemCount(); + Integer getLastResponseTotalItemCount(); } diff --git a/src/main/java/com/softlayer/api/Service.java b/src/main/java/com/softlayer/api/Service.java index db71a62..57b30a6 100644 --- a/src/main/java/com/softlayer/api/Service.java +++ b/src/main/java/com/softlayer/api/Service.java @@ -4,5 +4,5 @@ public interface Service extends Maskable, ResultLimitable { /** Get an async version of this service */ - public ServiceAsync asAsync(); + ServiceAsync asAsync(); } diff --git a/src/main/java/com/softlayer/api/Type.java b/src/main/java/com/softlayer/api/Type.java index f3f277b..158ec49 100644 --- a/src/main/java/com/softlayer/api/Type.java +++ b/src/main/java/com/softlayer/api/Type.java @@ -25,6 +25,6 @@ public Map getUnknownProperties() { */ public void setUnknownProperties(Map unknownProperties) { this.unknownProperties = Collections.unmodifiableMap( - new HashMap(unknownProperties)); + new HashMap<>(unknownProperties)); } } diff --git a/src/main/java/com/softlayer/api/http/BuiltInHttpClientFactory.java b/src/main/java/com/softlayer/api/http/BuiltInHttpClientFactory.java index da13a30..2d96b32 100644 --- a/src/main/java/com/softlayer/api/http/BuiltInHttpClientFactory.java +++ b/src/main/java/com/softlayer/api/http/BuiltInHttpClientFactory.java @@ -5,7 +5,6 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; -import java.net.ProtocolException; import java.net.URL; import java.util.List; import java.util.Map; @@ -17,8 +16,6 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import javax.xml.bind.DatatypeConverter; - import com.softlayer.api.ResponseHandler; /** @@ -92,19 +89,19 @@ public void setThreadPool(ExecutorService threadPool) { class BuiltInHttpClient implements HttpClient, HttpResponse { - final HttpBasicAuthCredentials credentials; + final HttpCredentials credentials; final String method; final String fullUrl; final Map> headers; HttpURLConnection connection; - public BuiltInHttpClient(HttpCredentials credentials, String method, - String fullUrl, Map> headers) { - // We only support basic auth - if (credentials != null && !(credentials instanceof HttpBasicAuthCredentials)) { - throw new UnsupportedOperationException("Only basic auth is supported, not " + credentials.getClass()); - } - this.credentials = (HttpBasicAuthCredentials) credentials; + public BuiltInHttpClient( + HttpCredentials credentials, + String method, + String fullUrl, + Map> headers + ) { + this.credentials = credentials; this.method = method; this.fullUrl = fullUrl; this.headers = headers; @@ -138,29 +135,18 @@ public HttpResponse invokeSync(Callable setupBody) { // fairly fast and safe. openConnection(); if (credentials != null) { - // XXX: Using JAXB datatype converter here because it's the only base 64 I trust to be around... - // should we embed a base 64 encoder in here? - HttpBasicAuthCredentials authCredentials = (HttpBasicAuthCredentials) credentials; - try { - connection.addRequestProperty("Authorization", "Basic " + new String(DatatypeConverter. - printBase64Binary((authCredentials.username + ':' + authCredentials.apiKey).getBytes("UTF-8")))); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + connection.addRequestProperty( + "Authorization", + credentials.getHeader() + ); } for (Map.Entry> headerEntry : headers.entrySet()) { for (String headerValue : headerEntry.getValue()) { connection.addRequestProperty(headerEntry.getKey(), headerValue); } } - if (!"GET".equals(method)) { - try { - connection.setRequestMethod(method); - } catch (ProtocolException e) { - throw new RuntimeException(e); - } - } try { + connection.setRequestMethod(method); setupBody.call(); } catch (Exception e) { throw new RuntimeException(e); @@ -170,31 +156,21 @@ public HttpResponse invokeSync(Callable setupBody) { @Override public Future invokeAsync(final Callable setupBody) { - return getThreadPool().submit(new Callable() { - @Override - public HttpResponse call() throws Exception { - // We let any exception here properly bubble out of the future - HttpResponse response = invokeSync(setupBody); - return response; - } - }); + return getThreadPool().submit(() -> invokeSync(setupBody)); } @Override public Future invokeAsync(final Callable setupBody, final ResponseHandler callback) { - return getThreadPool().submit(new Callable() { - @Override - public Void call() throws Exception { - HttpResponse resp; - try { - resp = invokeSync(setupBody); - } catch (Exception e) { - callback.onError(e); - return null; - } - callback.onSuccess(resp); + return getThreadPool().submit(() -> { + HttpResponse response; + try { + response = invokeSync(setupBody); + } catch (Exception e) { + callback.onError(e); return null; } + callback.onSuccess(response); + return null; }); } diff --git a/src/main/java/com/softlayer/api/http/HttpBasicAuthCredentials.java b/src/main/java/com/softlayer/api/http/HttpBasicAuthCredentials.java index 4752926..1e10ab4 100644 --- a/src/main/java/com/softlayer/api/http/HttpBasicAuthCredentials.java +++ b/src/main/java/com/softlayer/api/http/HttpBasicAuthCredentials.java @@ -1,6 +1,8 @@ package com.softlayer.api.http; -/** HTTP basic authentication support for username and API key */ +import java.util.Base64; + +/** HTTP basic authorization support for username and API key */ public class HttpBasicAuthCredentials implements HttpCredentials { public final String username; @@ -10,4 +12,15 @@ public HttpBasicAuthCredentials(String username, String apiKey) { this.username = username; this.apiKey = apiKey; } + + /** + * Gets the encoded representation of the basic authentication credentials + * for use in an HTTP Authorization header. + * + * @return String + */ + public String getHeader() { + String authPair = username + ':' + apiKey; + return "Basic " + Base64.getEncoder().encodeToString(authPair.getBytes()); + } } diff --git a/src/main/java/com/softlayer/api/http/HttpBearerCredentials.java b/src/main/java/com/softlayer/api/http/HttpBearerCredentials.java new file mode 100644 index 0000000..951f4f2 --- /dev/null +++ b/src/main/java/com/softlayer/api/http/HttpBearerCredentials.java @@ -0,0 +1,24 @@ +package com.softlayer.api.http; + +/** HTTP Bearer authorization support for IBM IAM Tokens. + * + * @see IAM Tokens + * @see Authenticating SoftLayer API + */ +public class HttpBearerCredentials implements HttpCredentials { + + protected final String token; + + public HttpBearerCredentials(String token) { + this.token = token; + } + + /** + * Formats the token into a HTTP Authorization header. + * + * @return String + */ + public String getHeader() { + return "Bearer " + token; + } +} diff --git a/src/main/java/com/softlayer/api/http/HttpClient.java b/src/main/java/com/softlayer/api/http/HttpClient.java index 30fa8a2..f3a2496 100644 --- a/src/main/java/com/softlayer/api/http/HttpClient.java +++ b/src/main/java/com/softlayer/api/http/HttpClient.java @@ -14,14 +14,14 @@ public interface HttpClient extends Closeable { /** Stream to write body contents to (if at all). When called, callers are expected to close it. */ - public OutputStream getBodyStream(); + OutputStream getBodyStream(); /** Make synchronous HTTP invocation. Throws if unable to connect. Errors from the API are returned normally. */ - public HttpResponse invokeSync(Callable setupBody); + HttpResponse invokeSync(Callable setupBody); /** Make asynchronous HTTP invocation. All errors (inability to connect or API errors) are in the future. */ - public Future invokeAsync(Callable setupBody); + Future invokeAsync(Callable setupBody); - /** Callback-form of {@link #invokeAsync()} */ - public Future invokeAsync(Callable setupBody, ResponseHandler callback); + /** Callback-form of {@link #invokeAsync(Callable)} */ + Future invokeAsync(Callable setupBody, ResponseHandler callback); } diff --git a/src/main/java/com/softlayer/api/http/HttpCredentials.java b/src/main/java/com/softlayer/api/http/HttpCredentials.java index a0ae093..a751a46 100644 --- a/src/main/java/com/softlayer/api/http/HttpCredentials.java +++ b/src/main/java/com/softlayer/api/http/HttpCredentials.java @@ -2,4 +2,5 @@ /** Base interface for all accepted HTTP credentials */ public interface HttpCredentials { + String getHeader(); } diff --git a/src/main/java/com/softlayer/api/http/HttpResponse.java b/src/main/java/com/softlayer/api/http/HttpResponse.java index 817081e..d444c46 100644 --- a/src/main/java/com/softlayer/api/http/HttpResponse.java +++ b/src/main/java/com/softlayer/api/http/HttpResponse.java @@ -7,10 +7,10 @@ /** Interface representing an HTTP response from the HTTP client */ public interface HttpResponse { - public int getStatusCode(); + int getStatusCode(); - public Map> getHeaders(); + Map> getHeaders(); /** When this is used by the caller, he is expected to close it */ - public InputStream getInputStream(); + InputStream getInputStream(); } diff --git a/src/main/java/com/softlayer/api/json/Base64.java b/src/main/java/com/softlayer/api/json/Base64.java deleted file mode 100644 index 872e34f..0000000 --- a/src/main/java/com/softlayer/api/json/Base64.java +++ /dev/null @@ -1,2067 +0,0 @@ -package com.softlayer.api.json; - -// We use this public domain version because we cannot assume the user is on Java 8 - -/** - *

Encodes and decodes to and from Base64 notation.

- *

Homepage: http://iharder.net/base64.

- * - *

Example:

- * - * String encoded = Base64.encode( myByteArray ); - *
- * byte[] myByteArray = Base64.decode( encoded ); - * - *

The options parameter, which appears in a few places, is used to pass - * several pieces of information to the encoder. In the "higher level" methods such as - * encodeBytes( bytes, options ) the options parameter can be used to indicate such - * things as first gzipping the bytes before encoding them, not inserting linefeeds, - * and encoding using the URL-safe and Ordered dialects.

- * - *

Note, according to RFC3548, - * Section 2.1, implementations should not add line feeds unless explicitly told - * to do so. I've got Base64 set to this behavior now, although earlier versions - * broke lines by default.

- * - *

The constants defined in Base64 can be OR-ed together to combine options, so you - * might make a call like this:

- * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); - *

to compress the data before encoding it and then making the output have newline characters.

- *

Also...

- * String encoded = Base64.encodeBytes( crazyString.getBytes() ); - * - * - * - *

- * Change Log: - *

- *
    - *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the - * value 01111111, which is an invalid base 64 character but should not - * throw an ArrayIndexOutOfBoundsException either. Led to discovery of - * mishandling (or potential for better handling) of other bad input - * characters. You should now get an IOException if you try decoding - * something that has bad characters in it.
  • - *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded - * string ended in the last column; the buffer was not properly shrunk and - * contained an extra (null) byte that made it into the string.
  • - *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size - * was wrong for files of size 31, 34, and 37 bytes.
  • - *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing - * the Base64.OutputStream closed the Base64 encoding (by padding with equals - * signs) too soon. Also added an option to suppress the automatic decoding - * of gzipped streams. Also added experimental support for specifying a - * class loader when using the - * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} - * method.
  • - *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java - * footprint with its CharEncoders and so forth. Fixed some javadocs that were - * inconsistent. Removed imports and specified things like java.io.IOException - * explicitly inline.
  • - *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the - * final encoded data will be so that the code doesn't have to create two output - * arrays: an oversized initial one and then a final, exact-sized one. Big win - * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not - * using the gzip options which uses a different mechanism with streams and stuff).
  • - *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some - * similar helper methods to be more efficient with memory by not returning a - * String but just a byte array.
  • - *
  • v2.3 - This is not a drop-in replacement! This is two years of comments - * and bug fixes queued up and finally executed. Thanks to everyone who sent - * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. - * Much bad coding was cleaned up including throwing exceptions where necessary - * instead of returning null values or something similar. Here are some changes - * that may affect you: - *
      - *
    • Does not break lines, by default. This is to keep in compliance with - * RFC3548.
    • - *
    • Throws exceptions instead of returning null values. Because some operations - * (especially those that may permit the GZIP option) use IO streams, there - * is a possiblity of an java.io.IOException being thrown. After some discussion and - * thought, I've changed the behavior of the methods to throw java.io.IOExceptions - * rather than return null if ever there's an error. I think this is more - * appropriate, though it will require some changes to your code. Sorry, - * it should have been done this way to begin with.
    • - *
    • Removed all references to System.out, System.err, and the like. - * Shame on me. All I can say is sorry they were ever there.
    • - *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed - * such as when passed arrays are null or offsets are invalid.
    • - *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. - * This was especially annoying before for people who were thorough in their - * own projects and then had gobs of javadoc warnings on this file.
    • - *
    - *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
  • - *
  • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
      - *
    1. The default is RFC3548 format.
    2. - *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
    4. - *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
    6. - *
    - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
  • - * - *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
  • - *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
  • - *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
  • - *
  • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
  • - *
  • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
  • - *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
  • - *
  • v1.4 - Added helper methods to read/write files.
  • - *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • - *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
  • - *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • - *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • - *
- * - *

- * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

- * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.7 - */ -class Base64 -{ - -/* ******** P U B L I C F I E L D S ******** */ - - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding in first bit. Value is one. */ - public final static int ENCODE = 1; - - - /** Specify decoding in first bit. Value is zero. */ - public final static int DECODE = 0; - - - /** Specify that data should be gzip-compressed in second bit. Value is two. */ - public final static int GZIP = 2; - - /** Specify that gzipped data should not be automatically gunzipped. */ - public final static int DONT_GUNZIP = 4; - - - /** Do break lines when encoding. Value is 8. */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - -/* ******** P R I V A T E F I E L D S ******** */ - - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte)'='; - - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte)'\n'; - - - /** Preferred encoding. */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - -/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' - }; - - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9,-9,-9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - -/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' - }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - - -/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, - * and it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { - (byte)'-', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', - (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'_', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' - }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' - 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' - 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - -/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet( int options ) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet( int options ) { - if( (options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet - - - - /** Defeats instantiation. */ - private Base64(){} - - - - -/* ******** E N C O D I N G M E T H O D S ******** */ - - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) { - encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); - return b4; - } // end encode3to4 - - - /** - *

Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

- *

This is the lowest level of the encoding methods with - * all possible parameters.

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options ) { - - byte[] ALPHABET = getAlphabet( options ); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) - | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) - | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); - - switch( numSigBytes ) - { - case 3: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; - return destination; - - case 2: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - case 1: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = EQUALS_SIGN; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded ByteBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){ - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while( raw.hasRemaining() ){ - int rem = Math.min(3,raw.remaining()); - raw.get(raw3,0,rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); - encoded.put(enc4); - } // end input remaining - } - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded CharBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){ - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while( raw.hasRemaining() ){ - int rem = Math.min(3,raw.remaining()); - raw.get(raw3,0,rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); - for( int i = 0; i < 4; i++ ){ - encoded.put( (char)(enc4[i] & 0xFF) ); - } - } // end input remaining - } - - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject The object to encode - * @return The Base64-encoded object - * @throws java.io.IOException if there is an error - * @throws NullPointerException if serializedObject is null - * @since 1.4 - */ - public static String encodeObject( java.io.Serializable serializableObject ) - throws java.io.IOException { - return encodeObject( serializableObject, NO_OPTIONS ); - } // end encodeObject - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     * 
- *

- * Example: encodeObject( myObj, Base64.GZIP ) or - *

- * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * @param serializableObject The object to encode - * @param options Specified options - * @return The Base64-encoded object - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @since 2.0 - */ - public static String encodeObject( java.io.Serializable serializableObject, int options ) - throws java.io.IOException { - - if( serializableObject == null ){ - throw new NullPointerException( "Cannot serialize a null object." ); - } // end if: null - - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.util.zip.GZIPOutputStream gzos = null; - java.io.ObjectOutputStream oos = null; - - - try { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - if( (options & GZIP) != 0 ){ - // Gzip - gzos = new java.util.zip.GZIPOutputStream(b64os); - oos = new java.io.ObjectOutputStream( gzos ); - } else { - // Not gzipped - oos = new java.io.ObjectOutputStream( b64os ); - } - oos.writeObject( serializableObject ); - } // end try - catch( java.io.IOException e ) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try{ oos.close(); } catch( Exception e ){} - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue){ - // Fall back to some Java default - return new String( baos.toByteArray() ); - } // end catch - - } // end encode - - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @return The data in Base64-encoded form - * @throws NullPointerException if source array is null - * @since 1.4 - */ - public static String encodeBytes( byte[] source ) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int options ) throws java.io.IOException { - return encodeBytes( source, 0, source.length, options ); - } // end encodeBytes - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - *

As of v 2.3, if there is an error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @return The Base64-encoded data as a String - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 1.4 - */ - public static String encodeBytes( byte[] source, int off, int len ) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes( source, off, len, NO_OPTIONS ); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes( source, off, len, options ); - - // Return value according to relevant encoding. - try { - return new String( encoded, PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String( encoded ); - } // end catch - - } // end encodeBytes - - - - - /** - * Similar to {@link #encodeBytes(byte[])} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * - * @param source The data to convert - * @return The Base64-encoded data as a byte[] (of ASCII characters) - * @throws NullPointerException if source array is null - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes( byte[] source ) { - byte[] encoded = null; - try { - encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS ); - } catch( java.io.IOException ex ) { - assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); - } - return encoded; - } - - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { - - if( source == null ){ - throw new NullPointerException( "Cannot serialize a null array." ); - } // end if: null - - if( off < 0 ){ - throw new IllegalArgumentException( "Cannot have negative offset: " + off ); - } // end if: off < 0 - - if( len < 0 ){ - throw new IllegalArgumentException( "Cannot have length offset: " + len ); - } // end if: len < 0 - - if( off + len > source.length ){ - throw new IllegalArgumentException( - String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length)); - } // end if: off < 0 - - - - // Compress? - if( (options & GZIP) != 0 ) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - gzos = new java.util.zip.GZIPOutputStream( b64os ); - - gzos.write( source, off, len ); - gzos.close(); - } // end try - catch( java.io.IOException e ) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) != 0; - - //int len43 = len * 4 / 3; - //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding - if( breakLines ){ - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters - } - byte[] outBuff = new byte[ encLen ]; - - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for( ; d < len2; d+=3, e+=4 ) { - encode3to4( source, d+off, 3, outBuff, e, options ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) - { - outBuff[e+4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if( d < len ) { - encode3to4( source, d+off, len - d, outBuff, e, options ); - e += 4; - } // end if: some padding needed - - - // Only resize array if we didn't guess it right. - if( e <= outBuff.length - 1 ){ - // If breaking lines and the last byte falls right at - // the line length (76 bytes per line), there will be - // one extra byte, and the array will need to be resized. - // Not too bad of an estimate on array size, I'd say. - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff,0, finalOut,0,e); - //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); - return finalOut; - } else { - //System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - - - - -/* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

This is the lowest level of the decoding methods with - * all possible parameters.

- * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException if source or destination arrays are null - * @throws IllegalArgumentException if srcOffset or destOffset are invalid - * or there is not enough room in the array. - * @since 1.3 - */ - private static int decode4to3( - byte[] source, int srcOffset, - byte[] destination, int destOffset, int options ) { - - // Lots of error checking and exception throwing - if( source == null ){ - throw new NullPointerException( "Source array was null." ); - } // end if - if( destination == null ){ - throw new NullPointerException( "Destination array was null." ); - } // end if - if( srcOffset < 0 || srcOffset + 3 >= source.length ){ - throw new IllegalArgumentException( String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) ); - } // end if - if( destOffset < 0 || destOffset +2 >= destination.length ){ - throw new IllegalArgumentException( String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) ); - } // end if - - - byte[] DECODABET = getDecodabet( options ); - - // Example: Dk== - if( source[ srcOffset + 2] == EQUALS_SIGN ) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - return 1; - } - - // Example: DkL= - else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) - | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); - - - destination[ destOffset ] = (byte)( outBuff >> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); - destination[ destOffset + 2 ] = (byte)( outBuff ); - - return 3; - } - } // end decodeToBytes - - - - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 2.3.1 - */ - public static byte[] decode( byte[] source ) - throws java.io.IOException { - byte[] decoded = null; -// try { - decoded = decode( source, 0, source.length, Base64.NO_OPTIONS ); -// } catch( java.io.IOException ex ) { -// assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); -// } - return decoded; - } - - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param options Can specify options such as alphabet type to use - * @return decoded data - * @throws java.io.IOException If bogus characters exist in source data - * @since 1.3 - */ - public static byte[] decode( byte[] source, int off, int len, int options ) - throws java.io.IOException { - - // Lots of error checking and exception throwing - if( source == null ){ - throw new NullPointerException( "Cannot decode null source array." ); - } // end if - if( off < 0 || off + len > source.length ){ - throw new IllegalArgumentException( String.format( - "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) ); - } // end if - - if( len == 0 ){ - return new byte[0]; - }else if( len < 4 ){ - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " + len ); - } // end if - - byte[] DECODABET = getDecodabet( options ); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiDecode = 0; // Special value from DECODABET - - for( i = off; i < off+len; i++ ) { // Loop through source - - sbiDecode = DECODABET[ source[i]&0xFF ]; - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if( sbiDecode >= WHITE_SPACE_ENC ) { - if( sbiDecode >= EQUALS_SIGN_ENC ) { - b4[ b4Posn++ ] = source[i]; // Save non-whitespace - if( b4Posn > 3 ) { // Time to decode? - outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if( source[i] == EQUALS_SIGN ) { - break; - } // end if: equals sign - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else { - // There's a bad input character in the Base64 stream. - throw new java.io.IOException( String.format( - "Bad Base64 input character decimal %d in array position %d", ((int)source[i])&0xFF, i ) ); - } // end else: - } // each input character - - byte[] out = new byte[ outBuffPosn ]; - System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); - return out; - } // end decode - - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @return the decoded data - * @throws java.io.IOException If there is a problem - * @since 1.4 - */ - public static byte[] decode( String s ) throws java.io.IOException { - return decode( s, NO_OPTIONS ); - } - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @param options encode options such as URL_SAFE - * @return the decoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if s is null - * @since 1.4 - */ - public static byte[] decode( String s, int options ) throws java.io.IOException { - - if( s == null ){ - throw new NullPointerException( "Input string was null." ); - } // end if - - byte[] bytes; - try { - bytes = s.getBytes( PREFERRED_ENCODING ); - } // end try - catch( java.io.UnsupportedEncodingException uee ) { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode( bytes, 0, bytes.length, options ); - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - boolean dontGunzip = (options & DONT_GUNZIP) != 0; - if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) { - - int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream( bytes ); - gzis = new java.util.zip.GZIPInputStream( bais ); - - while( ( length = gzis.read( buffer ) ) >= 0 ) { - baos.write(buffer,0,length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch( java.io.IOException e ) { - e.printStackTrace(); - // Just return originally-decoded bytes - } // end catch - finally { - try{ baos.close(); } catch( Exception e ){} - try{ gzis.close(); } catch( Exception e ){} - try{ bais.close(); } catch( Exception e ){} - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * - * @param encodedObject The Base64 data to decode - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 1.5 - */ - public static Object decodeToObject( String encodedObject ) - throws java.io.IOException, java.lang.ClassNotFoundException { - return decodeToObject(encodedObject,NO_OPTIONS,null); - } - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * If loader is not null, it will be the class loader - * used when deserializing. - * - * @param encodedObject The Base64 data to decode - * @param options Various parameters related to decoding - * @param loader Optional class loader to use in deserializing classes. - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 2.3.4 - */ - public static Object decodeToObject( - String encodedObject, int options, final ClassLoader loader ) - throws java.io.IOException, java.lang.ClassNotFoundException { - - // Decode and gunzip if necessary - byte[] objBytes = decode( encodedObject, options ); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try { - bais = new java.io.ByteArrayInputStream( objBytes ); - - // If no custom class loader is provided, use Java's builtin OIS. - if( loader == null ){ - ois = new java.io.ObjectInputStream( bais ); - } // end if: no loader provided - - // Else make a customized object input stream that uses - // the provided class loader. - else { - ois = new java.io.ObjectInputStream(bais){ - @Override - public Class resolveClass(java.io.ObjectStreamClass streamClass) - throws java.io.IOException, ClassNotFoundException { - Class c = Class.forName(streamClass.getName(), false, loader); - if( c == null ){ - return super.resolveClass(streamClass); - } else { - return c; // Class loader knows of this class. - } // end else: not null - } // end resolveClass - }; // end ois - } // end else: no custom class loader - - obj = ois.readObject(); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - catch( java.lang.ClassNotFoundException e ) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - finally { - try{ bais.close(); } catch( Exception e ){} - try{ ois.close(); } catch( Exception e ){} - } // end finally - - return obj; - } // end decodeObject - - - - /** - * Convenience method for encoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToEncode byte array of data to encode in base64 form - * @param filename Filename for saving encoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if dataToEncode is null - * @since 2.1 - */ - public static void encodeToFile( byte[] dataToEncode, String filename ) - throws java.io.IOException { - - if( dataToEncode == null ){ - throw new NullPointerException( "Data to encode was null." ); - } // end iff - - Base64.OutputStream bos = null; - try { - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.ENCODE ); - bos.write( dataToEncode ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - } // end encodeToFile - - - /** - * Convenience method for decoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToDecode Base64-encoded data as a string - * @param filename Filename for saving decoded data - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static void decodeToFile( String dataToDecode, String filename ) - throws java.io.IOException { - - Base64.OutputStream bos = null; - try{ - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.DECODE ); - bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - } // end decodeToFile - - - - - /** - * Convenience method for reading a base64-encoded - * file and decoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading encoded data - * @return decoded byte array - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static byte[] decodeFromFile( String filename ) - throws java.io.IOException { - - byte[] decodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if( file.length() > Integer.MAX_VALUE ) - { - throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." ); - } // end if: file too big for int index - buffer = new byte[ (int)file.length() ]; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.DECODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { - length += numBytes; - } // end while - - // Save in a variable to return - decodedData = new byte[ length ]; - System.arraycopy( buffer, 0, decodedData, 0, length ); - - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return decodedData; - } // end decodeFromFile - - - - /** - * Convenience method for reading a binary file - * and base64-encoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading binary data - * @return base64-encoded string - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static String encodeFromFile( String filename ) - throws java.io.IOException { - - String encodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.ENCODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { - length += numBytes; - } // end while - - // Save in a variable to return - encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); - - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return encodedData; - } // end encodeFromFile - - /** - * Reads infile and encodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void encodeFileToFile( String infile, String outfile ) - throws java.io.IOException { - - String encoded = Base64.encodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output. - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end encodeFileToFile - - - /** - * Reads infile and decodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void decodeFileToFile( String infile, String outfile ) - throws java.io.IOException { - - byte[] decoded = Base64.decodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( decoded ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end decodeFileToFile - - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - - - /** - * A {@link Base64.InputStream} will read data from another - * java.io.InputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class InputStream extends java.io.FilterInputStream { - - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - private byte[] decodabet; // Local copies to avoid extra method calls - - - /** - * Constructs a {@link Base64.InputStream} in DECODE mode. - * - * @param in the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream( java.io.InputStream in ) { - this( in, DECODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.InputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- *

- * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * - * @param in the java.io.InputStream from which to read data. - * @param options Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public InputStream( java.io.InputStream in, int options ) { - - super( in ); - this.options = options; // Record for later - this.breakLines = (options & DO_BREAK_LINES) > 0; - this.encode = (options & ENCODE) > 0; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[ bufferLength ]; - this.position = -1; - this.lineLength = 0; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert - * to/from Base64 and returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException { - - // Do we need to get data? - if( position < 0 ) { - if( encode ) { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for( int i = 0; i < 3; i++ ) { - int b = in.read(); - - // If end of stream, b is -1. - if( b >= 0 ) { - b3[i] = (byte)b; - numBinaryBytes++; - } else { - break; // out of for loop - } // end else: end of stream - - } // end for: each needed input byte - - if( numBinaryBytes > 0 ) { - encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); - position = 0; - numSigBytes = 4; - } // end if: got data - else { - return -1; // Must be end of stream - } // end else - } // end if: encoding - - // Else decoding - else { - byte[] b4 = new byte[4]; - int i = 0; - for( i = 0; i < 4; i++ ) { - // Read four "meaningful" bytes: - int b = 0; - do{ b = in.read(); } - while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); - - if( b < 0 ) { - break; // Reads a -1 if end of stream - } // end if: end of stream - - b4[i] = (byte)b; - } // end for: each needed input byte - - if( i == 4 ) { - numSigBytes = decode4to3( b4, 0, buffer, 0, options ); - position = 0; - } // end if: got four characters - else if( i == 0 ){ - return -1; - } // end else if: also padded correctly - else { - // Must have broken out from above. - throw new java.io.IOException( "Improperly padded Base64 input." ); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if( position >= 0 ) { - // End of relevant data? - if( /*!encode &&*/ position >= numSigBytes ){ - return -1; - } // end if: got data - - if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) { - lineLength = 0; - return '\n'; - } // end if - else { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[ position++ ]; - - if( position >= bufferLength ) { - position = -1; - } // end if: end - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else { - throw new java.io.IOException( "Error in Base64 code reading stream." ); - } // end else - } // end read - - - /** - * Calls {@link #read()} repeatedly until the end of stream - * is reached or len bytes are read. - * Returns number of bytes read into array or -1 if - * end of stream is encountered. - * - * @param dest array to hold values - * @param off offset for array - * @param len max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read( byte[] dest, int off, int len ) - throws java.io.IOException { - int i; - int b; - for( i = 0; i < len; i++ ) { - b = read(); - - if( b >= 0 ) { - dest[off + i] = (byte) b; - } - else if( i == 0 ) { - return -1; - } - else { - break; // Out of 'for' loop - } // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - - - - - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out ) { - this( out, ENCODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.OutputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- *

- * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out, int options ) { - super( out ); - this.breakLines = (options & DO_BREAK_LINES) != 0; - this.encode = (options & ENCODE) != 0; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[ bufferLength ]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) - throws java.io.IOException { - // Encoding suspended? - if( suspendEncoding ) { - this.out.write( theByte ); - return; - } // end if: supsended - - // Encode? - if( encode ) { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) { // Enough to encode. - - this.out.write( encode3to4( b4, buffer, bufferLength, options ) ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) { - this.out.write( NEW_LINE ); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else { - // Meaningful Base64 character? - if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) { // Enough to output. - - int len = Base64.decode4to3( buffer, 0, b4, 0, options ); - out.write( b4, 0, len ); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) { - throw new java.io.IOException( "Invalid character in Base64 data." ); - } // end else: not white space either - } // end else: decoding - } // end write - - - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write( byte[] theBytes, int off, int len ) - throws java.io.IOException { - // Encoding suspended? - if( suspendEncoding ) { - this.out.write( theBytes, off, len ); - return; - } // end if: supsended - - for( int i = 0; i < len; i++ ) { - write( theBytes[ off + i ] ); - } // end for: each byte written - - } // end write - - - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - * @throws java.io.IOException if there's an error. - */ - public void flushBase64() throws java.io.IOException { - if( position > 0 ) { - if( encode ) { - out.write( encode3to4( b4, buffer, position, options ) ); - position = 0; - } // end if: encoding - else { - throw new java.io.IOException( "Base64 input not properly padded." ); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - - - /** - * Suspends encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @throws java.io.IOException if there's an error flushing - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - - /** - * Resumes encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() { - this.suspendEncoding = false; - } // end resumeEncoding - - - - } // end inner class OutputStream - - -} // end class Base64 diff --git a/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java b/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java index 946652e..e47d558 100644 --- a/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java +++ b/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java @@ -14,11 +14,8 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Supplier; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -55,7 +52,7 @@ class GsonJsonMarshallerFactory extends JsonMarshallerFactory implements JsonMar create(); ApiTypes types = Entity.class.getPackage().getAnnotation(ApiTypes.class); - typeClasses = new HashMap>(types.value().length); + typeClasses = new HashMap<>(types.value().length); for (Class clazz : types.value()) { typeClasses.put(clazz.getAnnotation(ApiType.class).value(), clazz); } @@ -93,7 +90,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { return null; } // Obtain all ApiProperty fields and make them accessible... - Map fields = new HashMap(); + Map fields = new HashMap<>(); loadFields(typeClass, fields); return (TypeAdapter) new EntityTypeAdapter((Class) typeClass, fields); } @@ -190,12 +187,12 @@ public Entity read(JsonReader in) throws IOException { // we're an adapter for. So if we have SoftLayer_Something and a newer release of the // API has a type extending it but we don't have a generated class for it, it will get // properly serialized to a SoftLayer_Something. + // If the API returns a type that isn't the same or a subtype of the type class, + // try as best we can to fit the data within the type class. Class clazz = typeClasses.get(apiTypeName); Entity result; - if (clazz == null) { + if (clazz == null || !typeClass.isAssignableFrom(clazz)) { result = readForThisType(in); - } else if (!typeClass.isAssignableFrom(clazz)) { - throw new RuntimeException("Expecting " + typeClass + " to be super type of " + clazz); } else { result = ((EntityTypeAdapter) gson.getAdapter(clazz)).readForThisType(in); } @@ -207,13 +204,11 @@ private Entity readForThisType(JsonReader in) throws IOException { // Begin/end object (and the first "complexType" property) are done outside of here Entity entity; try { - entity = typeClass.newInstance(); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { + entity = typeClass.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } - Map unknownProperties = new HashMap(); + Map unknownProperties = new HashMap<>(); while (in.hasNext()) { String propertyName = in.nextName(); EntityJsonField field = fields.get(propertyName); @@ -245,23 +240,16 @@ static class GregorianCalendarTypeAdapter extends TypeAdapter // can just remove/add the colon as necessary. This is a better solution than using // JAXB libraries. // Ref: http://stackoverflow.com/questions/2201925/converting-iso-8601-compliant-string-to-java-util-date - - final ThreadLocal secondFormat = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); - } - }; + final ThreadLocal secondFormat = ThreadLocal.withInitial( + () -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + ); // Some times come back with fractions of a second all the way down to 6 digits. // Luckily we can just use the presence of a decimal point as a discriminator between // this format and the one above. - final ThreadLocal subSecondFormat = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - } - }; + final ThreadLocal subSecondFormat = ThreadLocal.withInitial( + () -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + ); @Override public void write(JsonWriter out, GregorianCalendar value) throws IOException { @@ -357,7 +345,7 @@ public void write(JsonWriter out, byte[] value) throws IOException { if (value == null) { out.nullValue(); } else { - out.value(Base64.encodeBytes(value)); + out.value(Base64.getEncoder().encodeToString(value)); } } @@ -366,8 +354,8 @@ public byte[] read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; - } - return Base64.decode(in.nextString()); + } + return Base64.getDecoder().decode(in.nextString()); } } @@ -409,7 +397,7 @@ public List read(JsonReader in) throws IOException { // We only take over if it's the beginning of an object, otherwise delegate if (in.peek() == JsonToken.BEGIN_OBJECT) { // Send back a mutable list of 1 - List result = new ArrayList(1); + List result = new ArrayList<>(1); result.add(instanceDelegate.read(in)); return result; } diff --git a/src/main/java/com/softlayer/api/json/JsonMarshaller.java b/src/main/java/com/softlayer/api/json/JsonMarshaller.java index b5f4b0f..0ee3848 100644 --- a/src/main/java/com/softlayer/api/json/JsonMarshaller.java +++ b/src/main/java/com/softlayer/api/json/JsonMarshaller.java @@ -11,8 +11,8 @@ public interface JsonMarshaller { /** Convert the given object to JSON on the stream. The output stream is closed by this marshaller */ - public void toJson(Object object, OutputStream out); + void toJson(Object object, OutputStream out); /** Convert the JSON stream to the given type. The input stream is closed by this marshaller */ - public T fromJson(Type type, InputStream in); + T fromJson(Type type, InputStream in); } diff --git a/src/test/java/com/softlayer/api/MaskTest.java b/src/test/java/com/softlayer/api/MaskTest.java new file mode 100644 index 0000000..928b0be --- /dev/null +++ b/src/test/java/com/softlayer/api/MaskTest.java @@ -0,0 +1,173 @@ +package com.softlayer.api; + +import static org.junit.Assert.*; + +import java.net.URLEncoder; +import java.util.Collections; + +import org.junit.Ignore; +import org.junit.Test; + +import com.softlayer.api.http.FakeHttpClientFactory; +import com.softlayer.api.json.GsonJsonMarshallerFactoryTest; +import com.softlayer.api.service.TestEntity; + +public class MaskTest { + static { + GsonJsonMarshallerFactoryTest.addTestEntityToGson(); + } + + @Test + public void testWithMask() throws Exception { + FakeHttpClientFactory http = new FakeHttpClientFactory(200, + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); + client.setHttpClientFactory(http); + + TestEntity entity = new TestEntity(); + entity.setFoo("blah"); + TestEntity.Service service = TestEntity.service(client); + service.withMask().foo().child().date(); + service.withMask().child().baz(); + + assertEquals("some response", service.doSomethingStatic(123L, entity)); + assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" + + "?objectMask=" + URLEncoder.encode(service.withMask().getMask(), "UTF-8"), http.fullUrl); + assertTrue(http.invokeSyncCalled); + } + + @Test + public void testSetObjectMask() throws Exception { + FakeHttpClientFactory http = new FakeHttpClientFactory(200, + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); + client.setHttpClientFactory(http); + + TestEntity entity = new TestEntity(); + entity.setFoo("blah"); + TestEntity.Service service = TestEntity.service(client); + TestEntity.Mask mask = new TestEntity.Mask(); + mask.foo().child().date(); + mask.child().baz(); + service.setMask(mask); + + assertEquals("some response", service.doSomethingStatic(123L, entity)); + assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" + + "?objectMask=" + URLEncoder.encode(mask.getMask(), "UTF-8"), http.fullUrl); + assertTrue(http.invokeSyncCalled); + } + + @Test + public void testSetStringMask() { + FakeHttpClientFactory http = new FakeHttpClientFactory(200, + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); + client.setHttpClientFactory(http); + + TestEntity entity = new TestEntity(); + entity.setFoo("blah"); + TestEntity.Service service = TestEntity.service(client); + service.setMask("yay-a-mask"); + + assertEquals("some response", service.doSomethingStatic(123L, entity)); + assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" + + "?objectMask=yay-a-mask", http.fullUrl); + assertTrue(http.invokeSyncCalled); + } + + @Test(expected = IllegalArgumentException.class) + public void testMaskMustNotBeNull() { + RestApiClient client = new RestApiClient("http://example.com/"); + TestEntity.Service service = TestEntity.service(client); + service.setMask((Mask) null); + } + + @Test + public void testMaskRemoval() { + RestApiClient client = new RestApiClient("http://example.com/"); + TestEntity.Service service = TestEntity.service(client); + service.withMask().baz(); + assertEquals("baz", service.withMask().toString()); + service.clearMask(); + assertEquals("", service.withMask().toString()); + } + + @Test + public void testRecursiveMaskAndLocal() { + RestApiClient client = new RestApiClient("http://example.com/"); + TestEntity.Service service = TestEntity.service(client); + service.withMask().recursiveProperty().recursiveProperty().baz(); + service.withMask().recursiveProperty().recursiveProperty().foo(); + service.withMask().recursiveProperty().date(); + assertEquals("recursiveProperty[date,recursiveProperty[foo,baz]]", + service.withMask().toString()); + } + + @Test + public void testRecursiveMask() { + RestApiClient client = new RestApiClient("http://example.com/"); + TestEntity.Service service = TestEntity.service(client); + service.withMask().recursiveProperty().baz(); + service.withMask().recursiveProperty().foo(); + service.withMask().recursiveProperty().date(); + + assertEquals("recursiveProperty[date,foo,baz]", + service.withMask().toString()); + } + + @Test + public void testMultiLevelMask() { + FakeHttpClientFactory http = new FakeHttpClientFactory(200, + Collections.emptyMap(),""); + RestApiClient client = new RestApiClient("http://example.com/"); + client.setHttpClientFactory(http); + + TestEntity.Service service = TestEntity.service(client); + service.withMask().recursiveProperty().baz(); + service.withMask().recursiveProperty().foo(); + + service.withMask().moreChildren().recursiveProperty().baz(); + service.withMask().moreChildren().date(); + String result = service.getRecursiveProperty(); + + assertEquals("moreChildren[date,recursiveProperty.baz],recursiveProperty[foo,baz]", + service.withMask().toString()); + } + + @Test + public void testNoChangeMaskScope() { + FakeHttpClientFactory http = new FakeHttpClientFactory(200, + Collections.emptyMap(),""); + RestApiClient client = new RestApiClient("http://example.com/"); + client.setHttpClientFactory(http); + + TestEntity.Service service = TestEntity.service(client); + service.withMask().testThing().id(); + service.withMask().testThing().first(); + + TestEntity result = service.getObject(); + assertEquals("testThing[id,first]", service.withMask().toString()); + String expected = "http://example.com/SoftLayer_TestEntity.json?objectMask=mask%5BtestThing%5Bid%2Cfirst%5D%5D"; + assertEquals(expected, http.fullUrl); + } + + /** + * This doesn't work due to the issues mentioned in https://github.com/softlayer/softlayer-java/issues/19 + */ + @Test + @Ignore + public void testChangeMaskScope() { + RestApiClient client = new RestApiClient("http://example.com/"); + + TestEntity.Service service = TestEntity.service(client); + service.withMask().recursiveProperty().baz(); + service.withMask().recursiveProperty().foo(); + + String result = service.getRecursiveProperty(); + assertEquals("baz,foo", service.withMask().toString()); + } +} + diff --git a/src/test/java/com/softlayer/api/RestApiClientTest.java b/src/test/java/com/softlayer/api/RestApiClientTest.java index a141cb7..e44b7e5 100644 --- a/src/test/java/com/softlayer/api/RestApiClientTest.java +++ b/src/test/java/com/softlayer/api/RestApiClientTest.java @@ -5,10 +5,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.reflect.Proxy; -import java.net.URLEncoder; import java.util.Collections; import java.util.GregorianCalendar; -import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,7 +61,7 @@ public void testGetFullUrl() { client.getFullUrl("SomeService", "deleteObject", null, null, null)); assertEquals("http://example.com/SomeService.json", client.getFullUrl("SomeService", "createObject", null, null, null)); - assertEquals("http://example.com/SomeService.json", + assertEquals("http://example.com/SomeService/createObjects.json", client.getFullUrl("SomeService", "createObjects", null, null, null)); assertEquals("http://example.com/SomeService.json", client.getFullUrl("SomeService", "editObject", null, null, null)); @@ -85,39 +83,38 @@ private String withOutputCaptured(Callable closure) throws Exception { @Test public void testLogRequest() throws Exception { - assertEquals("Running VERB on URL with no body\n", withOutputCaptured(new Callable() { - @Override - public Void call() throws Exception { + assertEquals( + "Running VERB on URL with no body\n", + withOutputCaptured(() -> { new RestApiClient().logRequest("VERB", "URL", new Object[0]); return null; - } - })); - assertEquals("Running VERB on URL with body: {\"parameters\":[{\"key\":\"value\"}]}\n", - withOutputCaptured(new Callable() { - @Override - public Void call() throws Exception { - new RestApiClient().logRequest("VERB", "URL", - new Object[] { Collections.singletonMap("key", "value") }); - return null; - } - })); + }) + ); + assertEquals( + "Running VERB on URL with body: {\"parameters\":[{\"key\":\"value\"}]}\n", + withOutputCaptured(() -> { + new RestApiClient().logRequest("VERB", "URL", + new Object[]{Collections.singletonMap("key", "value")}); + return null; + }) + ); } @Test public void testLogResponse() throws Exception { - assertEquals("Got 123 on URL with body: some body\n", withOutputCaptured(new Callable() { - @Override - public Void call() throws Exception { + assertEquals( + "Got 123 on URL with body: some body\n", + withOutputCaptured(() -> { new RestApiClient().logResponse("URL", 123, "some body"); return null; - } - })); + }) + ); } @Test(expected = IllegalStateException.class) public void testFailedIfCallingNonStaticWithoutId() { FakeHttpClientFactory http = new FakeHttpClientFactory(123, - Collections.>emptyMap(), "some response"); + Collections.emptyMap(), "some response"); RestApiClient client = new RestApiClient("http://example.com/"); client.setHttpClientFactory(http); TestEntity.service(client).doSomethingNonStatic(new GregorianCalendar()); @@ -126,7 +123,7 @@ public void testFailedIfCallingNonStaticWithoutId() { @Test(expected = IllegalStateException.class) public void testFailedIfCallingNonStaticAsyncWithoutId() { FakeHttpClientFactory http = new FakeHttpClientFactory(123, - Collections.>emptyMap(), "some response"); + Collections.emptyMap(), "some response"); RestApiClient client = new RestApiClient("http://example.com/"); client.setHttpClientFactory(http); TestEntity.service(client).asAsync().doSomethingNonStatic(new GregorianCalendar()); @@ -135,7 +132,7 @@ public void testFailedIfCallingNonStaticAsyncWithoutId() { @Test public void testSuccess() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); + Collections.emptyMap(), "\"some response\""); RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); client.setHttpClientFactory(http); TestEntity entity = new TestEntity(); @@ -154,9 +151,10 @@ public void testSuccess() throws Exception { @Test public void testBadRequestFailure() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(ApiException.BadRequest.STATUS, - Collections.>emptyMap(), + Collections.emptyMap(), "{\"error\": \"some error\", \"code\": \"some code\"}"); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); TestEntity entity = new TestEntity(); entity.setFoo("blah"); @@ -172,11 +170,14 @@ public void testBadRequestFailure() throws Exception { @Test public void testAsyncFutureSuccess() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); + assertEquals("some response", TestEntity.service(client).asAsync().doSomethingStatic(123L, entity).get()); assertEquals("user", ((HttpBasicAuthCredentials) http.credentials).username); assertEquals("key", ((HttpBasicAuthCredentials) http.credentials).apiKey); @@ -191,10 +192,12 @@ public void testAsyncFutureSuccess() throws Exception { @Test public void testAsyncFutureFailure() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(ApiException.BadRequest.STATUS, - Collections.>emptyMap(), + Collections.emptyMap(), "{\"error\": \"some error\", \"code\": \"some code\"}"); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); try { @@ -209,9 +212,11 @@ public void testAsyncFutureFailure() throws Exception { @Test public void testAsyncCallbackSuccess() throws Exception { final FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); final AtomicBoolean successCalled = new AtomicBoolean(); @@ -227,6 +232,7 @@ public void onSuccess(String value) { successCalled.set(true); } }).get(); + assertEquals("user", ((HttpBasicAuthCredentials) http.credentials).username); assertEquals("key", ((HttpBasicAuthCredentials) http.credentials).apiKey); assertEquals("GET", http.method); @@ -241,10 +247,12 @@ public void onSuccess(String value) { @Test public void testAsyncCallbackFailure() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(ApiException.BadRequest.STATUS, - Collections.>emptyMap(), + Collections.emptyMap(), "{\"error\": \"some error\", \"code\": \"some code\"}"); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); final AtomicBoolean errorCalled = new AtomicBoolean(); @@ -267,19 +275,19 @@ public void onSuccess(String value) { @Test public void testCallWithLog() throws Exception { final FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - String output = withOutputCaptured(new Callable() { - @Override - public Void call() throws Exception { - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"). - withLoggingEnabled(); + Collections.emptyMap(), "\"some response\""); + String output = withOutputCaptured(() -> { + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key") + .withLoggingEnabled(); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); TestEntity.service(client).doSomethingStatic(123L, entity); return null; - } }); + assertTrue(http.invokeSyncCalled); assertEquals( "Running GET on http://example.com/SoftLayer_TestEntity/doSomethingStatic.json with body: " @@ -292,78 +300,31 @@ public Void call() throws Exception { @Test public void testDifferentMethodName() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "[]"); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + Collections.emptyMap(), "[]"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + assertEquals(Collections.emptyList(), TestEntity.service(client).fakeName()); assertEquals("http://example.com/SoftLayer_TestEntity/actualName.json", http.fullUrl); assertNull(http.outStream); assertEquals(RestApiClient.HEADERS, http.headers); assertTrue(http.invokeSyncCalled); } - - @Test - public void testWithMask() throws Exception { - FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); - client.setHttpClientFactory(http); - TestEntity entity = new TestEntity(); - entity.setFoo("blah"); - TestEntity.Service service = TestEntity.service(client); - service.withMask().foo().child().date(); - service.withMask().child().baz(); - assertEquals("some response", service.doSomethingStatic(123L, entity)); - assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" - + "?objectMask=" + URLEncoder.encode(service.withMask().getMask(), "UTF-8"), http.fullUrl); - assertTrue(http.invokeSyncCalled); - } - - @Test - public void testSetObjectMask() throws Exception { - FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); - client.setHttpClientFactory(http); - TestEntity entity = new TestEntity(); - entity.setFoo("blah"); - TestEntity.Service service = TestEntity.service(client); - TestEntity.Mask mask = new TestEntity.Mask(); - mask.foo().child().date(); - mask.child().baz(); - service.setMask(mask); - assertEquals("some response", service.doSomethingStatic(123L, entity)); - assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" - + "?objectMask=" + URLEncoder.encode(mask.getMask(), "UTF-8"), http.fullUrl); - assertTrue(http.invokeSyncCalled); - } - - @Test - public void testSetStringMask() throws Exception { - FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); - client.setHttpClientFactory(http); - TestEntity entity = new TestEntity(); - entity.setFoo("blah"); - TestEntity.Service service = TestEntity.service(client); - service.setMask("yay-a-mask"); - assertEquals("some response", service.doSomethingStatic(123L, entity)); - assertEquals("http://example.com/SoftLayer_TestEntity/doSomethingStatic.json" - + "?objectMask=yay-a-mask", http.fullUrl); - assertTrue(http.invokeSyncCalled); - } - + @Test public void testWithResultLimit() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, - Collections.>emptyMap(), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + Collections.emptyMap(), "\"some response\""); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); TestEntity.Service service = TestEntity.service(client); service.setResultLimit(new ResultLimit(1, 2)); + assertEquals(1, service.getResultLimit().offset); assertEquals(2, service.getResultLimit().limit); assertEquals("some response", service.doSomethingStatic(123L, entity)); @@ -377,11 +338,14 @@ public void testWithTotalItemsResponseHeader() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, Collections.singletonMap("SoftLayer-Total-Items", Collections.singletonList("234")), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); TestEntity.Service service = TestEntity.service(client); + assertEquals("some response", service.doSomethingStatic(123L, entity)); assertTrue(http.invokeSyncCalled); assertEquals(234, service.getLastResponseTotalItemCount().intValue()); @@ -392,11 +356,14 @@ public void testWithTotalItemsAsyncFutureResponseHeader() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, Collections.singletonMap("SoftLayer-Total-Items", Collections.singletonList("234")), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); + TestEntity entity = new TestEntity(); entity.setFoo("blah"); TestEntity.ServiceAsync service = TestEntity.service(client).asAsync(); + assertEquals("some response", service.doSomethingStatic(123L, entity).get()); assertTrue(http.invokeAsyncFutureCalled); assertEquals(234, service.getLastResponseTotalItemCount().intValue()); @@ -407,7 +374,8 @@ public void testWithTotalItemsAsyncCallbackResponseHeader() throws Exception { FakeHttpClientFactory http = new FakeHttpClientFactory(200, Collections.singletonMap("SoftLayer-Total-Items", Collections.singletonList("234")), "\"some response\""); - RestApiClient client = new RestApiClient("http://example.com/").withCredentials("user", "key"); + RestApiClient client = new RestApiClient("http://example.com/") + .withCredentials("user", "key"); client.setHttpClientFactory(http); TestEntity entity = new TestEntity(); entity.setFoo("blah"); @@ -430,23 +398,6 @@ public void onSuccess(String value) { assertTrue(successCalled.get()); } - @Test(expected = IllegalArgumentException.class) - public void testMaskMustNotBeNull() { - RestApiClient client = new RestApiClient("http://example.com/"); - TestEntity.Service service = TestEntity.service(client); - service.setMask((Mask) null); - } - - @Test - public void testMaskRemoval() { - RestApiClient client = new RestApiClient("http://example.com/"); - TestEntity.Service service = TestEntity.service(client); - service.withMask().baz(); - assertEquals("baz", service.withMask().toString()); - service.clearMask(); - assertEquals("", service.withMask().toString()); - } - @Test public void testNormalObjectMethodsOnService() { RestApiClient client = new RestApiClient("http://example.com/"); diff --git a/src/test/java/com/softlayer/api/ResultLimitTest.java b/src/test/java/com/softlayer/api/ResultLimitTest.java new file mode 100644 index 0000000..e53d921 --- /dev/null +++ b/src/test/java/com/softlayer/api/ResultLimitTest.java @@ -0,0 +1,26 @@ +package com.softlayer.api; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class ResultLimitTest { + @Test + public void testConstructorWithLimit() + { + int limit = 123; + ResultLimit resultLimit = new ResultLimit(limit); + assertEquals(0, resultLimit.offset); + assertEquals(limit, resultLimit.limit); + } + + @Test + public void testConstructorWithOffsetAndLimit() + { + int limit = 456; + int offset = 789; + ResultLimit resultLimit = new ResultLimit(offset, limit); + assertEquals(offset, resultLimit.offset); + assertEquals(limit, resultLimit.limit); + } +} diff --git a/src/test/java/com/softlayer/api/http/BuiltInHttpClientFactoryTest.java b/src/test/java/com/softlayer/api/http/BuiltInHttpClientFactoryTest.java index 786c604..15a355c 100644 --- a/src/test/java/com/softlayer/api/http/BuiltInHttpClientFactoryTest.java +++ b/src/test/java/com/softlayer/api/http/BuiltInHttpClientFactoryTest.java @@ -5,7 +5,6 @@ import java.net.HttpURLConnection; import java.util.Collections; -import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -16,28 +15,22 @@ public class BuiltInHttpClientFactoryTest { - @Test - public void testGetHttpClientWithoutBasicAuth() { - try { - new BuiltInHttpClientFactory().getHttpClient(new HttpCredentials() { }, "GET", - "http://example.com", Collections.>emptyMap()); - fail(); - } catch (UnsupportedOperationException e) { - assertTrue(e.getMessage().contains("basic auth")); - } - } - @Test public void testGetThreadPoolDefaultsToDaemonThreads() throws Exception { - boolean daemon = new BuiltInHttpClientFactory().getThreadPool().submit(new Callable() { - @Override - public Boolean call() throws Exception { - return Thread.currentThread().isDaemon(); - } - }).get(); + boolean daemon = new BuiltInHttpClientFactory().getThreadPool().submit( + () -> Thread.currentThread().isDaemon() + ).get(); assertTrue(daemon); } + @Test + public void testGetThreadPoolLazyLoading() { + BuiltInHttpClientFactory factory = new BuiltInHttpClientFactory(); + ExecutorService threadPool = factory.getThreadPool(); + assertNotNull(threadPool); + assertEquals(threadPool, factory.getThreadPool()); + } + @Test public void testSetThreadPoolShutsDownNonUserDefined() { BuiltInHttpClientFactory factory = new BuiltInHttpClientFactory(); @@ -60,9 +53,14 @@ public void testSetThreadPoolDoesNotShutDownUserDefined() { @Test public void testInvokeSyncSetsUpProperly() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "NOTGET", "http://example.com", - Collections.singletonMap("header", Collections.singletonList("some header value")))); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "NOTGET", + "http://example.com", + Collections.singletonMap("header", Collections.singletonList("some header value")) + ) + ); client.connection = mock(HttpURLConnection.class); // Skip opening connection doNothing().when(client).openConnection(); @@ -82,9 +80,14 @@ public void testInvokeSyncSetsUpProperly() throws Exception { @Test public void testInvokeAsyncFutureResult() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "GET", "http://example.com", - Collections.>emptyMap())); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "GET", + "http://example.com", + Collections.emptyMap() + ) + ); Callable callable = mock(Callable.class); doReturn(client).when(client).invokeSync(callable); assertEquals(client, client.invokeAsync(callable).get()); @@ -94,9 +97,14 @@ public void testInvokeAsyncFutureResult() throws Exception { @Test @SuppressWarnings("unchecked") public void testInvokeAsyncCallbackSuccess() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "GET", "http://example.com", - Collections.>emptyMap())); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "GET", + "http://example.com", + Collections.emptyMap() + ) + ); Callable callable = mock(Callable.class); doReturn(client).when(client).invokeSync(callable); ResponseHandler handler = mock(ResponseHandler.class); @@ -109,9 +117,14 @@ public void testInvokeAsyncCallbackSuccess() throws Exception { @Test @SuppressWarnings("unchecked") public void testInvokeAsyncCallbackError() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "GET", "http://example.com", - Collections.>emptyMap())); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "GET", + "http://example.com", + Collections.emptyMap() + ) + ); Callable callable = mock(Callable.class); doThrow(RuntimeException.class).when(client).invokeSync(callable); ResponseHandler handler = mock(ResponseHandler.class); @@ -123,9 +136,14 @@ public void testInvokeAsyncCallbackError() throws Exception { @Test public void testGetInputStreamOnSuccess() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "GET", "http://example.com", - Collections.>emptyMap())); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "GET", + "http://example.com", + Collections.emptyMap() + ) + ); client.connection = mock(HttpURLConnection.class); when(client.connection.getResponseCode()).thenReturn(250); client.getInputStream(); @@ -135,9 +153,14 @@ public void testGetInputStreamOnSuccess() throws Exception { @Test public void testGetErrorStreamOnFailure() throws Exception { - BuiltInHttpClient client = spy(new BuiltInHttpClientFactory().getHttpClient( - new HttpBasicAuthCredentials("some user", "some key"), "GET", "http://example.com", - Collections.>emptyMap())); + BuiltInHttpClient client = spy( + new BuiltInHttpClientFactory().getHttpClient( + new HttpBasicAuthCredentials("some user", "some key"), + "GET", + "http://example.com", + Collections.emptyMap() + ) + ); client.connection = mock(HttpURLConnection.class); when(client.connection.getResponseCode()).thenReturn(450); client.getInputStream(); diff --git a/src/test/java/com/softlayer/api/http/HttpBasicAuthCredentialsTest.java b/src/test/java/com/softlayer/api/http/HttpBasicAuthCredentialsTest.java new file mode 100644 index 0000000..051523e --- /dev/null +++ b/src/test/java/com/softlayer/api/http/HttpBasicAuthCredentialsTest.java @@ -0,0 +1,21 @@ +package com.softlayer.api.http; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class HttpBasicAuthCredentialsTest { + + @Test + public void testConstructor() { + HttpBasicAuthCredentials authCredentials = new HttpBasicAuthCredentials("username", "apiKey"); + assertEquals("username", authCredentials.username); + assertEquals("apiKey", authCredentials.apiKey); + } + + @Test + public void testGetHeader() { + HttpBasicAuthCredentials authCredentials = new HttpBasicAuthCredentials("username", "apiKey"); + assertEquals("Basic dXNlcm5hbWU6YXBpS2V5", authCredentials.getHeader()); + } +} diff --git a/src/test/java/com/softlayer/api/http/HttpBearerCredentialsTest.java b/src/test/java/com/softlayer/api/http/HttpBearerCredentialsTest.java new file mode 100644 index 0000000..0a1d67d --- /dev/null +++ b/src/test/java/com/softlayer/api/http/HttpBearerCredentialsTest.java @@ -0,0 +1,22 @@ +package com.softlayer.api.http; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class HttpBearerCredentialsTest { + + public final String bearerToken = "qqqqwwwweeerrttyyuuiiooppasddfgfgjghjkjklZXxcvcvbvbnnbm"; + @Test + public void testConstructor() { + HttpBearerCredentials authCredentials = new HttpBearerCredentials(bearerToken); + assertEquals(bearerToken, authCredentials.token); + } + + @Test + public void testGetHeader() { + HttpBearerCredentials authCredentials = new HttpBearerCredentials(bearerToken); + String header = "Bearer " + bearerToken; + assertEquals(header, authCredentials.getHeader()); + } +} diff --git a/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java b/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java index c10392e..5233db7 100644 --- a/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java +++ b/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java @@ -21,6 +21,7 @@ import com.google.gson.reflect.TypeToken; import com.softlayer.api.service.Entity; import com.softlayer.api.service.TestEntity; +import com.softlayer.api.service.TestThing; public class GsonJsonMarshallerFactoryTest { @@ -51,21 +52,22 @@ private String toJson(Object obj) throws Exception { public void testRead() throws Exception { Entity entity = fromJson(Entity.class, "{" - + "\"complexType\": \"SoftLayer_TestEntity\"," - + "\"bar\": \"some string\"," - + "\"foo\": \"another string\"," - + "\"baz\": null," - + "\"date\": \"1984-02-25T20:15:25-06:00\"," - + "\"notApiProperty\": \"bad value\"," - + "\"child\": {" - + " \"complexType\": \"SoftLayer_TestEntity\"," - + " \"bar\": \"child string\"" - + "}," - + "\"moreChildren\": [" - + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 1\" }," - + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 2\" }" - + "]" - + "}"); + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"bar\": \"some string\"," + + "\"foo\": \"another string\"," + + "\"baz\": null," + + "\"date\": \"1984-02-25T20:15:25-06:00\"," + + "\"notApiProperty\": \"bad value\"," + + "\"child\": {" + + " \"complexType\": \"SoftLayer_TestEntity\"," + + " \"bar\": \"child string\"" + + "}," + + "\"moreChildren\": [" + + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 1\" }," + + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 2\" }" + + "]," + + "\"testThing\": {\"complexType\": \"SoftLayer_TestThing\", \"id\": 123}" + + "}"); assertEquals(TestEntity.class, entity.getClass()); TestEntity obj = (TestEntity) entity; assertEquals("some string", obj.getFoo()); @@ -80,10 +82,74 @@ public void testRead() throws Exception { assertEquals("bad value", obj.getUnknownProperties().get("notApiProperty")); assertEquals("child string", obj.getChild().getFoo()); assertNull(obj.getChild().getBaz()); + assertEquals(0, obj.getChild().getUnknownProperties().size()); assertFalse(obj.getChild().isBazSpecified()); assertEquals(2, obj.getMoreChildren().size()); assertEquals("child 1", obj.getMoreChildren().get(0).getFoo()); assertEquals("child 2", obj.getMoreChildren().get(1).getFoo()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + } + + @Test + public void testReadPropertyWithIncorrectComplexTypeCoercesTheType() throws Exception { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"complexType\": \"SoftLayer_TestEntity\", \"id\": 123, \"foo\": \"unknown!\"}" + + "}"); + assertEquals(TestEntity.class, entity.getClass()); + TestEntity obj = (TestEntity) entity; + assertEquals(0, obj.getUnknownProperties().size()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + assertEquals(1, obj.getTestThing().getUnknownProperties().size()); + assertEquals("unknown!", obj.getTestThing().getUnknownProperties().get("foo")); + } + + + @Test + public void testReadPropertyWithUnknownComplexTypeCoercesTheType() throws Exception { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"complexType\": \"WhoKnows\", \"id\": 123, \"foo\": \"unknown!\"}" + + "}"); + assertEquals(TestEntity.class, entity.getClass()); + TestEntity obj = (TestEntity) entity; + assertEquals(0, obj.getUnknownProperties().size()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + assertEquals(1, obj.getTestThing().getUnknownProperties().size()); + assertEquals("unknown!", obj.getTestThing().getUnknownProperties().get("foo")); + } + + @Test + public void testReadPropertyThrowsExceptionWithoutComplexType() { + + Exception e = assertThrows(RuntimeException.class, () -> { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"id\": 123}" + + "}"); + }); + + assertEquals("Expected 'complexType' as first property", e.getMessage()); + } + + @Test + public void testReadPropertyThrowsExceptioWithComplexTypeNotFirst() { + + Exception e = assertThrows(RuntimeException.class, () -> { + Entity entity = fromJson(Entity.class, + "{" + + "\"testThing\": {\"id\": 123}," + + "\"complexType\": \"SoftLayer_TestEntity\"" + + "}"); + }); + + assertEquals("Expected 'complexType' as first property", e.getMessage()); } @Test @@ -102,7 +168,7 @@ public void testWrite() throws Exception { obj.getMoreChildren().get(1).setFoo("child 2"); Map actual = fromJson(new TypeToken>() { }.getType(), toJson(obj)); - Map expected = new HashMap(6); + Map expected = new HashMap<>(6); expected.put("complexType", "SoftLayer_TestEntity"); expected.put("bar", "some string"); expected.put("baz", null); @@ -111,14 +177,14 @@ public void testWrite() throws Exception { (offsetMinutes < 0 ? '-' : '+') + String.format("%1$02d:%2$02d", Math.abs(offsetMinutes / 60), Math.abs(offsetMinutes % 60)); expected.put("date", "1984-02-25T20:15:25" + expectedTimeZone); - Map childMap = new HashMap(); + Map childMap = new HashMap<>(); childMap.put("complexType", "SoftLayer_TestEntity"); childMap.put("bar", "child string"); expected.put("child", childMap); - Map child1Map = new HashMap(); + Map child1Map = new HashMap<>(); child1Map.put("complexType", "SoftLayer_TestEntity"); child1Map.put("bar", "child 1"); - Map child2Map = new HashMap(); + Map child2Map = new HashMap<>(); child2Map.put("complexType", "SoftLayer_TestEntity"); child2Map.put("bar", "child 2"); expected.put("moreChildren", Arrays.asList(child1Map, child2Map)); @@ -128,7 +194,14 @@ public void testWrite() throws Exception { @Test public void testReadBothDateFormats() throws Exception { String regular = "\"1984-02-25T20:15:25-06:00\""; - Calendar expected = new GregorianCalendar(1984, Calendar.FEBRUARY, 25, 20, 15, 25); + Calendar expected = new GregorianCalendar( + 1984, + Calendar.FEBRUARY, + 25, + 20, + 15, + 25 + ); expected.setTimeZone(TimeZone.getTimeZone("GMT-06:00")); assertEquals(expected.getTimeInMillis(), fromJson(GregorianCalendar.class, regular).getTimeInMillis()); diff --git a/src/test/java/com/softlayer/api/service/TestEntity.java b/src/test/java/com/softlayer/api/service/TestEntity.java index 66411e2..df3f7d8 100644 --- a/src/test/java/com/softlayer/api/service/TestEntity.java +++ b/src/test/java/com/softlayer/api/service/TestEntity.java @@ -5,16 +5,16 @@ import java.util.List; import java.util.concurrent.Future; -import com.softlayer.api.ApiClient; -import com.softlayer.api.ResponseHandler; import com.softlayer.api.annotation.ApiMethod; import com.softlayer.api.annotation.ApiProperty; import com.softlayer.api.annotation.ApiService; import com.softlayer.api.annotation.ApiType; +import com.softlayer.api.ApiClient; +import com.softlayer.api.ResponseHandler; @ApiType("SoftLayer_TestEntity") public class TestEntity extends Entity { - + @ApiProperty(canBeNullOrNotSet = true) protected Long id; @@ -37,75 +37,75 @@ public void unsetId() { id = null; idSpecified = false; } - + @ApiProperty("bar") protected String foo; - + public String getFoo() { return foo; } - + public void setFoo(String foo) { this.foo = foo; } - + @ApiProperty(canBeNullOrNotSet = true) protected String baz; protected boolean bazSpecified; - + public String getBaz() { return baz; } - + public void setBaz(String baz) { bazSpecified = true; this.baz = baz; } - + public boolean isBazSpecified() { return bazSpecified; } - + public void unsetBaz() { baz = null; bazSpecified = false; } - + @ApiProperty protected GregorianCalendar date; - + public GregorianCalendar getDate() { return date; } - + public void setDate(GregorianCalendar date) { this.date = date; } - + protected String notApiProperty; - + public String getNotApiProperty() { return notApiProperty; } - + public void setNotApiProperty(String notApiProperty) { this.notApiProperty = notApiProperty; } - + @ApiProperty protected TestEntity child; - + public TestEntity getChild() { return child; } - + public void setChild(TestEntity child) { this.child = child; } - + @ApiProperty protected List moreChildren; - + public List getMoreChildren() { if (moreChildren == null) { moreChildren = new ArrayList(); @@ -113,6 +113,26 @@ public List getMoreChildren() { return moreChildren; } + @ApiProperty + protected List recursiveProperty; + + public List getRecursiveProperty() { + if (recursiveProperty == null) { + recursiveProperty = new ArrayList(); + } + return recursiveProperty; + } + + @ApiProperty + protected TestThing testThing; + + public TestThing getTestThing() { + if (testThing == null) { + testThing = new TestThing(); + } + return testThing; + } + public Service asService(ApiClient client) { return service(client, id); } @@ -124,64 +144,89 @@ public static Service service(ApiClient client) { public static Service service(ApiClient client, Long id) { return client.createService(Service.class, id == null ? null : id.toString()); } - + @ApiService("SoftLayer_TestEntity") public static interface Service extends com.softlayer.api.Service { public ServiceAsync asAsync(); + public Mask withNewMask(); + public Mask withMask(); + public void setMask(Mask mask); - + @ApiMethod public String doSomethingStatic(Long param1, TestEntity param2); - + @ApiMethod("actualName") public List fakeName(); - + @ApiMethod(instanceRequired = true) public Void doSomethingNonStatic(GregorianCalendar param1); + + @ApiMethod("getRecursiveProperty") + public String getRecursiveProperty(); + + @ApiMethod("getObject") + public TestEntity getObject(); + + @ApiMethod("getTestThing") + public TestThing getTestThing(); } - + public static interface ServiceAsync extends com.softlayer.api.ServiceAsync { public Mask withNewMask(); + public Mask withMask(); + public void setMask(Mask mask); - + public Future doSomethingStatic(Long param1, TestEntity param2); + public Future doSomethingStatic(Long param1, TestEntity param2, ResponseHandler handler); - + public Future fakeName(); + public Future fakeName(ResponseHandler handler); - + public Future doSomethingNonStatic(GregorianCalendar param1); + public Future doSomethingNonStatic(GregorianCalendar param1, ResponseHandler handler); } - + public static class Mask extends Entity.Mask { - + public Mask foo() { - withLocalProperty("bar"); + withLocalProperty("foo"); return this; } - + public Mask baz() { withLocalProperty("baz"); return this; } - + public Mask date() { withLocalProperty("date"); return this; } - + public Mask child() { return withSubMask("child", Mask.class); } - + public Mask moreChildren() { return withSubMask("moreChildren", Mask.class); } + + public Mask recursiveProperty() { + return withSubMask("recursiveProperty", com.softlayer.api.service.TestEntity.Mask.class); + } + + public TestThing.Mask testThing() { + return withSubMask("testThing", com.softlayer.api.service.TestThing.Mask.class); + } } -} \ No newline at end of file +} diff --git a/src/test/java/com/softlayer/api/service/TestThing.java b/src/test/java/com/softlayer/api/service/TestThing.java new file mode 100644 index 0000000..548b87c --- /dev/null +++ b/src/test/java/com/softlayer/api/service/TestThing.java @@ -0,0 +1,139 @@ +package com.softlayer.api.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import com.softlayer.api.ApiClient; +import com.softlayer.api.ResponseHandler; +import com.softlayer.api.ResultLimit; +import com.softlayer.api.annotation.ApiMethod; +import com.softlayer.api.annotation.ApiProperty; +import com.softlayer.api.annotation.ApiService; +import com.softlayer.api.annotation.ApiType; + +@ApiType("SoftLayer_TestThing") +public class TestThing extends Entity { + + @ApiProperty(canBeNullOrNotSet = true) + protected Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + idSpecified = true; + this.id = id; + } + + protected boolean idSpecified; + + public boolean isIdSpecified() { + return idSpecified; + } + + public void unsetId() { + id = null; + idSpecified = false; + } + + @ApiProperty("first") + protected String first; + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + @ApiProperty("second") + protected String second; + + public String getSecond() { + return second; + } + + public void setSecond(String second) { + this.second = second; + } + + @ApiProperty("testEntity") + protected List testEntity; + + public List getTestEntity() { + if (testEntity == null) { + testEntity = new ArrayList(); + } + return testEntity; + } + + public Service asService(ApiClient client) { + return service(client, id); + } + + public static Service service(ApiClient client) { + return client.createService(Service.class, null); + } + + public static Service service(ApiClient client, Long id) { + return client.createService(Service.class, id == null ? null : id.toString()); + } + + @ApiService("SoftLayer_TestThing") + public static interface Service extends com.softlayer.api.Service { + public ServiceAsync asAsync(); + + public Mask withNewMask(); + + public Mask withMask(); + + public void setMask(Mask mask); + + @ApiMethod("getObject") + public TestThing getObject(); + + @ApiMethod("getTestEntity") + public List getTestEntity(); + } + + public static interface ServiceAsync extends com.softlayer.api.ServiceAsync { + public Mask withNewMask(); + + public Mask withMask(); + + public void setMask(Mask mask); + + public Future getObject(); + + public Future getObject(ResponseHandler handler); + + public Future> getTestEntity(); + + public Future getTestEntity(ResponseHandler> handler); + } + + public static class Mask extends Entity.Mask { + + public Mask id() { + withLocalProperty("id"); + return this; + } + + public Mask first() { + withLocalProperty("first"); + return this; + } + + public Mask second() { + withLocalProperty("second"); + return this; + } + + public Mask testEntity() { + return withSubMask("testEntity", Mask.class); + } + } +}