commit 664252827e905a04aeac535059f340993ce99401 Author: etorres Date: Sat Dec 19 13:04:16 2015 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76729a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ + +# Created by https://www.gitignore.io/api/eclipse,java,maven + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + + +### Java ### +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d7ee87 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Core Utils +Utilities to facilitate Java applications development. + +## Master Build Status + +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/grycap/coreutils/blob/master/LICENSE) +[![Build Status](https://api.travis-ci.org/grycap/coreutils.svg)](https://travis-ci.org/grycap/coreutils/builds) +[![Coverage Status](https://coveralls.io/repos/grycap/coreutils/badge.svg?branch=master&service=github)](https://coveralls.io/github/grycap/coreutils?branch=master) + +## Environment variables + +``GRYCAP_TESTS_PRINT_OUTPUT`` set value to ``true`` to print tests output. + +## Installation + +### Install from source + +``$ mvn clean install coreutils`` + +## Development + +### Run all tests logging to the console + +``$ mvn clean verify -pl coreutils-core -Dgrycap.tests.print.out=true |& tee /tmp/LOGFILE`` + +### Run functional and sanity tests logging to the console + +``$ mvn clean test -pl coreutils-core -Dgrycap.tests.print.out=true |& tee /tmp/LOGFILE`` + +## Continuous integration + +``$ mvn clean verify coreutils`` diff --git a/coreutils-common/.gitignore b/coreutils-common/.gitignore new file mode 100644 index 0000000..76729a5 --- /dev/null +++ b/coreutils-common/.gitignore @@ -0,0 +1,74 @@ + +# Created by https://www.gitignore.io/api/eclipse,java,maven + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + + +### Java ### +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + diff --git a/coreutils-common/pom.xml b/coreutils-common/pom.xml new file mode 100644 index 0000000..264b67e --- /dev/null +++ b/coreutils-common/pom.xml @@ -0,0 +1,69 @@ + + + + 4.0.0 + + org.grycap.coreutils + coreutils + 0.1.0 + + + coreutils-common + Common base utilities + Provides utilities to simplify common development tasks + https://github.com/grycap/coreutils + + + + + org.grycap.coreutils + coreutils-test + ${org.grycap.coreutils.version} + test + + + + + com.google.code.findbugs + jsr305 + + + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + + + + + com.typesafe + config + + + \ No newline at end of file diff --git a/coreutils-common/src/main/java/org/grycap/coreutils/common/Configurer.java b/coreutils-common/src/main/java/org/grycap/coreutils/common/Configurer.java new file mode 100644 index 0000000..783e7a0 --- /dev/null +++ b/coreutils-common/src/main/java/org/grycap/coreutils/common/Configurer.java @@ -0,0 +1,68 @@ +/* + * Core Utils - Common Utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.common; + +import static org.apache.commons.lang3.StringUtils.trimToNull; + +import java.io.File; + +import javax.annotation.Nullable; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigParseOptions; + +/** + * Configuration loader. + * @author Erik Torres + * @since @since 0.1.0 + */ +public class Configurer { + + /** + * Loads and merges application configuration with default properties. + * @param confname - optional configuration filename + * @param rootPath - only load configuration properties underneath this path that this code + * module owns and understands + * @return Configuration loaded from the provided filename or from default properties. + */ + public Config loadConfig(final @Nullable String confname, final String rootPath) { + // load configuration properties + Config config; + final String confname2 = trimToNull(confname); + if (confname2 != null) { + final ConfigParseOptions options = ConfigParseOptions.defaults().setAllowMissing(false); + final Config customConfig = ConfigFactory.parseFileAnySyntax(new File(confname2), options); + final Config regularConfig = ConfigFactory.load(); + final Config combined = customConfig.withFallback(regularConfig); + config = ConfigFactory.load(combined); + } else { + config = ConfigFactory.load(); + } + // validate + config.checkValid(ConfigFactory.defaultReference(), rootPath); + return config; + } + +} \ No newline at end of file diff --git a/coreutils-fiber/.gitignore b/coreutils-fiber/.gitignore new file mode 100644 index 0000000..76729a5 --- /dev/null +++ b/coreutils-fiber/.gitignore @@ -0,0 +1,74 @@ + +# Created by https://www.gitignore.io/api/eclipse,java,maven + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + + +### Java ### +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + diff --git a/coreutils-fiber/pom.xml b/coreutils-fiber/pom.xml new file mode 100644 index 0000000..64a40a7 --- /dev/null +++ b/coreutils-fiber/pom.xml @@ -0,0 +1,276 @@ + + + + 4.0.0 + + + org.grycap.coreutils + coreutils + 0.1.0 + + + coreutils-fiber + Common clients integrated with green threads (fibers) + Provides commonly-used HTTP, REST and other clients, integrated with with green threads (fibers) to minimize the performance impact in virtual environments + https://github.com/grycap/coreutils + + + + + org.grycap.coreutils + coreutils-logging + ${org.grycap.coreutils.version} + + + org.grycap.coreutils + coreutils-test + ${org.grycap.coreutils.version} + test + + + + + co.paralleluniverse + quasar-core + jdk8 + + + co.paralleluniverse + comsat-httpclient + + + co.paralleluniverse + comsat-okhttp + + + + + net.jodah + concurrentunit + test + + + + + org.mock-server + mockserver-integration-testing + test + + + ch.qos.logback + logback-classic + + + + + + + + silent + + true + + + + verbose + + + grycap.tests.print.out + true + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.testOutputDirectory}/logback-test-tmpfile.xml + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + ${project.build.testOutputDirectory}/logback-test-tmpfile.xml + + + + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + ${m2e.lifecycle-mapping.version} + + + + + + org.apache.maven.plugins + maven-antrun-plugin + [${maven-antrun-plugin.version},) + + run + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + [${maven-dependency-plugin.version},) + + properties + + + + + + + + + org.mock-server + mockserver-maven-plugin + [${mockserver-maven-plugin.version},) + + start + stop + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven-antrun-plugin.version} + + + instrument-classes + compile + + + + + + + + + + + + + run + + + + instrument-test-classes + test-compile + + + + + + + + + + + + + run + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + getClasspathFilenames + + properties + + + + + + + org.mock-server + mockserver-maven-plugin + + org.grycap.coreutils.fiber.test.mockserver.FiberExpectationInitializer + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dco.paralleluniverse.fibers.verifyInstrumentation=true + -Dco.paralleluniverse.fibers.disableAgentWarning + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + false + -Dco.paralleluniverse.fibers.verifyInstrumentation=true + -Dco.paralleluniverse.fibers.disableAgentWarning + + + + + \ No newline at end of file diff --git a/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/Http2Client.java b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/Http2Client.java new file mode 100644 index 0000000..d4aeec8 --- /dev/null +++ b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/Http2Client.java @@ -0,0 +1,244 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.http; + +import static com.google.common.collect.Lists.newArrayList; +import static java.nio.file.Files.createTempDirectory; +import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute; +import static java.nio.file.attribute.PosixFilePermissions.fromString; +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.trimToNull; +import static org.slf4j.LoggerFactory.getLogger; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; + +import com.squareup.okhttp.Cache; +import com.squareup.okhttp.CacheControl; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; + +import co.paralleluniverse.fibers.okhttp.FiberOkHttpClient; +import okio.BufferedSink; + +/** + * HTTP+SPDY client. + * @author Erik Torres + * @since @since 0.1.0 + */ +public class Http2Client { + + private static final Logger LOGGER = getLogger(Http2Client.class); + + private static final int CACHE_SIZE_MIB = 32 * 1024 * 1024; // 32 MiB + + private static OkHttpClient __client = null; + + private Lock mutex = new ReentrantLock(); + + public static Http2Client getHttp2Client() { + return new Http2Client(); + } + + /** + * Creates a {@link OkHttpClient} instance and configures it with a cache. The same instance is used across the application to + * benefit from a common cache storage and to prevent cache corruption. The cache directory is created private to the user who + * runs the application and its content is deleted when the JVM starts its shutting down sequence. + * @return A {@link OkHttpClient} instance that can be used everywhere in the application. + */ + private OkHttpClient client() { + mutex.lock(); + try { + if (__client == null) { + __client = new FiberOkHttpClient(); + try { + final File cacheDir = createTempDirectory("coreutils-okhttp-cache-", asFileAttribute(fromString("rwx------"))).toFile(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + FileUtils.deleteQuietly(cacheDir); + } + }); + final Cache cache = new Cache(cacheDir, CACHE_SIZE_MIB); + __client.setCache(cache); + } catch (IOException e) { + LOGGER.error("Failed to create directory cache", e); + } + } + return __client; + } finally { + mutex.unlock(); + } + } + + /** + * Retrieve information from a server via a HTTP GET request. + * @param url - URL target of this request + * @param nocache - don't accept an invalidated cached response, and don't store the server's response in any cache + * @param callback - is called back when the response is readable + */ + public void asyncGet(final String url, final boolean nocache, final Callback callback) { + asyncGet(url, null, nocache, callback); + } + + /** + * Retrieve information from a server via a HTTP GET request. + * @param url - URL target of this request + * @param nocache - don't accept an invalidated cached response, and don't store the server's response in any cache + * @param callback - is called back when the response is readable + */ + public void asyncGetJson(final String url, final boolean nocache, final Callback callback) { + asyncGet(url, newArrayList("application/json"), nocache, callback); + } + + /** + * Retrieve information from a server via a HTTP GET request. + * @param url - URL target of this request + * @param acceptableMediaTypes - Content-Types that are acceptable for this request + * @param nocache - don't accept an invalidated cached response, and don't store the server's response in any cache + * @param callback - is called back when the response is readable + */ + public void asyncGet(final String url, final @Nullable List acceptableMediaTypes, final boolean nocache, final Callback callback) { + final String url2 = requireNonNull(trimToNull(url), "A non-empty URL expected"); + requireNonNull(callback, "A valid callback expected"); + // configure cache + final CacheControl.Builder cacheControlBuilder = new CacheControl.Builder(); + if (nocache) cacheControlBuilder.noCache().noStore(); + else cacheControlBuilder.maxStale(3600, TimeUnit.SECONDS); + // prepare request + final Request.Builder requestBuilder = new Request.Builder().cacheControl(cacheControlBuilder.build()).url(url2); + ofNullable(acceptableMediaTypes).orElse(emptyList()).stream().filter(Objects::nonNull).forEach(type -> requestBuilder.addHeader("Accept", type)); + // submit request + client().newCall(requestBuilder.build()).enqueue(callback); + } + + /** + * Posts data to a server via a HTTP POST request. + * @param url - URL target of this request + * @param mediaType - Content-Type header for this request + * @param supplier - supplies the content of this request + * @param callback - is called back when the response is readable + */ + public void asyncPost(final String url, final String mediaType, final Supplier supplier, final Callback callback) { + requireNonNull(supplier, "A valid supplier expected"); + asyncPostBytes(url, mediaType, () -> ofNullable(supplier.get()).orElse("").getBytes(), callback); + } + + /** + * Posts the content of a buffer of bytes to a server via a HTTP POST request. + * @param url - URL target of this request + * @param mediaType - Content-Type header for this request + * @param supplier - supplies the content of this request + * @param callback - is called back when the response is readable + */ + public void asyncPostBytes(final String url, final String mediaType, final Supplier supplier, final Callback callback) { + final String url2 = requireNonNull(trimToNull(url), "A non-empty URL expected"); + final String mediaType2 = requireNonNull(trimToNull(mediaType), "A non-empty media type expected"); + requireNonNull(supplier, "A valid supplier expected"); + requireNonNull(callback, "A valid callback expected"); + // prepare request + final Request request = new Request.Builder().url(url2).post(new RequestBody() { + @Override + public MediaType contentType() { + return MediaType.parse(mediaType2 + "; charset=utf-8"); + } + @Override + public void writeTo(final BufferedSink sink) throws IOException { + sink.write(supplier.get()); + } + }).build(); + // submit request + client().newCall(request).enqueue(callback); + } + + /** + * Puts data to a server via a HTTP PUT request. + * @param url - URL target of this request + * @param mediaType - Content-Type header for this request + * @param supplier - supplies the content of this request + * @param callback - is called back when the response is readable + */ + public void asyncPut(final String url, final String mediaType, final Supplier supplier, final Callback callback) { + requireNonNull(supplier, "A valid supplier expected"); + asyncPutBytes(url, mediaType, () -> ofNullable(supplier.get()).orElse("").getBytes(), callback); + } + + /** + * Puts the content of a buffer of bytes to a server via a HTTP PUT request. + * @param url - URL target of this request + * @param mediaType - Content-Type header for this request + * @param supplier - supplies the content of this request + * @param callback - is called back when the response is readable + */ + public void asyncPutBytes(final String url, final String mediaType, final Supplier supplier, final Callback callback) { + final String url2 = requireNonNull(trimToNull(url), "A non-empty URL expected"); + final String mediaType2 = requireNonNull(trimToNull(mediaType), "A non-empty media type expected"); + requireNonNull(supplier, "A valid supplier expected"); + requireNonNull(callback, "A valid callback expected"); + // prepare request + final Request request = new Request.Builder().url(url2).put(new RequestBody() { + @Override + public MediaType contentType() { + return MediaType.parse(mediaType2 + "; charset=utf-8"); + } + @Override + public void writeTo(final BufferedSink sink) throws IOException { + sink.write(supplier.get()); + } + }).build(); + // submit request + client().newCall(request).enqueue(callback); + } + + /** + * Delete HTTP method. + * @param supplier - supplies the content of this request + * @param callback - is called back when the response is readable + */ + public void asyncDelete(final String url, final Callback callback) { + final String url2 = requireNonNull(trimToNull(url), "A non-empty URL expected"); + requireNonNull(callback, "A valid callback expected"); + // prepare request + final Request request = new Request.Builder().url(url2).delete().build(); + // submit request + client().newCall(request).enqueue(callback); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/HttpDataFetcher.java b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/HttpDataFetcher.java new file mode 100644 index 0000000..ccb1340 --- /dev/null +++ b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/http/HttpDataFetcher.java @@ -0,0 +1,296 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.http; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableMap.of; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.Collections.synchronizedList; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.removeEnd; +import static org.apache.commons.lang3.StringUtils.trimToNull; +import static org.grycap.coreutils.fiber.net.UrlBuilder.getUrlBuilder; +import static org.slf4j.LoggerFactory.getLogger; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.utils.URIUtils; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.nio.client.methods.ZeroCopyConsumer; +import org.apache.http.nio.protocol.BasicAsyncRequestProducer; +import org.apache.http.nio.protocol.HttpAsyncRequestProducer; +import org.grycap.coreutils.fiber.net.UrlBuilder; +import org.slf4j.Logger; + +import com.google.common.collect.Range; + +import co.paralleluniverse.fibers.httpasyncclient.FiberCloseableHttpAsyncClient; + +/** + * High-concurrency data fetcher. Note that this class will name fetched files using a convention-over-configuration pattern + * where the suffix .partial is appended to the filenames to differentiate between files completely retrieved and + * files being fetched. Once the download complete, the file is moved to its final destination, by simply removing the suffix + * from the filename and renaming the file. + * @author Erik Torres + * @since 0.1.0 + */ +public class HttpDataFetcher { + + private static final Logger LOGGER = getLogger(HttpDataFetcher.class); + + /** + * Maximum allowed concurrency. Values above this limit will be trimmed. + */ + public static final int MAX_CONCURRENCY = 32; + + private final int concurrencyLevel; + + /** + * Convenient constructor that allows creating a new instance of this class setting the value of the concurrency level. + * Values above this {@link HttpDataFetcher#MAX_CONCURRENCY limit} will be trimmed. + * @param concurrencyLevel - desired concurrency level + */ + public HttpDataFetcher(final int concurrencyLevel) { + this.concurrencyLevel = Range.closed(1, MAX_CONCURRENCY).contains(concurrencyLevel) ? concurrencyLevel : MAX_CONCURRENCY; + LOGGER.info(new StringBuilder("Concurrency level: ").append(this.concurrencyLevel).toString()); + } + + /** + * Specialization of the method {@link HttpDataFetcher#fetchToDir(URL, String, List, String, String, File)} that allows fetching + * and saving a bunch of objects to the specified directory from a server that uses a REST or REST-like API where each object + * is retrieved from the URL formed appending the object's identifier to the path of the the base URL. + * @param baseUrl - base URL from where the objects will be fetched + * @param ids - a list with the identifiers of the all requests that will be attempted + * @param outdir - directory where the files will be stored + * @return A {@link CompletableFuture} that allows cancellation. Once each fetch operation is completed, its status is updated + * in the future with one of the possible values provided by the enumeration {@link FetchStatus}. + * @throws IOException If an error occurs during the execution of the method that prevents fetching or saving the files. + */ + public FecthFuture fetchToDir(final URL baseUrl, final List ids, final File outdir) throws IOException { + return fetchToDir(baseUrl, null, ids, null, null, outdir); + } + + /** + * Allows fetching and saving a bunch of objects to the specified directory from a server that uses a REST or REST-like API + * where each object is retrieved from the URL formed appending the object's identifier to the path of the the base URL, and + * optionally from a server that uses a parameter to identify the objects. Supports additional configuration options to name + * the fetched objects. + * @param baseUrl - base URL from where the objects will be fetched + * @param queryParam - if defined, a query parameter will be appended to the base URL with the identifier of the request + * @param ids - a list with the identifiers of the all requests that will be attempted + * @param prefix - optionally prepend this prefix to the filenames of the saved files + * @param suffix - optionally append this suffix to the filenames of the saved files + * @param outdir - directory where the files will be stored + * @return A {@link CompletableFuture} that allows cancellation. Once each fetch operation is completed, its status is updated + * in the future with one of the possible values provided by the enumeration {@link FetchStatus}. + * @throws IOException If an error occurs during the execution of the method that prevents fetching or saving the files. + */ + public FecthFuture fetchToDir(final URL baseUrl, final @Nullable String queryParam, final List ids, + final @Nullable String prefix, final @Nullable String suffix, final File outdir) throws IOException { + // check mandatory parameters + requireNonNull(baseUrl, "A valid URL expected"); + final FecthFuture toBeCompleted = new FecthFuture(requireNonNull(ids, "A valid list of identifiers expected").stream() + .map(StringUtils::trimToNull).filter(Objects::nonNull).distinct().collect(Collectors.toList())); + requireNonNull(outdir, "A valid output directory expected"); + checkArgument((outdir.isDirectory() && outdir.canWrite()) || outdir.mkdirs(), new StringBuilder("Cannot write to the output directory: ") + .append(outdir.getAbsolutePath()).toString()); + // get optional parameters + final Optional queryParam2 = ofNullable(trimToNull(queryParam)); + final String prefix2 = ofNullable(prefix).orElse(""); + final String suffix2 = ofNullable(suffix).orElse(""); + try (final CloseableHttpAsyncClient asyncHttpClient = createFiberCloseableHttpAsyncClient()) { + asyncHttpClient.start(); + final UrlBuilder urlBuilder = getUrlBuilder(baseUrl); + // an explanation is needed since this code is instrumented by Quasar and Comsat: requests are created during the first part of + // this lambda expression (map), but they are not executed until the get() method is called in the second part of the expression + // (forEach). Here that parallel stream is used to block and wait for the requests to complete. In case that a single stream is + // used, each request will be created and executed sequentially. Therefore, the alternative to parallel stream is to separate + // the lambda expression in two loops, creating the requests in the first loop and calling get() in the second one. + toBeCompleted.monList.parallelStream().map(m -> { + try { + // create output file + final File outfile = new File(outdir, new StringBuilder(prefix2).append(m.id).append(suffix2).append(".partial").toString()); + checkState(outfile.createNewFile(), new StringBuilder("Cannot create the output file: ") + .append(outfile.getAbsolutePath()).toString()); + // create the HTTP request + final HttpHost target = URIUtils.extractHost(baseUrl.toURI()); + final HttpRequest request = new BasicHttpRequest("GET", urlBuilder.buildRelativeUrl(queryParam2.isPresent() ? null : m.id, + queryParam2.isPresent() ? of(queryParam2.get(), m.id) : null)); + final HttpAsyncRequestProducer producer = new BasicAsyncRequestProducer(target, request); + // create the consumer + final ZeroCopyConsumer consumer = new ZeroCopyConsumer(outfile) { + @Override + protected File process(final HttpResponse response, final File file, final ContentType contentType) throws Exception { + final StatusLine status = response.getStatusLine(); + if (LOGGER.isDebugEnabled()) + LOGGER.debug(new StringBuilder("Got file: statusCode=").append(status.getStatusCode()) + .append(", file=").append(file.getAbsolutePath()).toString()); + if (status.getStatusCode() != HttpStatus.SC_OK) + throw new ClientProtocolException(new StringBuilder("Object fetch failed: ").append(status).toString()); + return file; + } + }; + // prepare request + m.future = asyncHttpClient.execute(producer, consumer, new FutureCallback() { + @Override + public void cancelled() { + toBeCompleted.update(m.id, FetchStatus.CANCELLED); + LOGGER.info("Task cancelled"); + } + @Override + public void completed(final File result) { + try { + final Path path = result.toPath(); + Files.move(path, path.resolveSibling(removeEnd(result.getName(), ".partial")), REPLACE_EXISTING); + toBeCompleted.update(m.id, FetchStatus.COMPLETED); + } catch (IOException ex) { + toBeCompleted.update(m.id, FetchStatus.FAILED); + LOGGER.error("Fecth failed to move file to its final destination with error", ex); + } + } + @Override + public void failed(final Exception ex) { + toBeCompleted.update(m.id, FetchStatus.FAILED); + LOGGER.error("Fecth failed with error", ex); + } + }); + } catch (Exception e) { + LOGGER.error(new StringBuilder("Failed to fetch object with id: ").append(m.id).toString(), e); + } + return m; + }).forEach(m -> { + try { + // submit requests and wait for completion + m.future.get(); + } catch (Exception ignore) { /* exceptions are handled in the callback functions */ } + }); + } + return toBeCompleted; + } + + private CloseableHttpAsyncClient createFiberCloseableHttpAsyncClient() { + return FiberCloseableHttpAsyncClient.wrap(HttpAsyncClients + .custom() + .setMaxConnPerRoute(concurrencyLevel) + .setMaxConnTotal(concurrencyLevel) + .build()); + } + + /** + * Extends Java 8 {@link CompletableFuture} with real cancellation that will attempt to cancel the requests managed by this future. + * @author Erik Torres + * @since 0.0.1 + * @see CompletableFuture can't be Interrupted + */ + public static class FecthFuture extends CompletableFuture> { + + private final List monList; + + public FecthFuture(final List ids) { + this.monList = synchronizedList(requireNonNull(ids, "A non-empty list expected").stream() + .map(FetchMonitor::new).collect(Collectors.toList())); + checkArgument(!monList.isEmpty(), "A non-empty list expected"); + } + + public void update(final String id, final FetchStatus newStatus) { + monList.stream().filter(m -> m.id.equals(id)).findFirst().ifPresent(m -> { + m.status = requireNonNull(newStatus, "A non-empty status expected"); + }); + if (!monList.stream().anyMatch(m -> FetchStatus.PENDING.equals(m.status))) { + complete(monList.stream().collect(Collectors.toMap(FetchMonitor::getId, FetchMonitor::getStatus))); + } + } + + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + monList.stream().forEach(m -> { try { m.future.cancel(true); } catch (Exception ignore) { } }); + return super.cancel(mayInterruptIfRunning); + } + + } + + /** + * Monitors fetch operation status. + * @author Erik Torres + * @since 0.0.1 + */ + private static class FetchMonitor { + + private final String id; + private Future future; + private FetchStatus status = FetchStatus.PENDING; + + public FetchMonitor(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public FetchStatus getStatus() { + return status; + } + + } + + /** + * Possible status after object fetching. + * @author Erik Torres + * @since 0.0.1 + */ + public enum FetchStatus { + PENDING, + COMPLETED, + CANCELLED, + FAILED + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/net/UrlBuilder.java b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/net/UrlBuilder.java new file mode 100644 index 0000000..9e1fa17 --- /dev/null +++ b/coreutils-fiber/src/main/java/org/grycap/coreutils/fiber/net/UrlBuilder.java @@ -0,0 +1,132 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.net; + +import static java.net.URLDecoder.decode; +import static java.nio.charset.Charset.defaultCharset; +import static java.util.Collections.emptyMap; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.stripEnd; +import static org.apache.commons.lang3.StringUtils.trimToNull; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nullable; + +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.client.utils.URLEncodedUtils; + +/** + * Builder for {@link URL} instances. + * @author Erik Torres + * @since 0.1.0 + */ +public class UrlBuilder { + + /** + * The base URL that the methods provided by this class will use to create new URLs. + */ + private final URL baseUrl; + + private UrlBuilder(final URL baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Convenient factory method that creates a new instance of this class. + * @param baseUrl - base URL that will be used to create new URLs + * @return A new instance of this class, allowing callers to use the methods of this class to create new URLs + * from the base URL provided as argument. + */ + public static UrlBuilder getUrlBuilder(final URL baseUrl) { + URL url = null; + try { + url = new URL(decode(requireNonNull(baseUrl).toString(), defaultCharset().name())); + } catch (MalformedURLException | UnsupportedEncodingException e) { + throw new IllegalArgumentException(new StringBuilder("Cannot create a URI from the provided URL: ") + .append(baseUrl != null ? baseUrl.toString() : "null").toString(), e); + } + return new UrlBuilder(url); + } + + /** + * Creates a new URL relative to the base URL provided in the constructor of this class. The new relative URL + * includes the path, query parameters and the internal reference of the {@link UrlBuilder#baseUrl base URL} + * provided with this class. An additional fragment, as well as additional query parameters can be optionally + * added to the new URL. In addition to the parameters passed to the method as an argument, the supplied + * fragment can also include parameters that will be added to the created URL. The created URL is normalized + * and unencoded before returning it to the caller. The current implementation has the following limitations: + *
    + *
  • Arrays are not supported: q=foo&q=bar will produce an error.
  • + *
  • Internal references are only supported in the base URL. Any additional reference provided with the + * fragment will be silently ignored: the fragment /rd#ref will be appended to the base URL as + * /rd, ignoring the internal reference.
  • + *
+ * @param fragment - optional URL fragment (may include parameters, but not references) that will be added + * to the base URL + * @param params - optional query parameters that will be added to the base URL + * @return A relative URL created from the base URL provided in the constructor of this class and adding the + * fragment and parameters passed as arguments to this method. + */ + public String buildRelativeUrl(final @Nullable String fragment, final @Nullable Map params) { + String url = null; + final Optional fragment2 = ofNullable(trimToNull(fragment)); + try { + final Optional fragmentUrl = ofNullable(fragment2.isPresent() ? new URL("http://example.com/" + fragment2.get()) : null); + final URIBuilder uriBuilder = new URIBuilder(); + // add path + uriBuilder.setPath(new StringBuilder(ofNullable(trimToNull(baseUrl.getPath())).orElse("/")) + .append(fragmentUrl.isPresent() ? "/" + stripEnd(fragmentUrl.get().getPath(), "/") : "") + .toString().replaceAll("[/]{2,}", "/")); + // add query parameters + if (isNotBlank(baseUrl.getQuery())) { + uriBuilder.setParameters(URLEncodedUtils.parse(baseUrl.getQuery(), defaultCharset())); + } + if (fragmentUrl.isPresent() && isNotBlank(fragmentUrl.get().getQuery())) { + URLEncodedUtils.parse(fragmentUrl.get().getQuery(), defaultCharset()).stream().forEach(p -> { + uriBuilder.addParameter(p.getName(), p.getValue()); + }); + } + ofNullable(params).orElse(emptyMap()).entrySet().stream().forEach(p -> { + uriBuilder.addParameter(p.getKey(), p.getValue()); + }); + // add internal reference + uriBuilder.setFragment(baseUrl.getRef()); + // build relative URL + url = uriBuilder.build().normalize().toString(); + } catch (MalformedURLException | URISyntaxException e) { + throw new IllegalStateException(new StringBuilder("Failed to create relative URL from provided parameters: fragment=") + .append(fragment2.orElse("null")).append(", params=").append(params != null ? params.toString() : "null").toString(), e); + } + return url; + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchCancellationTest.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchCancellationTest.java new file mode 100644 index 0000000..4f64236 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchCancellationTest.java @@ -0,0 +1,97 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.grycap.coreutils.fiber.test.mockserver.FiberExpectationInitializer.MOCK_SERVER_BASE_URL; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.File; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.grycap.coreutils.fiber.http.HttpDataFetcher; +import org.grycap.coreutils.fiber.http.HttpDataFetcher.FetchStatus; +import org.grycap.coreutils.test.category.IntegrationTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; + +import com.google.common.collect.ImmutableList; + +import co.paralleluniverse.fibers.futures.AsyncCompletionStage; + +/** + * Tests data fetching with cancellation. + * @author Erik Torres + * @since 0.1.0 + */ +@Category(IntegrationTests.class) +public class FetchCancellationTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + @Test + public void testFetch() throws Exception { + // create fetcher + final HttpDataFetcher fetcher = new HttpDataFetcher(2); + assertThat("Fetcher was created", fetcher, notNullValue()); + + // create output folder + final File outDir = tmpFolder.newFolder(randomAlphanumeric(12)); + assertThat("Output dir was created", outDir, notNullValue()); + assertThat("Output dir is writable", outDir.canWrite()); + + // submit request and cancel + final ExecutorService executorService = Executors.newFixedThreadPool(2); + final Future> future = executorService.submit(new Callable>() { + @Override + public Map call() throws Exception { + return AsyncCompletionStage.get(fetcher.fetchToDir(new URL(MOCK_SERVER_BASE_URL + "/fetch/long-waiting"), + ImmutableList.of("1"), outDir), 120000l, TimeUnit.SECONDS); + } + }); + assertThat("Request was cancelled", future.cancel(true)); + assertThat("File does not exist", not(new File(outDir, "1").exists())); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchTest.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchTest.java new file mode 100644 index 0000000..24afdb4 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchTest.java @@ -0,0 +1,164 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test; + +import static java.util.Collections.emptyList; +import static org.apache.commons.collections.ListUtils.union; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.grycap.coreutils.fiber.test.mockserver.FiberExpectationInitializer.MOCK_SERVER_BASE_URL; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseValidator.validateJson; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseValidator.validateXml; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.number.OrderingComparison.greaterThan; + +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.grycap.coreutils.fiber.http.HttpDataFetcher; +import org.grycap.coreutils.fiber.http.HttpDataFetcher.FecthFuture; +import org.grycap.coreutils.fiber.http.HttpDataFetcher.FetchStatus; +import org.grycap.coreutils.test.category.IntegrationTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import com.google.common.collect.ImmutableList; + +import co.paralleluniverse.fibers.futures.AsyncCompletionStage; + +/** + * Tests data fetching. + * @author Erik Torres + * @since 0.1.0 + */ +@RunWith(Parameterized.class) +@Category(IntegrationTests.class) +public class FetchTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + /** + * Provides an input dataset with different data formats (JSON, XML) and different access methods (URL fragment, + * query parameter). Some tests will surpass the concurrency level. Invalid identifiers are included to test that + * the fetcher handles 404 status responses. + * @return Parameters for the different test scenarios. + */ + @Parameters(name = "{index}: path={0}, queryParam={1}, validIds={2}, invalidIds={3}, concurrencyLevel={4}, useFiber={5}") + public static Collection data() { + return Arrays.asList(new Object[][] { + /*0*/ { "/test/json", null, ImmutableList.of("1", "2"), emptyList(), 4, true }, + /*1*/ { "/test/xml", "q", ImmutableList.of("1", "2"), emptyList(), 8, true }, + /*2*/ { "/test/json", null, emptyList(), ImmutableList.of("3", "4"), 4, true }, + /*3*/ { "/test/json", null, ImmutableList.of("1", "2"), ImmutableList.of("3", "4"), 4, true }, + /*4*/ { "/test/xml", "q", ImmutableList.of("1", "2"), ImmutableList.of("5", "6", "7"), 2, true }, + /*5*/ { "/test/json", null, ImmutableList.of("1", "2"), emptyList(), 8, false } + }); + } + + @Parameter(value = 0) public String path; + @Parameter(value = 1) public String queryParam; + @Parameter(value = 2) public List validIds; + @Parameter(value = 3) public List invalidIds; + @Parameter(value = 4) public int concurrencyLevel; + @Parameter(value = 5) public boolean useFiber; + + @Test + public void testFetch() throws Exception { + // create fetcher + final HttpDataFetcher fetcher = new HttpDataFetcher(concurrencyLevel); + assertThat("Fetcher was created", fetcher, notNullValue()); + + // create output folder + final File outDir = tmpFolder.newFolder(randomAlphanumeric(12)); + assertThat("Output dir was created", outDir, notNullValue()); + assertThat("Output dir is writable", outDir.canWrite()); + + // create test dataset + @SuppressWarnings("unchecked") + final List ids = union(validIds, invalidIds); + final URL url = new URL(MOCK_SERVER_BASE_URL + path); + + // fetch from server + Map results = null; + if (useFiber) { + results = AsyncCompletionStage.get(fetcher.fetchToDir(url, queryParam, ids, "file-", ".tmp", outDir), 30l, TimeUnit.SECONDS); + } else { + final FecthFuture toBeCompleted = fetcher.fetchToDir(url, queryParam, ids, null, null, outDir); + assertThat("Fetch task was created", toBeCompleted, notNullValue()); + results = toBeCompleted.get(30l, TimeUnit.SECONDS); + } + assertThat("Results are available", results, notNullValue()); + assertThat("Result count coincides with expected", results.size(), equalTo(ids.size())); + assertThat("Ids coincides with expected", results.keySet(), hasItems(ids.toArray(new String[ids.size()]))); + pw.println(" >> Results: " + results); + if (!validIds.isEmpty()) { + assertThat("Status coincides with expected", results.values(), allOf(hasItem(FetchStatus.COMPLETED), + not(hasItems(FetchStatus.PENDING, FetchStatus.CANCELLED)))); + } + if (!invalidIds.isEmpty()) { + assertThat("Status coincides with expected", results.values(), allOf(hasItem(FetchStatus.FAILED), + not(hasItems(FetchStatus.PENDING, FetchStatus.CANCELLED)))); + } + + // check files in the output directory + for (final String id : validIds) { + final File outFile = new File(outDir, useFiber ? new StringBuilder("file-").append(id).append(".tmp").toString() : id); + assertThat("Output file was created", outDir, notNullValue()); + assertThat("Output file file exists and is redable", outFile.canRead(), equalTo(true)); + assertThat("Output file file is not empty", outFile.length(), greaterThan(0l)); + if (path.contains("xml")) { + validateXml(outFile, id); + } else if (path.contains("json")) { + validateJson(outFile, id); + } + } + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchValidationTest.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchValidationTest.java new file mode 100644 index 0000000..9d2f8dd --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/FetchValidationTest.java @@ -0,0 +1,91 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test; + +import static java.util.Collections.emptyList; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.grycap.coreutils.fiber.test.mockserver.FiberExpectationInitializer.MOCK_SERVER_BASE_URL; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.net.URL; + +import org.grycap.coreutils.fiber.http.HttpDataFetcher; +import org.grycap.coreutils.test.category.SanityTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; + +import com.google.common.collect.ImmutableList; + +/** + * Tests fetcher with invalid inputs. + * @author Erik Torres + * @since 0.1.0 + */ +@Category(SanityTests.class) +public class FetchValidationTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + @Test(expected=NullPointerException.class) + public void testFetchWithoutUrl() throws Exception { + final HttpDataFetcher fetcher = new HttpDataFetcher(2); + assertThat("Fetcher was created", fetcher, notNullValue()); + fetcher.fetchToDir(null, ImmutableList.of("1"), tmpFolder.newFolder(randomAlphanumeric(12))); + } + + @Test(expected=NullPointerException.class) + public void testFetchWithoutList() throws Exception { + final HttpDataFetcher fetcher = new HttpDataFetcher(2); + assertThat("Fetcher was created", fetcher, notNullValue()); + fetcher.fetchToDir(new URL(MOCK_SERVER_BASE_URL + "/test/json"), null, tmpFolder.newFolder(randomAlphanumeric(12))); + } + + @Test(expected=IllegalArgumentException.class) + public void testFetchWithEmptyList() throws Exception { + final HttpDataFetcher fetcher = new HttpDataFetcher(2); + assertThat("Fetcher was created", fetcher, notNullValue()); + fetcher.fetchToDir(new URL(MOCK_SERVER_BASE_URL + "/test/json"), emptyList(), tmpFolder.newFolder(randomAlphanumeric(12))); + } + + @Test(expected=NullPointerException.class) + public void testFetchWithoutOutputDir() throws Exception { + final HttpDataFetcher fetcher = new HttpDataFetcher(2); + assertThat("Fetcher was created", fetcher, notNullValue()); + fetcher.fetchToDir(new URL(MOCK_SERVER_BASE_URL + "/test/json"), ImmutableList.of("1"), null); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/Http2ClientTest.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/Http2ClientTest.java new file mode 100644 index 0000000..afd4718 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/Http2ClientTest.java @@ -0,0 +1,219 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Optional.ofNullable; +import static org.grycap.coreutils.fiber.http.Http2Client.getHttp2Client; +import static org.grycap.coreutils.fiber.test.mockserver.FiberExpectationInitializer.MOCK_SERVER_BASE_URL; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseValidator.isValidJson; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseValidator.isValidXml; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.grycap.coreutils.fiber.http.Http2Client; +import org.grycap.coreutils.test.category.IntegrationTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import net.jodah.concurrentunit.Waiter; + +/** + * Tests the {@link Http2Client}. + * @author Erik Torres + * @since @since 0.1.0 + */ +@RunWith(Parameterized.class) +@Category(IntegrationTests.class) +public class Http2ClientTest { + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + /** + * Provides an input dataset with different data formats (JSON, XML) and different access methods (URL fragment, + * query parameter). Some tests will surpass the concurrency level. + * @return Parameters for the different test scenarios. + */ + @Parameters(name = "{index}: method={0}, path={1}, objectId={2}, contentType={3}, nocache={4}") + public static Collection data() { + return Arrays.asList(new Object[][] { + /* 0*/ { "GET", "/test/json/1", "1", of("application/json"), true }, + /* 1*/ { "GET", "/test/json/2", "2", of("application/json"), true }, + /* 2*/ { "GET", "/test/xml/1", "1", of("application/xml"), true }, + /* 3*/ { "GET", "/test/xml/2", "2", of("application/xml"), true }, + /* 4*/ { "GET", "/test/json?q=1", "1", of("application/json"), true }, + /* 5*/ { "GET", "/test/json?q=2", "2", of("application/json"), true }, + /* 6*/ { "GET", "/test/xml?q=1", "1", of("application/xml"), true }, + /* 7*/ { "GET", "/test/xml?q=2", "2", of("application/xml"), true }, + /* 8*/ { "GET", "/test/json/1", "1", null, true }, + /* 9*/ { "GET", "/test/xml/1", "1", null, true }, + /*10*/ { "GET", "/test/json/1", "1", null, false }, + /*11*/ { "GET", "/test/xml/1", "1", null, false }, + /*12*/ { "POST", "/test/json", "X", of("application/json"), true }, + /*13*/ { "POST", "/test/xml", "Y", of("application/xml"), true }, + /*14*/ { "PUT", "/test/json/1", "1", of("application/json"), true }, + /*15*/ { "PUT", "/test/xml/2", "2", of("application/xml"), true }, + /*16*/ { "DELETE", "/test/json/2", "2", of("application/json"), true }, + /*17*/ { "DELETE", "/test/xml/1", "1", of("application/xml"), true } + }); + } + + @Parameter(value = 0) public String method; + @Parameter(value = 1) public String path; + @Parameter(value = 2) public String objectId; + @Parameter(value = 3) public List contentType; + @Parameter(value = 4) public boolean nocache; + + @Test + public void test() throws Exception { + // create the client + final Http2Client client = getHttp2Client(); + assertThat("HTTP+SPDY client was created", client, notNullValue()); + + // prepare the test + final String url = new StringBuilder(MOCK_SERVER_BASE_URL).append(path).toString(); + final Waiter waiter = new Waiter(); + + // submit request + switch (method) { + case "POST": + submitPost(client, url, waiter); + break; + case "PUT": + submitPut(client, url, waiter); + break; + case "DELETE": + submitDelete(client, url, waiter); + break; + case "GET": + default: + submitGet(client, url, waiter); + break; + } + waiter.await(30l, TimeUnit.SECONDS); + } + + private void submitGet(final Http2Client client, final String url, final Waiter waiter) { + client.asyncGet(url, contentType, nocache, new Callback() { + @Override + public void onResponse(final Response response) throws IOException { + waiter.assertTrue(response.isSuccessful()); + final String payload = response.body().source().readUtf8(); + waiter.assertThat(payload, allOf(notNullValue(), not(equalTo("")))); + if (path.contains("xml")) { + isValidXml(payload, objectId); + } else if (path.contains("json")) { + isValidJson(payload, objectId); + } + waiter.resume(); + } + @Override + public void onFailure(final Request request, final IOException throwable) { + waiter.fail(throwable); + } + }); + } + + private void submitPost(final Http2Client client, final String url, final Waiter waiter) { + Supplier supplier = null; + if (path.contains("xml")) { + supplier = () -> "Y"; + } else if (path.contains("json")) { + supplier = () -> "{ \"object\" : \"X\" }"; + } + client.asyncPost(url, ofNullable(contentType).orElse(newArrayList("application/json")).get(0), supplier, new Callback() { + @Override + public void onResponse(final Response response) throws IOException { + waiter.assertTrue(response.isSuccessful()); + waiter.resume(); + } + @Override + public void onFailure(final Request request, final IOException throwable) { + waiter.fail(throwable); + } + }); + } + + private void submitPut(final Http2Client client, final String url, final Waiter waiter) { + Supplier supplier = null; + if (path.contains("xml")) { + supplier = () -> "Y"; + } else if (path.contains("json")) { + supplier = () -> "{ \"object\" : \"X\" }"; + } + client.asyncPut(url, ofNullable(contentType).orElse(newArrayList("application/json")).get(0), supplier, new Callback() { + @Override + public void onResponse(final Response response) throws IOException { + waiter.assertTrue(response.isSuccessful()); + waiter.resume(); + } + @Override + public void onFailure(final Request request, final IOException throwable) { + waiter.fail(throwable); + } + }); + } + + private void submitDelete(final Http2Client client, final String url, final Waiter waiter) { + client.asyncDelete(url, new Callback() { + @Override + public void onResponse(final Response response) throws IOException { + waiter.assertTrue(response.isSuccessful()); + waiter.resume(); + } + @Override + public void onFailure(final Request request, final IOException throwable) { + waiter.fail(throwable); + } + }); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/UrlBuilderTest.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/UrlBuilderTest.java new file mode 100644 index 0000000..93d0d8c --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/UrlBuilderTest.java @@ -0,0 +1,116 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Collections.emptyMap; +import static org.apache.commons.lang3.StringUtils.trim; +import static org.grycap.coreutils.fiber.net.UrlBuilder.getUrlBuilder; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.grycap.coreutils.fiber.net.UrlBuilder; +import org.grycap.coreutils.test.category.SanityTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests {@link UrlBuilder}. + * @author Erik Torres + * @since 0.1.0 + */ +@RunWith(Parameterized.class) +@Category(SanityTests.class) +public class UrlBuilderTest { + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + /** + * Provides different input datasets. + * @return Parameters for the different test scenarios. + */ + @Parameters(name = "{index}: baseUrl={0}, fragment={1}, query={2}, expected={3}") + public static Collection data() { + return Arrays.asList(new Object[][] { + /* 0*/ { "http://example.com", "", emptyMap(), "/" }, + /* 1*/ { "http://example.com", "rd", emptyMap(), "/rd" }, + /* 2*/ { "http://example.com", "", of("q", "foo"), "/?q=foo" }, + /* 3*/ { "http://example.com", "rd", of("q", "foo"), "/rd?q=foo" }, + /* 4*/ { "http://example.com", "/rd", of("q", "foo"), "/rd?q=foo" }, + /* 5*/ { "http://example.com", "/rd1/rd2", of("q", "foo"), "/rd1/rd2?q=foo" }, + /* 6*/ { "http://example.com", "/rd1/rd2#ref", of("q1", "foo", "q2", "bar"), "/rd1/rd2?q1=foo&q2=bar" }, + /* 7*/ { "http://example.com", "/rd?q1=foo#ref", of("q2", "bar"), "/rd?q1=foo&q2=bar" }, + /* 8*/ { "http://example.com", "/rd?q1=foo&q2=bar#ref", of("q3", "baz"), "/rd?q1=foo&q2=bar&q3=baz" }, + /* 9*/ { "http://example.com/rd1", "rd2#ref", of("q1", "foo"), "/rd1/rd2?q1=foo" }, + /*10*/ { "http://example.com/rd1", "/rd2#ref", of("q1", "foo"), "/rd1/rd2?q1=foo" }, + /*11*/ { "http://example.com/rd1", "/rd2", of("q1", "foo"), "/rd1/rd2?q1=foo" }, + /*12*/ { "http://example.com/rd1", "/rd2", emptyMap(), "/rd1/rd2" }, + /*13*/ { "http://example.com?q1=foo", "rd", of("q2", "bar"), "/rd?q1=foo&q2=bar" }, + /*14*/ { "http://example.com?q1=foo#ref", "rd", of("q2", "bar"), "/rd?q1=foo&q2=bar#ref" }, + /*15*/ { "http://example.com#ref", "rd", of("q", "foo"), "/rd?q=foo#ref" }, + /*16*/ { "http://example.com/rd?q=foo#ref", "", of("q", "bar"), "/rd?q=foo&q=bar#ref" }, + /*17*/ { "http://example.com#foo=bar", "rd", emptyMap(), "/rd#foo=bar" }, + /*18*/ { "http://example.com/rd1%3Fq1%3Dfoo%26q2%3Dbar%23ref", "/rd2", of("q3", "baz"), "/rd1/rd2?q1=foo&q2=bar&q3=baz#ref" }, + /*19*/ { "http://example.com/hello+world=foo%26bar", "rd", emptyMap(), "/hello%20world=foo&bar/rd"} + // unsupported: { "http://example.com/rd", "", of("q", "foo", "q", "bar"), "/rd?q=foo&q=bar" }, + }); + } + + @Parameter(value = 0) public String baseUrl; + @Parameter(value = 1) public String fragment; + @Parameter(value = 2) public Map query; + @Parameter(value = 3) public String expected; + + @Test + public void testUrlBuilder() throws Exception { + // create URL builder + final UrlBuilder urlBuilder = getUrlBuilder(new URL(baseUrl)); + assertThat("URL builder was created", urlBuilder, notNullValue()); + + // test URL + final String url = urlBuilder.buildRelativeUrl(fragment, query); + assertThat("URL coincides with expected", trim(url), allOf(notNullValue(), equalTo(expected))); + pw.println("URL: " + url); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/AbstractExpectationResponse.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/AbstractExpectationResponse.java new file mode 100644 index 0000000..985ae68 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/AbstractExpectationResponse.java @@ -0,0 +1,90 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.mockserver.model.Parameter; + +/** + * Base definition of response expectations. + * @author Erik Torres + * @since 0.1.0 + */ +public class AbstractExpectationResponse { + + private final String path; + private final List parameters; + private final int statusCode; + private final String contentType; + private final String body; + private final long delay; + + /** + * Convenient constructor that receives initial values for all the parameters of this class. + * @param path - the path where clients expect the server is listening for incoming requests + * @param parameters - query parameters accompanying the request + * @param statusCode - the response status code after the request is processed in the server + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public AbstractExpectationResponse(final String path, final Map parameters, final int statusCode, final String contentType, final String body, final long delay) { + this.path = path; + this.parameters = parameters.entrySet().stream().map(p -> { + return new Parameter(p.getKey(), p.getValue()); + }).collect(Collectors.toList()); + this.statusCode = statusCode; + this.contentType = contentType; + this.body = body; + this.delay = delay; + } + + public String getPath() { + return path; + } + + public List getParameters() { + return parameters; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContentType() { + return contentType; + } + + public String getBody() { + return body; + } + + public long getDelay() { + return delay; + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpGetResponse.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpGetResponse.java new file mode 100644 index 0000000..5b4b99f --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpGetResponse.java @@ -0,0 +1,76 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +import static java.util.Collections.emptyMap; + +import java.util.Map; + +/** + * Defines expectation responses for requests made with the HTTP GET method. + * @author Erik Torres + * @since 0.1.0 + */ +public class ExpectationHttpGetResponse extends AbstractExpectationResponse { + + /** + * Convenient constructor that can be used to create expectations where the server where the server responds + * to all correct requests with the HTTP status code OK (200) to the proper requests and no query parameters + * are submitted. This pattern is commonly used in REST and REST-like APIs. + * @param path - the path where clients expect the server is listening for incoming requests + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpGetResponse(final String path, final String contentType, final String body, final long delay) { + super(path, emptyMap(), 200, contentType, body, delay); + } + + /** + * Convenient constructor that can be used to create expectations where the server responds + * to all correct requests with the HTTP status code OK (200). + * @param path - the path where clients expect the server is listening for incoming requests + * @param parameters - query parameters accompanying the request + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpGetResponse(final String path, final Map parameters, final String contentType, final String body, final long delay) { + super(path, parameters, 200, contentType, body, delay); + } + + /** + * Convenient constructor that receives initial values for all the parameters of this class. + * @param path - the path where clients expect the server is listening for incoming requests + * @param parameters - query parameters accompanying the request + * @param statusCode - the response status code after the request is processed in the server + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpGetResponse(final String path, final Map parameters, final int statusCode, final String contentType, final String body, final long delay) { + super(path, parameters, statusCode, contentType, body, delay); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpPostResponse.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpPostResponse.java new file mode 100644 index 0000000..e425ae7 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ExpectationHttpPostResponse.java @@ -0,0 +1,76 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +import static java.util.Collections.emptyMap; + +import java.util.Map; + +/** + * Defines expectation responses for requests made with the HTTP POST method. + * @author Erik Torres + * @since 0.1.0 + */ +public class ExpectationHttpPostResponse extends AbstractExpectationResponse { + + /** + * Convenient constructor that can be used to create expectations where the server where the server responds + * to all correct requests with the HTTP status code CREATED (201) to the proper requests and no query parameters + * are submitted. This pattern is commonly used in REST and REST-like APIs. + * @param path - the path where clients expect the server is listening for incoming requests + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpPostResponse(final String path, final String contentType, final String body, final long delay) { + super(path, emptyMap(), 201, contentType, body, delay); + } + + /** + * Convenient constructor that can be used to create expectations where the server responds + * to all correct requests with the HTTP status code CREATED (201). + * @param path - the path where clients expect the server is listening for incoming requests + * @param parameters - query parameters accompanying the request + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpPostResponse(final String path, final Map parameters, final String contentType, final String body, final long delay) { + super(path, parameters, 201, contentType, body, delay); + } + + /** + * Convenient constructor that receives initial values for all the parameters of this class. + * @param path - the path where clients expect the server is listening for incoming requests + * @param parameters - query parameters accompanying the request + * @param statusCode - the response status code after the request is processed in the server + * @param contentType - the content type of the server response + * @param body - the content of the body sent with the server response + * @param delay - the server will wait this time before sending a response to the client + */ + public ExpectationHttpPostResponse(final String path, final Map parameters, final int statusCode, final String contentType, final String body, final long delay) { + super(path, parameters, statusCode, contentType, body, delay); + } + +} diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/FiberExpectationInitializer.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/FiberExpectationInitializer.java new file mode 100644 index 0000000..4a6e047 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/FiberExpectationInitializer.java @@ -0,0 +1,123 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +import static com.google.common.collect.ImmutableMap.of; +import static java.util.Collections.emptyMap; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseFactory.jsonObject1; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseFactory.jsonObject2; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseFactory.xmlObject1; +import static org.grycap.coreutils.fiber.test.mockserver.ObjectResponseFactory.xmlObject2; +import static org.mockserver.matchers.Times.exactly; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.StringBody.exact; + +import java.util.concurrent.TimeUnit; + +import org.mockserver.client.server.MockServerClient; +import org.mockserver.initialize.ExpectationInitializer; +import org.mockserver.model.Delay; +import org.mockserver.model.Header; + +import com.google.common.collect.ImmutableList; + +/** + * Sets Mock Server expectations for fetching tests. + * @author Erik Torres + * @since 0.1.0 + */ +public class FiberExpectationInitializer implements ExpectationInitializer { + + public static final String MOCK_SERVER_BASE_URL = "http://localhost:9080"; + + @Override + public void initializeExpectations(final MockServerClient client) { + // add GET operations + new ImmutableList.Builder() + // REST + JSON + .add(new ExpectationHttpGetResponse("/test/json/ids", "text/plain", "1,2", 0l)) + .add(new ExpectationHttpGetResponse("/test/json/1", "application/json", jsonObject1(), 300l)) + .add(new ExpectationHttpGetResponse("/test/json/2", "application/json", jsonObject2(), 500l)) + // REST + XML + .add(new ExpectationHttpGetResponse("/test/xml/ids", "text/plain", "1,2", 0l)) + .add(new ExpectationHttpGetResponse("/test/xml/1", "application/xml", xmlObject1(), 0l)) + .add(new ExpectationHttpGetResponse("/test/xml/2", "application/xml", xmlObject2(), 0l)) + // Query parameter + JSON + .add(new ExpectationHttpGetResponse("/test/json", of("q", "1"), "application/json", jsonObject1(), 100l)) + .add(new ExpectationHttpGetResponse("/test/json", of("q", "2"), "application/json", jsonObject2(), 0l)) + // Query parameter + XML + .add(new ExpectationHttpGetResponse("/test/xml", of("q", "1"), "application/xml", xmlObject1(), 300l)) + .add(new ExpectationHttpGetResponse("/test/xml", of("q", "2"), "application/xml", xmlObject2(), 120l)) + // Very long awaiting time + .add(new ExpectationHttpGetResponse("/test/long-waiting/1", "text/plain", "Hello World!", 20000l)) + .build().stream().forEach(r -> { + client.when(request().withMethod("GET").withPath(r.getPath()).withQueryStringParameters(r.getParameters())) + .respond(response().withStatusCode(r.getStatusCode()) + .withHeaders(new Header("Content-Type", r.getContentType() + "; charset=utf-8"), + new Header("Cache-Control", "public, max-age=86400")) + .withBody(r.getBody()) + .withDelay(new Delay(TimeUnit.MILLISECONDS, r.getDelay()))); + }); + // add POST operations + new ImmutableList.Builder() + .add(new ExpectationHttpPostResponse("/test/json", "application/json", "{ \"object\" : \"X\" }", 0l)) + .add(new ExpectationHttpPostResponse("/test/xml", "application/xml", "Y", 0l)) + .build().stream().forEach(r -> { + client.when(request().withMethod("POST").withPath(r.getPath()).withQueryStringParameters(r.getParameters()) + .withBody(exact(r.getBody())), exactly(1)) + .respond(response().withStatusCode(r.getStatusCode()) + .withHeaders(new Header("Cache-Control", "public, max-age=86400"), + new Header("Location", r.getPath() + "/createdId")) + .withDelay(new Delay(TimeUnit.MILLISECONDS, r.getDelay()))); + }); + // add PUT operations + new ImmutableList.Builder() + .add(new AbstractExpectationResponse("/test/json/1", emptyMap(), 204, "application/json", "{ \"object\" : \"X\" }", 0l){ }) + .add(new AbstractExpectationResponse("/test/json/2", emptyMap(), 204, "application/json", "{ \"object\" : \"X\" }", 0l){ }) + .add(new AbstractExpectationResponse("/test/xml/1", emptyMap(), 204, "application/xml", "Y", 0l){ }) + .add(new AbstractExpectationResponse("/test/xml/2", emptyMap(), 204, "application/xml", "Y", 0l){ }) + .build().stream().forEach(r -> { + client.when(request().withMethod("PUT").withPath(r.getPath()).withQueryStringParameters(r.getParameters()) + .withBody(exact(r.getBody())), exactly(1)) + .respond(response().withStatusCode(r.getStatusCode()) + .withHeaders(new Header("Cache-Control", "public, max-age=86400")) + .withDelay(new Delay(TimeUnit.MILLISECONDS, r.getDelay()))); + }); + // add DELETE operations + new ImmutableList.Builder() + .add(new AbstractExpectationResponse("/test/json/1", emptyMap(), 204, "application/json", jsonObject1(), 0l){ }) + .add(new AbstractExpectationResponse("/test/json/2", emptyMap(), 204, "application/json", jsonObject2(), 0l){ }) + .add(new AbstractExpectationResponse("/test/xml/1", emptyMap(), 204, "application/xml", xmlObject1(), 0l){ }) + .add(new AbstractExpectationResponse("/test/xml/2", emptyMap(), 204, "application/xml", xmlObject2(), 0l){ }) + .build().stream().forEach(r -> { + client.when(request().withMethod("DELETE").withPath(r.getPath()).withQueryStringParameters(r.getParameters()), + exactly(1)) + .respond(response().withStatusCode(r.getStatusCode()) + .withHeaders(new Header("Cache-Control", "public, max-age=86400")) + .withDelay(new Delay(TimeUnit.MILLISECONDS, r.getDelay()))); + }); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseFactory.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseFactory.java new file mode 100644 index 0000000..2e93338 --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseFactory.java @@ -0,0 +1,173 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +/** + * Creates objects using standard data formats. Despite the format, all objects include a recognizable identifier in the + * form: coreutils -> objectId -> Integer. All objects are valid, well-formed documents that can be parsed + * with standard tools. + * @author Erik Torres + * @since 0.1.0 + * @see JSON Example + */ +public final class ObjectResponseFactory { + + /** + * Creates a JSON formatted string with object Id 1. + * @return JSON formatted string with object Id 1. + */ + public static String jsonObject1() { + return new StringBuilder() + .append("{") + .append("\"glossary\": {") + .append("\"coreutils\": {") + .append("\"objectId\": \"1\"") + .append("},") + .append("\"title\": \"example glossary\",") + .append("\"GlossDiv\": {") + .append("\"title\": \"S\",") + .append("\"GlossList\": {") + .append("\"GlossEntry\": {") + .append("\"ID\": \"SGML\",") + .append("\"SortAs\": \"SGML\",") + .append("\"GlossTerm\": \"Standard Generalized Markup Language\",") + .append("\"Acronym\": \"SGML\",") + .append("\"Abbrev\": \"ISO 8879:1986\",") + .append("\"GlossDef\": {") + .append("\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",") + .append("\"GlossSeeAlso\": [\"GML\", \"XML\"]") + .append("},") + .append("\"GlossSee\": \"markup\"") + .append("}") + .append("}") + .append("}") + .append("}") + .append("}") + .toString(); + } + + /** + * Creates a JSON formatted string with object Id 2. + * @return JSON formatted string with object Id 2. + */ + public static String jsonObject2() { + return new StringBuilder() + .append("{\"menu\": {") + .append("\"coreutils\": {") + .append("\"objectId\": \"2\"") + .append("},") + .append("\"header\": \"SVG Viewer\",") + .append("\"items\": [") + .append("{\"id\": \"Open\"},") + .append("{\"id\": \"OpenNew\", \"label\": \"Open New\"},") + .append("null,") + .append("{\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},") + .append("{\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},") + .append("{\"id\": \"OriginalView\", \"label\": \"Original View\"},") + .append("null,") + .append("{\"id\": \"Quality\"},") + .append("{\"id\": \"Pause\"},") + .append("{\"id\": \"Mute\"},") + .append("null,") + .append("{\"id\": \"Find\", \"label\": \"Find...\"},") + .append("{\"id\": \"FindAgain\", \"label\": \"Find Again\"},") + .append("{\"id\": \"Copy\"},") + .append("{\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},") + .append("{\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},") + .append("{\"id\": \"ViewSVG\", \"label\": \"View SVG\"},") + .append("{\"id\": \"ViewSource\", \"label\": \"View Source\"},") + .append("{\"id\": \"SaveAs\", \"label\": \"Save As\"},") + .append("null,") + .append("{\"id\": \"Help\"},") + .append("{\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}") + .append("]") + .append("}}") + .toString(); + } + + /** + * Creates a XML formatted string with object Id 1. + * @return XML formatted string with object Id 1. + */ + public static String xmlObject1() { + return new StringBuilder() + .append("") + .append("example glossary") + .append("1") + .append("S") + .append("") + .append("") + .append("Standard Generalized Markup Language") + .append("SGML") + .append("ISO 8879:1986") + .append("") + .append("A meta-markup language, used to create markup") + .append("languages such as DocBook.") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .toString(); + } + + /** + * Creates a XML formatted string with object Id 2. + * @return XML formatted string with object Id 2. + */ + public static String xmlObject2() { + return new StringBuilder() + .append("") + .append("2") + .append("
Adobe SVG Viewer
") + .append("Open") + .append("Open New") + .append("") + .append("Zoom In") + .append("Zoom Out") + .append("Original View") + .append("") + .append("Quality") + .append("Pause") + .append("Mute") + .append("") + .append("Find...") + .append("Find Again") + .append("Copy") + .append("Copy Again") + .append("Copy SVG") + .append("View SVG") + .append("View Source") + .append("Save As") + .append("") + .append("Help") + .append("About Adobe CVG Viewer...") + .append("
") + .toString(); + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseValidator.java b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseValidator.java new file mode 100644 index 0000000..ee1ce6f --- /dev/null +++ b/coreutils-fiber/src/test/java/org/grycap/coreutils/fiber/test/mockserver/ObjectResponseValidator.java @@ -0,0 +1,121 @@ +/* + * Core Utils - Fiber-enabled clients. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.fiber.test.mockserver; + +import static java.nio.file.Files.readAllBytes; +import static java.util.Optional.ofNullable; +import static javax.xml.xpath.XPathConstants.STRING; +import static org.apache.commons.lang3.StringUtils.trimToNull; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import javax.annotation.Nullable; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.json.JSONException; +import org.json.JSONObject; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Validates object responses. + * @author Erik Torres + * @since 0.1.0 + */ +public final class ObjectResponseValidator { + + public static boolean isValidXml(final String payload, final String expectedId) { + boolean isValid = false; + try { + final String objectId = readObjectIdFromXml(payload); + isValid = expectedId.equals(trimToNull(objectId)); + } catch (Exception e) { + isValid = false; + e.printStackTrace(); + } + return isValid; + } + + public static void validateXml(final File file, final String expectedId) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { + final String objectId = readObjectIdFromXml(new String(readAllBytes(file.toPath()))); + assertThat("Object Id coincides with expected", objectId, allOf(notNullValue(), not(equalTo("")), equalTo(expectedId))); + } + + @Nullable + private static String readObjectIdFromXml(final String payload) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { + final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(payload.getBytes())); + final XPath xPath = XPathFactory.newInstance().newXPath(); + final XPathExpression xPathExpression = xPath.compile("//*/coreutils/objectId/text()"); + return trimToNull((String) xPathExpression.evaluate(document, STRING)); + } + + public static boolean isValidJson(final String payload, final String expectedId) { + boolean isValid = false; + try { + final String objectId = readObjectIdFromJson(payload); + isValid = expectedId.equals(trimToNull(objectId)); + } catch (Exception e) { + isValid = false; + e.printStackTrace(); + } + return isValid; + } + + public static void validateJson(final File file, final String expectedId) throws IOException, JSONException { + final String objectId = readObjectIdFromJson(new String(readAllBytes(file.toPath()))); + assertThat("Object Id coincides with expected", objectId, allOf(notNullValue(), not(equalTo("")), equalTo(expectedId))); + } + + @Nullable + private static String readObjectIdFromJson(final String payload) throws JSONException { + String objectId = null; + final JSONObject rootElem = new JSONObject(payload); + final String[] names = JSONObject.getNames(rootElem); + if (names != null) { + boolean found = false; + for (int i = 0; i < names.length && !found; i++) { + final JSONObject childElem = rootElem.getJSONObject(names[i]); + if (Arrays.asList(ofNullable(JSONObject.getNames(childElem)).orElse(new String[]{})).contains("coreutils")) { + objectId = trimToNull(childElem.getJSONObject("coreutils").getString("objectId")); + found = true; + } + } + } + return objectId; + } + +} \ No newline at end of file diff --git a/coreutils-fiber/src/test/resources/logback-test-tmpfile.xml b/coreutils-fiber/src/test/resources/logback-test-tmpfile.xml new file mode 100644 index 0000000..781f2a1 --- /dev/null +++ b/coreutils-fiber/src/test/resources/logback-test-tmpfile.xml @@ -0,0 +1,44 @@ + + + + + + true + + + + ${java.io.tmpdir}/coreutils-fiber-test.log + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/coreutils-fiber/src/test/resources/logback-test.xml b/coreutils-fiber/src/test/resources/logback-test.xml new file mode 100644 index 0000000..50a5671 --- /dev/null +++ b/coreutils-fiber/src/test/resources/logback-test.xml @@ -0,0 +1,40 @@ + + + + + + true + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/coreutils-logging/.gitignore b/coreutils-logging/.gitignore new file mode 100644 index 0000000..76729a5 --- /dev/null +++ b/coreutils-logging/.gitignore @@ -0,0 +1,74 @@ + +# Created by https://www.gitignore.io/api/eclipse,java,maven + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + + +### Java ### +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + diff --git a/coreutils-logging/pom.xml b/coreutils-logging/pom.xml new file mode 100644 index 0000000..00b6bc4 --- /dev/null +++ b/coreutils-logging/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + org.grycap.coreutils + coreutils + 0.1.0 + + + coreutils-logging + Common logging utilities + Provides utilities to simplify logging + https://github.com/grycap/coreutils + + + + + org.grycap.coreutils + coreutils-common + ${org.grycap.coreutils.version} + + + org.grycap.coreutils + coreutils-test + ${org.grycap.coreutils.version} + test + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jul-to-slf4j + + + org.slf4j + log4j-over-slf4j + + + \ No newline at end of file diff --git a/coreutils-logging/src/main/java/org/grycap/coreutils/logging/ExtendedLoggerFactory.java b/coreutils-logging/src/main/java/org/grycap/coreutils/logging/ExtendedLoggerFactory.java new file mode 100644 index 0000000..cf9d59f --- /dev/null +++ b/coreutils-logging/src/main/java/org/grycap/coreutils/logging/ExtendedLoggerFactory.java @@ -0,0 +1,50 @@ +/* + * Core Utils - Logging utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.logging; + +import static java.util.Objects.requireNonNull; + +import org.slf4j.Logger; + + +/** + * Wraps the SLF4J {@link org.slf4j.LoggerFactory} + * @author Erik Torres + * @since 0.1.0 + */ +public final class ExtendedLoggerFactory { + + /** + * Uses the method {@link org.slf4j.LoggerFactory#getLogger(Class)} to create a logger named corresponding to the + * class passed as parameter. In addition, this method will configure the logging system, installing the necessary + * bridges to unify logging across the entire application. + * @param clazz - the returned logger will be named after clazz + * @return A logger named corresponding to the class passed as parameter. + */ + public static Logger getLogger(final Class clazz) { + org.grycap.coreutils.logging.LogManager.getLogManager().init(); + return org.slf4j.LoggerFactory.getLogger(requireNonNull(clazz, "A non-null class expected")); + } + +} \ No newline at end of file diff --git a/coreutils-logging/src/main/java/org/grycap/coreutils/logging/LogManager.java b/coreutils-logging/src/main/java/org/grycap/coreutils/logging/LogManager.java new file mode 100644 index 0000000..7d658f4 --- /dev/null +++ b/coreutils-logging/src/main/java/org/grycap/coreutils/logging/LogManager.java @@ -0,0 +1,79 @@ +/* + * Core Utils - Logging utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.logging; + +import static org.slf4j.LoggerFactory.getLogger; +import static org.slf4j.bridge.SLF4JBridgeHandler.install; +import static org.slf4j.bridge.SLF4JBridgeHandler.removeHandlersForRootLogger; +import static org.slf4j.bridge.SLF4JBridgeHandler.uninstall; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; + +/** + * Manages loggers, installing the necessary bridges to unify logging across an entire library, service or application. + * @author Erik Torres + * @since 0.1.0 + */ +public class LogManager { + + private final static Logger LOGGER = getLogger(LogManager.class); + + private final static AtomicBoolean PRISTINE = new AtomicBoolean(true); + + private LogManager() { } + + public static LogManager getLogManager() { + return new LogManager(); + } + + /** + * The logging system is automatically configured when the method {@link LogManager#getLogger(Class)} is called. + * This method is conveniently provided to initialize the logging system before creating any logger with this class. + * Also, you can call this method to initialize the logging system without modifying your code (creating loggers + * with SLF4J or other logging framework). + */ + public void init() { + if (PRISTINE.getAndSet(false)) { + // removes/unregisters/detaches all handlers currently attached to the root logger + removeHandlersForRootLogger(); + // add SLF4JBridgeHandler to j.u.l's root logger (should be done once during the initialization phase of the application) + install(); + LOGGER.info("Log manager was loaded."); + } + } + + /** + * Uninstall the logging bridges returning to the original configuration provided by the individual logging frameworks. + */ + public void reset() { + LOGGER.info("About to reset log manager: no more coherent log messages guaranteed beyond this point."); + // uninstall the SLF4JBridgeHandler instance attached to the jul's root logger + uninstall(); + // removes/unregisters/detaches all handlers currently attached to the root logger + removeHandlersForRootLogger(); + } + +} \ No newline at end of file diff --git a/coreutils-logging/src/test/java/org/grycap/coreutils/logging/test/LogManagerTest.java b/coreutils-logging/src/test/java/org/grycap/coreutils/logging/test/LogManagerTest.java new file mode 100644 index 0000000..0d7a73d --- /dev/null +++ b/coreutils-logging/src/test/java/org/grycap/coreutils/logging/test/LogManagerTest.java @@ -0,0 +1,121 @@ +/* + * Core Utils - Logging utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.logging.test; + +import static org.apache.commons.io.FileUtils.deleteQuietly; +import static org.grycap.coreutils.logging.LogManager.getLogManager; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.grycap.coreutils.test.category.FunctionalTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestRule; +import org.slf4j.Logger; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.util.StatusPrinter; + +/** + * Tests the log manager. + * @author Erik Torres + * @since 0.1.0 + */ +@Category(FunctionalTests.class) +public class LogManagerTest { + + private static final String TESTLOG_FILENAME = new StringBuilder(System.getProperty("java.io.tmpdir")).append(System.getProperty("file.separator")) + .append("coreutils-logging-test.log").toString(); + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + @BeforeClass + public static void cleanup() { + deleteQuietly(new File(TESTLOG_FILENAME)); + } + + @AfterClass + public static void reset() { + getLogManager().reset(); + } + + @Test + public void testFactory() throws Exception { + // setup logging via the factory method that internally calls the initialization method + final Logger logger = org.grycap.coreutils.logging.ExtendedLoggerFactory.getLogger(DummyClass.class); + assertThat("Logger is not null", logger, notNullValue()); + + // j.u.l. logger + String msg = "This message was generated with j.u.l., logged with Logback+SL4J"; + final java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(LogManagerTest.class.getCanonicalName()); + julLogger.setLevel(java.util.logging.Level.INFO); + julLogger.info(msg); + assertThatLogfileContains(msg); + + // log4j logger + msg = "This message was generated with log4j, logged with Logback+SL4J"; + final org.apache.log4j.Logger log4jLogger = org.apache.log4j.Logger.getLogger(LogManagerTest.class); + log4jLogger.setLevel(org.apache.log4j.Level.INFO); + log4jLogger.info(msg); + assertThatLogfileContains(msg); + + // SLF4J logger + msg = "This message was generated with SL4J, logged with Logback+SL4J"; + final org.slf4j.Logger slf4jLogger = org.slf4j.LoggerFactory.getLogger(LogManagerTest.class); + slf4jLogger.info(msg); + assertThatLogfileContains(msg); + + // print logback internal state + if (pw.isPrintEnabled()) { + final LoggerContext loggerContext = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); + StatusPrinter.print(loggerContext); + } + } + + private void assertThatLogfileContains(final String msg) throws IOException { + assertThat("Log file contains expected message", Files.lines(Paths.get(TESTLOG_FILENAME)).anyMatch(line -> line.contains(msg))); + } + + /** + * Dummy class. + * @author Erik Torres + * @since 0.1.0 + */ + public static class DummyClass { } + +} \ No newline at end of file diff --git a/coreutils-logging/src/test/resources/logback-test.xml b/coreutils-logging/src/test/resources/logback-test.xml new file mode 100644 index 0000000..51976c5 --- /dev/null +++ b/coreutils-logging/src/test/resources/logback-test.xml @@ -0,0 +1,51 @@ + + + + + + true + + + + + + ${java.io.tmpdir}/coreutils-logging-test.log + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/coreutils-test/.gitignore b/coreutils-test/.gitignore new file mode 100644 index 0000000..76729a5 --- /dev/null +++ b/coreutils-test/.gitignore @@ -0,0 +1,74 @@ + +# Created by https://www.gitignore.io/api/eclipse,java,maven + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + + +### Java ### +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + diff --git a/coreutils-test/pom.xml b/coreutils-test/pom.xml new file mode 100644 index 0000000..39062a1 --- /dev/null +++ b/coreutils-test/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + + org.grycap.coreutils + coreutils + 0.1.0 + + + coreutils-test + Common testing utilities + Provides utilities to simplify common testing tasks + https://github.com/grycap/coreutils + + + + + com.google.code.findbugs + jsr305 + + + + + com.google.code.gson + gson + + + + + junit + junit + + + + + org.hamcrest + hamcrest-library + + + + + net.jodah + concurrentunit + + + \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/category/FunctionalTests.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/FunctionalTests.java new file mode 100644 index 0000000..1168583 --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/FunctionalTests.java @@ -0,0 +1,31 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.category; + +/** + * Categorize unit tests that belongs to the functional testing group. + * @author Erik Torres + * @since 0.1.0 + */ +public interface FunctionalTests { } \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/category/IntegrationTests.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/IntegrationTests.java new file mode 100644 index 0000000..d22e6dc --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/IntegrationTests.java @@ -0,0 +1,31 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.category; + +/** + * Categorize unit tests that belongs to the integration testing group. + * @author Erik Torres + * @since 0.1.0 + */ +public interface IntegrationTests { } \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/category/SanityTests.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/SanityTests.java new file mode 100644 index 0000000..3e8addb --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/category/SanityTests.java @@ -0,0 +1,31 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.category; + +/** + * Categorize unit tests that belongs to the sanity testing group. + * @author Erik Torres + * @since 0.1.0 + */ +public interface SanityTests { } \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestPrinter.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestPrinter.java new file mode 100644 index 0000000..ff00ea9 --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestPrinter.java @@ -0,0 +1,134 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.rules; + +import static java.util.Optional.ofNullable; + +import javax.annotation.Nullable; + +import org.junit.rules.ExternalResource; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Provides common methods for printing objects during tests execution. This class can be configured to silently ignore + * print operations, producing no output. + * @author Erik Torres + * @since 0.1.0 + */ +public class TestPrinter extends ExternalResource { + + private final boolean print; + + /** + * Default constructor gets its configuration from the system environment. + */ + public TestPrinter() { + this(System.getProperty("grycap.tests.print.out", "false").equals("true")); + } + + /** + * Convenient constructor that receives configuration parameters during class creation. + * @param print - flag to enable/disable message printing to the STDOUT + */ + public TestPrinter(final boolean print) { + this.print = print; + } + + /** + * Checks whether the printer is configured to print the messages or not. + * @return true when the printer is configured to print the messages, otherwise false. + */ + public boolean isPrintEnabled() { + return print; + } + + /** + * Prints a new line character. + */ + public void println() { + println("", OutputFormat.PLAIN); + } + + /** + * Obtains a {@link String} representation of the provided object by calling the {@link Object#toString()} method and + * print its content to the standard output. + * @param obj - object that will be printed + */ + public void println(final @Nullable Object obj) { + println(obj, OutputFormat.PLAIN); + } + + /** + * Prints a {@link String} to the standard output. + * @param msg - message that will be printed + */ + public void println(final String msg) { + println(msg, OutputFormat.PLAIN); + } + + /** + * Obtains a JSON representation of the provided {@link String} and prints its content to the standard output. + * @param msg - message that will be printed + */ + public void printlnJson(final String msg) { + println(msg, OutputFormat.JSON); + } + + /** + * Transform the provided object to one of the supported data formats and print the object's content to the standard output. + * @param obj - object that will be printed + * @param format - data format that will be used to print the object + */ + private void println(final @Nullable Object obj, final @Nullable OutputFormat format) { + switch (ofNullable(format).orElse(OutputFormat.PLAIN)) { + case JSON: + if (print) System.out.println(GsonHelper.gson.toJson(obj)); + break; + case PLAIN: + default: + if (print) System.out.println(obj); + break; + } + } + + /** + * Supported output formats. + * @author Erik Torres + */ + public enum OutputFormat { + JSON, + PLAIN + } + + /** + * Lazy initialization of Gson-based JSON processor. + * @author Erik Torres + */ + private static class GsonHelper { + public static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + } + +} \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestWatcher2.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestWatcher2.java new file mode 100644 index 0000000..f63dd69 --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/rules/TestWatcher2.java @@ -0,0 +1,97 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.rules; + +import static java.util.Optional.ofNullable; + +import javax.annotation.Nullable; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +/** + * Keeps a log of each passing and failing test. + * @author Erik Torres + * @since 0.1.0 + */ +public class TestWatcher2 extends TestWatcher { + + /** + * Default header that will be printed before the status messages. + */ + private static final String DEFAULT_HEADER = " >>> TestUtils >>> "; + + /** + * Default footer that will be printed after the status messages. + */ + private static final String DEFAULT_FOOTER = ""; + + private final TestPrinter pw; + private final String header; + private final String footer; + + /** + * Convenient constructor that configures the class to use the default header and footer. + * @param pw - the printer that will be used to print the status messages + */ + public TestWatcher2(final TestPrinter pw) { + this(pw, DEFAULT_HEADER, DEFAULT_FOOTER); + } + + /** + * Convenient constructor that receives initial values of the class attributes as parameters. + * @param pw - the printer that will be used to print the status messages + * @param header - optional header that will be printed before the status messages or {@link #DEFAULT_HEADER} + * when a null value is passed + * @param footer - optional footer that will be printed after the status messages or {@link #DEFAULT_FOOTER} + * when a null value is passed + */ + public TestWatcher2(final TestPrinter pw, final @Nullable String header, final @Nullable String footer) { + this.pw = pw; + this.header = ofNullable(header).orElse(DEFAULT_HEADER); + this.footer = ofNullable(footer).orElse(DEFAULT_FOOTER);; + } + + @Override + protected void succeeded(final Description description) { + pw.println(new StringBuffer(header).append(description.getDisplayName()).append(" ").append("succeeded!").append(footer).toString()); + } + + @Override + protected void failed(final Throwable e, final Description description) { + pw.println(new StringBuffer(header).append(description.getDisplayName()).append(" ").append("failed!").append(footer).toString()); + } + + @Override + protected void skipped(final AssumptionViolatedException e, final Description description) { + pw.println(new StringBuffer(header).append(description.getDisplayName()).append(" ").append("skipped!").append(footer).toString()); + } + + @Override + protected void starting(final Description description) { + pw.println(new StringBuffer(header).append(description.getDisplayName()).append(" ").append("starting...").append(footer).toString()); + } + +} \ No newline at end of file diff --git a/coreutils-test/src/main/java/org/grycap/coreutils/test/util/ResourceLoadingUtils.java b/coreutils-test/src/main/java/org/grycap/coreutils/test/util/ResourceLoadingUtils.java new file mode 100644 index 0000000..7924f88 --- /dev/null +++ b/coreutils-test/src/main/java/org/grycap/coreutils/test/util/ResourceLoadingUtils.java @@ -0,0 +1,142 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test.util; + +import static java.io.File.separator; +import static java.util.Arrays.asList; +import static java.util.Optional.ofNullable; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +/** + * Utilities to load test resources from class-path and file-system. + * @author Erik Torres + * @since 0.1.0 + */ +public class ResourceLoadingUtils { + + /** + * Test resource directory is scanned for this file in order to find the directory where the application made available the + * files that contain test cases to validate the different test scenarios. A directory named {@link #TEST_DIRNAME} should be + * created at the same level of this file and the test files should be placed there. + */ + public static final String ANCHOR_FILENAME = "grycap-testfiles-m2"; + + /** + * Name of the directory where the test files are located. Files can be organized as needed inside this directory (e.g. using + * subdirectories). + */ + public static final String TEST_DIRNAME = "testfiles"; + + /** + * Finds the directory where the test files are located. + */ + public static final String TEST_RESOURCES_DIR; + static { + String candidateDir = null; + File anchorFile = null; + try { + anchorFile = new File(ResourceLoadingUtils.class.getClassLoader().getResource(ANCHOR_FILENAME).toURI()); + } catch (Exception e) { + anchorFile = new File(System.getProperty("user.dir")); + } finally { + final File resDir = new File(new StringBuffer(anchorFile.getParent()).append(separator).append(TEST_DIRNAME).toString()); + if (resDir != null && resDir.isDirectory() && resDir.canRead()) { + try { + candidateDir = resDir.getCanonicalPath(); + } catch (IOException e) { + // nothing to do + } + } + } + TEST_RESOURCES_DIR = candidateDir; + } + + /** + * Lists the resources with filenames that end with one of the provided extensions. For each matching resource + * the path name is provided. Files are not created to prevent the limits that OS could impose on the simultaneous + * number of opened files. + * @param dirname - name of the target directory + * @param extensions - valid file extensions + * @return The resources with filenames that end with one of the provided extensions. + */ + public static List getResourceFiles(final String dirname, final @Nullable String[] extensions) { + checkResourcesDir(); + final File dir = new File(new StringBuffer(TEST_RESOURCES_DIR).append(separator).append(dirname).toString()); + return asList(ofNullable(dir.listFiles(new ExtensionFilter(extensions))).orElse(new File[]{})).stream() + .map(File::getAbsolutePath) + .collect(Collectors.toList()); + } + + private static void checkResourcesDir() { + if (TEST_RESOURCES_DIR == null) { + throw new IllegalStateException("Invalid test resources pathname: " + TEST_RESOURCES_DIR); + } + } + + /** + * A filename filter that matches the files by their extension. + * @author Erik Torres + * @since 0.1.0 + */ + public static class ExtensionFilter implements FilenameFilter { + + private final List extensions; + + /** + * Convenient constructor to initialize the valid file extensions from an array. + * @param extensions - valid file extensions + */ + public ExtensionFilter(final @Nullable String[] extensions) { + this(asList(ofNullable(extensions).orElse(new String[]{}))); + } + + /** + * Convenient constructor to initialize the valid file extensions from a list. + * @param extensions - valid file extensions + */ + public ExtensionFilter(final @Nullable List extensions) { + this.extensions = ofNullable(extensions).orElse(new ArrayList<>()).stream() + .map(e -> e != null ? e.trim().toLowerCase() : null) + .filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public boolean accept(final File file, final String name) { + final String name2 = (name != null ? name.trim().toLowerCase() : null); + if (name2 == null) return false; + return extensions.stream().anyMatch(e -> name2.endsWith(e)); + } + + } + +} \ No newline at end of file diff --git a/coreutils-test/src/test/java/org/grycap/coreutils/test/ResourceLoadingTest.java b/coreutils-test/src/test/java/org/grycap/coreutils/test/ResourceLoadingTest.java new file mode 100644 index 0000000..8f4bddd --- /dev/null +++ b/coreutils-test/src/test/java/org/grycap/coreutils/test/ResourceLoadingTest.java @@ -0,0 +1,96 @@ +/* + * Core Utils - Testing utilities. + * Copyright 2015-2016 GRyCAP (Universitat Politecnica de Valencia) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This product combines work with different licenses. See the "NOTICE" text + * file for details on the various modules and licenses. + * + * The "NOTICE" text file is part of the distribution. Any derivative works + * that you distribute must include a readable copy of the "NOTICE" text file. + */ + +package org.grycap.coreutils.test; + +import static java.io.File.separator; +import static org.grycap.coreutils.test.util.ResourceLoadingUtils.getResourceFiles; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; + +import java.util.List; + +import org.grycap.coreutils.test.category.FunctionalTests; +import org.grycap.coreutils.test.rules.TestPrinter; +import org.grycap.coreutils.test.rules.TestWatcher2; +import org.grycap.coreutils.test.util.ResourceLoadingUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestRule; + +/** + * Tests resource loading utilities provided by {@link ResourceLoadingUtils}. + * @author Erik Torres + * @since 0.1.0 + */ +@Category(FunctionalTests.class) +public class ResourceLoadingTest { + + @Rule + public TestPrinter pw = new TestPrinter(); + + @Rule + public TestRule watchman = new TestWatcher2(pw); + + @Test + public void testSimpleDirectoryHierarchy() { + // test simple directory hierarchy + final List files = getResourceFiles("examples", new String[]{ ".txt", ".log" }); + assertThat("list of files coincides with expected", files, allOf(notNullValue(), hasSize(3))); + final String filenames = files.stream().map(s -> s + "\n").reduce("", String::concat); + assertThat("all expected files are included", filenames, allOf(containsString("example1.txt"), + containsString("example2.txt"), containsString("example4.log"))); + pw.println("\n >> Files: " + files); + } + + @Test + public void testNestedDirectoryHierarchy() { + // test complex directory hierarchy + final List files = getResourceFiles(new StringBuffer("examples").append(separator) + .append("inner_examples").toString(), new String[]{ ".txt" }); + assertThat("list of files coincides with expected", files, allOf(notNullValue(), hasSize(1))); + final String filenames = files.stream().map(s -> s + "\n").reduce("", String::concat); + assertThat("all expected files are included", filenames, containsString("example.txt")); + pw.println("\n >> Files: " + files); + } + + @Test + public void testInvalidDirectory() { + // test empty directory + final List files = getResourceFiles("i_dont_exist", new String[]{ ".txt" }); + assertThat("list of files coincides with expected", files, allOf(notNullValue(), empty())); + } + + @Test + public void testEmptyExtensionList() { + // test empty extension list + final List files = getResourceFiles("examples", null); + assertThat("list of files coincides with expected", files, allOf(notNullValue(), empty())); + } + +} \ No newline at end of file diff --git a/coreutils-test/src/test/resources/grycap-testfiles-m2 b/coreutils-test/src/test/resources/grycap-testfiles-m2 new file mode 100644 index 0000000..e69de29 diff --git a/coreutils-test/src/test/resources/testfiles/examples/example1.txt b/coreutils-test/src/test/resources/testfiles/examples/example1.txt new file mode 100644 index 0000000..e69de29 diff --git a/coreutils-test/src/test/resources/testfiles/examples/example2.txt b/coreutils-test/src/test/resources/testfiles/examples/example2.txt new file mode 100644 index 0000000..e69de29 diff --git a/coreutils-test/src/test/resources/testfiles/examples/example3 b/coreutils-test/src/test/resources/testfiles/examples/example3 new file mode 100644 index 0000000..e69de29 diff --git a/coreutils-test/src/test/resources/testfiles/examples/example4.log b/coreutils-test/src/test/resources/testfiles/examples/example4.log new file mode 100644 index 0000000..e69de29 diff --git a/coreutils-test/src/test/resources/testfiles/examples/inner_examples/example.txt b/coreutils-test/src/test/resources/testfiles/examples/inner_examples/example.txt new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2b086e5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,331 @@ + + + + 4.0.0 + + org.grycap.coreutils + coreutils + 0.1.0 + pom + + Core utils project + Core utils parent pom + https://github.com/grycap/coreutils + + 2015 + + + GRyCAP + http://www.grycap.upv.es/ + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + https://github.com/grycap/coreutils + scm:git:git://github.com/grycap/coreutils.git + scm:git:git@github.com:grycap/coreutils.git + + + + + etserrano@gmail.com + Erik Torres + https://github.com/etorres + etorres + + + + + 1.8 + 1.8 + + UTF-8 + UTF-8 + + + 1.1.3 + 4.12 + ${project.version} + 1.3 + 3.10.2 + 1.7.13 + + + 1.0.0 + 1.8 + 3.3 + 2.10 + 2.19 + 2.19 + 3.10.2 + + + + + + + org.apache.commons + commons-lang3 + 3.4 + + + commons-io + commons-io + 2.4 + + + + + com.google.code.findbugs + jsr305 + 3.0.1 + + + + + com.google.code.gson + gson + 2.5 + + + + + com.typesafe + config + 1.3.0 + + + + + co.paralleluniverse + quasar-core + 0.7.3 + jdk8 + + + co.paralleluniverse + comsat-httpclient + 0.5.0 + + + co.paralleluniverse + comsat-okhttp + 0.5.0 + + + + + ch.qos.logback + logback-classic + ${ch.qos.logback.version} + + + org.slf4j + slf4j-api + + + + + + + org.slf4j + jul-to-slf4j + ${org.slf4j.version} + + + org.slf4j + log4j-over-slf4j + ${org.slf4j.version} + + + + + junit + junit + ${junit.version} + + + + + org.hamcrest + hamcrest-library + ${org.hamcrest.version} + + + org.hamcrest + hamcrest-core + + + + + + + net.jodah + concurrentunit + 0.4.2 + + + + + org.mock-server + mockserver-integration-testing + ${org.mock-server.version} + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.mock-server + mockserver-maven-plugin + ${mockserver-maven-plugin.version} + + 9080 + WARN + true + + + + pre-integration-test + pre-integration-test + + start + + + + post-integration-test + post-integration-test + + stop + + + + + + org.mock-server + mockserver-integration-testing + ${org.mock-server.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.surefire + surefire-junit47 + ${maven-surefire-plugin.version} + + + + + **/*Test.java + + org.grycap.coreutils.test.category.IntegrationTests + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + org.apache.maven.surefire + surefire-junit47 + ${maven-surefire-plugin.version} + + + + + integration-test + + integration-test + verify + + + + + + **/*Test.java + + org.grycap.coreutils.test.category.IntegrationTests + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + xml + 256m + true + true + + + + + + + + org.codehaus.mojo + cobertura-maven-plugin + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.0.0 + + + + + + coreutils-test + coreutils-common + coreutils-logging + coreutils-fiber + + \ No newline at end of file