diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..29c65d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build and Test + run: mvn clean test post-integration-test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d0f98e9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + tags: [ 'v*' ] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Deploy to Maven Central + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + MAVEN_GPG_PASSPHRASE: '' + run: mvn deploy -DskipTests=true diff --git a/.gitignore b/.gitignore index 0ff5580..fc2d930 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ buildNumber.properties out gen NOTES +.travis/ipdata.out.key diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java old mode 100755 new mode 100644 index d475a89..b901097 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -1,22 +1,18 @@ /* -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you 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. -*/ - + * Copyright 2007-present the original author or authors. + * + * 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. + */ import java.net.*; import java.io.*; import java.nio.channels.*; @@ -24,11 +20,12 @@ Licensed to the Apache Software Foundation (ASF) under one public class MavenWrapperDownloader { + private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ - private static final String DEFAULT_DOWNLOAD_URL = - "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar"; + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to @@ -76,13 +73,13 @@ public static void main(String args[]) { } } } - System.out.println("- Downloading from: : " + url); + System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( - "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); @@ -98,6 +95,16 @@ public static void main(String args[]) { } private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties old mode 100755 new mode 100644 index a5fcc11..642d572 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,2 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip \ No newline at end of file +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe12322..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: required -dist: trusty -language: java -jdk: - - openjdk8 -script: - - mvn clean test post-integration-test -after_success: - - mvn sonar:sonar -Dsonar.projectKey=yassine_ipdata-java-client -addons: - sonarcloud: - organization: yassine-github - token: - secure: PYT5/IbiqFd877vdi+CYTx7srXr5ACvAgBSL4rxPqwVSQUdGIZxVTj8JcZp7Cl2WiDj9zDlTHSDaJDt5ZpXnmN5quJBizuCZDVArm3LoRADjxhOd9N8rQEYEE1TZOHlu1+g1i/abOL+av9Spn7lWk1LvuH5BW3b0GiAdqJFevfnJsUYDh4iYHiMYjTIP5dO/cZUnQzgFgZS6YkMA28cgQIy/F03z6tO9oM6kcJ/DGBnqvdGAT2ZxtWhFaaNwJazlxSePCwnOok+v3vgACzGospTJLQ4AdGklYIK+ljGpz7AbiRs7yhvzsib6i5Wr0IEmZmLiyIgjExPdnV+rXAndgig/0bnhv1YzyEf/hCzMczpHvNa0HDAGCxkTIsw76XlumwPMGURdlypWEoWrz/9cwiPMnQ0XcAOTN56msA5nBbNwnJ9KDKIVWKWg447OhbImlTr+qxHI/L+8e1dFxyA1iBg7/wtU/27V2QRABvhdlVGNn1khdkIKhsueYHbBjloUTILHBFKTtJdZdD2z/Vf9cu51EQDwv5IPfkkz/UER8pw4AVKgFi6v57M3unqE5C6g8dZr5CELAqy3tGjf6v9HaTF3AdGEkyIJuxiiO8LM20MJm+IreR/pIv8SVeT7f747hbPCQ+mMe5rSE4KVtSI88OeUcI193pdd5PBg0jU262I= - - mvn sonar:sonar -cache: - directories: - - '$HOME/.m2/repository' - - '$HOME/.sonar/cache' -notifications: - email: false diff --git a/LICENSE b/LICENSE index f49a4e1..f8f5dd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,21 @@ - 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. \ No newline at end of file +MIT License + +Copyright (c) 2020 IPdata + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b5cd287..619f88d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,178 @@ # ipdata-java-client -![Build Status](https://www.travis-ci.org/yassine/ipdata-java-client.svg?branch=master) -[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?metric=coverage&project=com.github.yassine%3Aipdata-java-client)](https://sonarcloud.io/dashboard/index/com.github.yassine:ipdata-java-client) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?metric=alert_status&project=com.github.yassine%3Aipdata-java-client)](https://sonarcloud.io/dashboard/index/com.github.yassine:ipdata-java-client) -[![Maintainability](https://sonarcloud.io/api/project_badges/measure?metric=sqale_rating&project=com.github.yassine%3Aipdata-java-client)](https://sonarcloud.io/dashboard/index/com.github.yassine:ipdata-java-client) -[![Reliability](https://sonarcloud.io/api/project_badges/measure?metric=reliability_rating&project=com.github.yassine%3Aipdata-java-client)](https://sonarcloud.io/dashboard/index/com.github.yassine:ipdata-java-client) +![Build Status](https://github.com/ipdata/java/actions/workflows/ci.yml/badge.svg) + + +An 100% compliant [ipdata.co](https://ipdata.co) API java client. + +**Table of Contents** + +- [Install](#install) +- [Use](#use) + - [Configuration](#configuration) + - [API](#create-an-instance) + - [Basic Usage](#basic-usage) + - [Single Field Selection](#single-field-selection) + - [Multiple Field Selection](#multiple-field-selection) + - [Bulk data](#bulk-data) + + +## Install +You can have the library from Maven Central. +``` + + co.ipdata.client + ipdata-java-client + 0.2.0 + +``` + +## Use + +### Configuration +A builder is available to help configure your client configuration, you'll have to provide the API endpoint and an [API key](https://ipdata.co/pricing.html): +```java +import io.ipdata.client.Ipdata; + +/.../ +URL url = new URL("https://api.ipdata.co"); +IpdataService ipdataService = Ipdata.builder().url(url) + .key("MY_KEY").get(); +/.../ +``` + +To use the EU endpoint for GDPR compliance, pass the EU API URL instead: +```java +URL url = new URL("https://eu-api.ipdata.co"); +IpdataService ipdataService = Ipdata.builder().url(url) + .key("MY_KEY").get(); +``` + +Optionally, you can configure a cache for faster access (less than 1ms latency on requests that hit the cache). + +The cache is configurable for time and space eviction policies: + +```java +URL url = new URL("https://api.ipdata.co"); +IpdataService ipdataService = Ipdata.builder().url(url) + .withCache() + .timeout(30, TimeUnit.MINUTES) //ttl after first write + .maxSize(8 * 1024) //no more than 8*1024 items shall be stored in cache + .registerCacheConfig() + .key("MY_KEY") + .get(); +IpdataModel model = ipdataService.ipdata("1.1.1.1"); //cache miss here +ipdataService.ipdata("1.1.1.1"); //cache hit from now on on ip address "1.1.1.1" +``` + +### API +The client is fully compliant with the API. The data model of the api is available under the package ``io.ipdata.client.model``. +Interaction with the API is captured by the Service Interface ``io.ipdata.client.service.IpdataService``: + +#### Basic Usage +To get all available information about a given IP address, you can use the ``ipdata`` method of the service Interface: +```java +IpdataModel model = ipdataService.ipdata("1.1.1.1"); +System.out.println(jsonSerialize(model)); +``` + +Output: +```json +{ + "ip": "1.1.1.1", + "is_eu": false, + "city": null, + "region": null, + "region_code": null, + "region_type": null, + "country_name": "Australia", + "country_code": "AU", + "continent_name": "Oceania", + "continent_code": "OC", + "latitude": -33.494, + "longitude": 143.2104, + "postal": null, + "calling_code": "61", + "flag": "https://ipdata.co/flags/au.png", + "emoji_flag": "\ud83c\udde6\ud83c\uddfa", + "emoji_unicode": "U+1F1E6 U+1F1FA", + "asn": { + "asn": "AS13335", + "name": "Cloudflare, Inc.", + "domain": "cloudflare.com", + "route": "1.1.1.0/24", + "type": "hosting" + }, + "company": { + "name": "Cloudflare, Inc.", + "domain": "cloudflare.com", + "network": "1.1.1.0/24", + "type": "hosting" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "Australian Dollar", + "code": "AUD", + "symbol": "AU$", + "native": "$", + "plural": "Australian dollars" + }, + "time_zone": { + "name": "Australia/Sydney", + "abbr": "AEDT", + "offset": "+1100", + "is_dst": true, + "current_time": "2020-01-29T20:22:52.283874+11:00" + }, + "threat": { + "is_tor": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false + }, + "count": "0" +} +``` + +#### Single Field Selection +If you're interested in only one field from the model capturing an IP address information, The service interface +exposes a method on each available field: + +```java +boolean isEu = ipdataService.isEu("1.1.1.1"); +AsnModel asn = ipdataService.asn("1.1.1.1"); +TimeZone tz = ipdataService.timeZone("1.1.1.1"); +ThreatModel threat = ipdataService.threat("1.1.1.1"); +/*...*/ +``` +The list of available fields is available [here](https://docs.ipdata.co/api-reference/response-fields) + +#### Multiple Field Selection +If you're interested in multiple fields for a given IP address, you'll use the ``getFields`` method: +```java +import io.ipdata.client.service.IpdataField; +import io.ipdata.client.service.IpdataService; + +/* The model will be hydrated by the selected fields only */ +IpdataModel model = ipdataService.getFields("1.1.1.1", IpdataField.ASN, IpdataField.CURRENCY); + +``` + +### Bulk data +You can as well get multiple responses at once by using the ``bulk`` api: + +```java +List models = ipdataService.bulk(Arrays.asList("1.1.1.1", "8.8.8.8")); +``` + + -An ipdata.co java client. diff --git a/mvnw b/mvnw index 961a825..41c0f0c 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script +# Maven Start Up Batch script # # Required ENV vars: # ------------------ @@ -114,7 +114,6 @@ if $mingw ; then M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then @@ -212,7 +211,11 @@ else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac @@ -221,22 +224,38 @@ else echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi - wget "$jarUrl" -O "$wrapperJarPath" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi - curl -o "$wrapperJarPath" "$jarUrl" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then @@ -277,6 +296,11 @@ if $cygwin; then MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ diff --git a/mvnw.cmd b/mvnw.cmd old mode 100755 new mode 100644 index 03d90e9..8611571 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,161 +1,182 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.groovy b/pom.groovy deleted file mode 100644 index 01b4b38..0000000 --- a/pom.groovy +++ /dev/null @@ -1,94 +0,0 @@ -project { - modelVersion '4.0.0' - groupId 'io.ipdata.client' - artifactId 'ipdata-java-client' - version '0.1.0-SNAPSHOT' - properties { - 'project.build.sourceEncoding' 'UTF-8' - 'maven.compiler.source' '6' - 'maven.compiler.target' '6' - 'version.client.feign' '9.7.0' - 'version.build.jacoco' '0.8.4' - 'version.build.surefire' '3.0.0-M3' - 'sonar.jacoco.reportPaths' '${project.build.directory}/coverage-reports/jacoco-ut.exec' - 'sonar.links.homepage' 'https://github.com/yassine/ipdata-java-client' - 'sonar.links.issue' 'https://github.com/yassine/ipdata-java-client' - 'sonar.links.scm' 'https://github.com/yassine/ipdata-java-client' - 'sonar.projectKey' 'yassine_ipdata-java-client' - 'sonar.projectName' 'spring-boot-sample' - 'sonar.projectVersion' '${project.version}' - 'sonar.host.url' 'https://sonarcloud.io' - } - dependencies { - dependency('io.github.openfeign:feign-core:${version.client.feign}') - dependency('io.github.openfeign:feign-jackson:${version.client.feign}') - dependency('io.github.openfeign:feign-httpclient:${version.client.feign}') - dependency('com.google.guava:guava:20.0') - dependency('org.slf4j:slf4j-api:1.7.30') - dependency('org.slf4j:slf4j-log4j12:1.7.30') - dependency('org.projectlombok:lombok:1.18.10') - /* testing */ - dependency('org.hamcrest:hamcrest:2.2:test') - dependency('junit:junit:4.13:test') - dependency('org.skyscreamer:jsonassert:1.5.0:test') - } - build { - plugins { - plugin('org.apache.maven.plugins:maven-resources-plugin:2.6') { - configuration { - encoding '${project.build.sourceEncoding}' - } - } - plugin { - groupId 'org.jacoco' - artifactId 'jacoco-maven-plugin' - version '${version.build.jacoco}' - executions { - execution { - id 'prepare-agent' - phase 'test-compile' - goals 'prepare-agent' - configuration { - propertyName 'surefireArgLine' - destFile '${project.build.directory}/coverage-reports/jacoco-ut.exec' - } - } - execution { - id 'post-test-reports' - phase 'post-integration-test' - goals 'report' - configuration { - dataFile '${project.build.directory}/coverage-reports/jacoco-ut.exec' - outputDirectory '${project.reporting.outputDirectory}/code-coverage' - } - } - } - } - plugin { - artifactId 'maven-surefire-plugin' - version '${version.build.surefire}' - configuration { - useFile 'false' - includes {} - additionalClasspathElements { - additionalClasspathElement '${project.basedir}/src/test/resources' - additionalClasspathElement '${project.build.testOutputDirectory}' - } - argLine '${surefireArgLine}' - } - } - plugin { - groupId 'org.codehaus.mojo' - artifactId 'sonar-maven-plugin' - version '3.6.0.1398' - } - } - resources { - resource { - directory 'src/main/resources' - filtering true - includes('VERSION') - } - } - } -} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4b3a67b --- /dev/null +++ b/pom.xml @@ -0,0 +1,215 @@ + + + 4.0.0 + co.ipdata.client + ipdata-java-client + 0.2.1 + A java client for ipdata.co + Ipdata java client + https://github.com/ipdata/java + + + The Apache Licence, Version 2.0 + http://www.apache.org/licenses/LICENCE-2.0.txt + + + + + ipdata.co + support@ipdata.co + + + + scm:git:git@github.com:ipdata/java.git + scm:git:git@github.com:ipdata/java.git + https://github.com/ipdata/java + 0.2.1 + + + + central + + + central + + + + 11 + 11 + UTF-8 + 0.8.14 + 3.5.5 + 11.10 + + + + io.github.openfeign + feign-core + ${version.client.feign} + + + io.github.openfeign + feign-jackson + ${version.client.feign} + + + io.github.openfeign + feign-httpclient + ${version.client.feign} + test + + + com.google.guava + guava + 33.4.8-jre + + + org.slf4j + slf4j-api + 1.7.36 + + + org.slf4j + slf4j-simple + 1.7.36 + test + + + org.projectlombok + lombok + 1.18.38 + provided + + + org.hamcrest + hamcrest + 2.2 + test + + + junit + junit + 4.13.2 + test + + + net.javacrumbs.json-unit + json-unit + 2.40.1 + test + + + + + + true + src/main/resources + + io/ipdata/client/VERSION + + + + + + maven-resources-plugin + 3.3.1 + + ${project.build.sourceEncoding} + + + + org.jacoco + jacoco-maven-plugin + ${version.build.jacoco} + + + prepare-agent + test-compile + + prepare-agent + + + surefireArgLine + ${project.build.directory}/coverage-reports/jacoco-ut.exec + + + + post-test-reports + post-integration-test + + report + + + ${project.build.directory}/coverage-reports/jacoco-ut.exec + ${project.reporting.outputDirectory}/code-coverage + + + + + + maven-surefire-plugin + ${version.build.surefire} + + false + + + ${project.basedir}/src/test/resources + ${project.build.testOutputDirectory} + + ${surefireArgLine} + + + + maven-source-plugin + 3.3.1 + + + + jar + + + + + + maven-javadoc-plugin + 3.12.0 + + + + jar + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 + true + + central + true + + + + maven-gpg-plugin + 3.2.8 + + FB32919AB5830162299F4125C49AB890BA00B57D + + --pinentry-mode + loopback + + + + + verify + + sign + + + + + + + diff --git a/src/main/java/io/ipdata/client/CacheConfigBuilder.java b/src/main/java/io/ipdata/client/CacheConfigBuilder.java index 5be5b7a..039f30c 100644 --- a/src/main/java/io/ipdata/client/CacheConfigBuilder.java +++ b/src/main/java/io/ipdata/client/CacheConfigBuilder.java @@ -1,11 +1,12 @@ package io.ipdata.client; import io.ipdata.client.service.CacheConfig; -import java.util.concurrent.TimeUnit; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; +import java.util.concurrent.TimeUnit; + @Accessors(fluent = true) @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class CacheConfigBuilder { @@ -32,7 +33,7 @@ public CacheConfigBuilder maxSize(long maxSize) { * The maximum duration before invalidating a cache entry. * @param timeout The duration * @param unit The duration unit - * @return + * @return this builder */ public CacheConfigBuilder timeout(int timeout, TimeUnit unit) { if (timeout <= 0) { diff --git a/src/main/java/io/ipdata/client/Ipdata.java b/src/main/java/io/ipdata/client/Ipdata.java index 2714120..cc4c4c7 100644 --- a/src/main/java/io/ipdata/client/Ipdata.java +++ b/src/main/java/io/ipdata/client/Ipdata.java @@ -4,8 +4,6 @@ import io.ipdata.client.service.CacheConfig; import io.ipdata.client.service.IpdataService; import io.ipdata.client.service.IpdataServiceBuilder; -import java.net.URL; -import java.util.concurrent.TimeUnit; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.Setter; @@ -13,10 +11,13 @@ import lombok.experimental.UtilityClass; import org.slf4j.Logger; +import java.net.URL; +import java.util.concurrent.TimeUnit; + @UtilityClass -public final class Ipdata { +public class Ipdata { - public static Ipdata.Builder builder() { + public Ipdata.Builder builder() { return new Builder(); } @@ -51,6 +52,16 @@ public static class Builder { @Setter private Client feignClient; + /** + * Overrides current cache config with null (no caching). + * + * @return this + */ + public Builder noCache() { + this.cacheConfig = null; + return this; + } + /** * Configures a cache with default configuration parameters. * Note: Overrides any previously configured cache. @@ -59,7 +70,7 @@ public static class Builder { */ public Builder withDefaultCache() { return new CacheConfigBuilder(this) - .maxSize(Long.MAX_VALUE) + .maxSize(1024) .timeout(4, TimeUnit.HOURS) .registerCacheConfig(); } diff --git a/src/main/java/io/ipdata/client/model/Asn.java b/src/main/java/io/ipdata/client/model/AsnModel.java similarity index 84% rename from src/main/java/io/ipdata/client/model/Asn.java rename to src/main/java/io/ipdata/client/model/AsnModel.java index 2e2c619..dd30836 100644 --- a/src/main/java/io/ipdata/client/model/Asn.java +++ b/src/main/java/io/ipdata/client/model/AsnModel.java @@ -5,10 +5,10 @@ import lombok.experimental.Accessors; @ToString @Getter @Accessors(fluent = true) -public class Asn { +public class AsnModel { private String asn; private String name; private String domain; private String route; - private String isp; + private String type; } diff --git a/src/main/java/io/ipdata/client/model/Blocklist.java b/src/main/java/io/ipdata/client/model/Blocklist.java new file mode 100644 index 0000000..5ca4201 --- /dev/null +++ b/src/main/java/io/ipdata/client/model/Blocklist.java @@ -0,0 +1,11 @@ +package io.ipdata.client.model; + +import lombok.Getter; +import lombok.ToString; + +@Getter @ToString +public class Blocklist { + private String name; + private String site; + private String type; +} diff --git a/src/main/java/io/ipdata/client/model/Company.java b/src/main/java/io/ipdata/client/model/Company.java new file mode 100644 index 0000000..2c0d349 --- /dev/null +++ b/src/main/java/io/ipdata/client/model/Company.java @@ -0,0 +1,13 @@ +package io.ipdata.client.model; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@ToString @Getter @Accessors(fluent = true) +public class Company { + private String name; + private String domain; + private String network; + private String type; +} diff --git a/src/main/java/io/ipdata/client/model/IpdataModel.java b/src/main/java/io/ipdata/client/model/IpdataModel.java index a99e321..7d8cf62 100644 --- a/src/main/java/io/ipdata/client/model/IpdataModel.java +++ b/src/main/java/io/ipdata/client/model/IpdataModel.java @@ -1,14 +1,12 @@ package io.ipdata.client.model; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; import lombok.ToString; import lombok.experimental.Accessors; -@Setter(AccessLevel.PACKAGE) +import java.util.List; + @ToString @Getter @Accessors(fluent = true) @@ -17,11 +15,14 @@ public class IpdataModel { @JsonProperty("is_eu") private boolean eu; private String city; - private String organization; + private String organisation; + private String region; private String regionCode; + private String regionType; private String countryName; private String countryCode; + private String continentName; private String continentCode; private double latitude; private double longitude; @@ -30,14 +31,15 @@ public class IpdataModel { private String flag; private String emojiFlag; private String emojiUnicode; - private Asn asn; + private AsnModel asn; private Carrier carrier; + private Company company; private List languages; private Currency currency; private TimeZone timeZone; - private Threat threat; + private ThreatModel threat; //meta - private int count; + private String count; public boolean isEu() { return eu; diff --git a/src/main/java/io/ipdata/client/model/Language.java b/src/main/java/io/ipdata/client/model/Language.java index 365e417..395358c 100644 --- a/src/main/java/io/ipdata/client/model/Language.java +++ b/src/main/java/io/ipdata/client/model/Language.java @@ -10,5 +10,5 @@ public class Language { private String name; @JsonProperty("native") private String nativeName; - private boolean rtl; + private int rtl; } diff --git a/src/main/java/io/ipdata/client/model/Scores.java b/src/main/java/io/ipdata/client/model/Scores.java new file mode 100644 index 0000000..5049a83 --- /dev/null +++ b/src/main/java/io/ipdata/client/model/Scores.java @@ -0,0 +1,17 @@ +package io.ipdata.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@Getter @ToString +public class Scores { + @JsonProperty("vpn_score") + private int vpnScore; + @JsonProperty("proxy_score") + private int proxyScore; + @JsonProperty("threat_score") + private int threatScore; + @JsonProperty("trust_score") + private int trustScore; +} diff --git a/src/main/java/io/ipdata/client/model/Threat.java b/src/main/java/io/ipdata/client/model/ThreatModel.java similarity index 65% rename from src/main/java/io/ipdata/client/model/Threat.java rename to src/main/java/io/ipdata/client/model/ThreatModel.java index 6a8e3e6..8b96baa 100644 --- a/src/main/java/io/ipdata/client/model/Threat.java +++ b/src/main/java/io/ipdata/client/model/ThreatModel.java @@ -1,13 +1,16 @@ package io.ipdata.client.model; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Getter; import lombok.ToString; @Getter @ToString -public class Threat { +public class ThreatModel { @JsonProperty("is_tor") private boolean tor; + @JsonProperty("is_vpn") + private boolean vpn; @JsonProperty("is_proxy") private boolean proxy; @JsonProperty("is_anonymous") @@ -20,4 +23,10 @@ public class Threat { private boolean threat; @JsonProperty("is_bogon") private boolean bogon; + @JsonProperty("is_icloud_relay") + private boolean icloudRelay; + @JsonProperty("is_datacenter") + private boolean datacenter; + private List blocklists; + private Scores scores; } diff --git a/src/main/java/io/ipdata/client/model/TimeZone.java b/src/main/java/io/ipdata/client/model/TimeZone.java index 48db583..a71e820 100644 --- a/src/main/java/io/ipdata/client/model/TimeZone.java +++ b/src/main/java/io/ipdata/client/model/TimeZone.java @@ -1,5 +1,6 @@ package io.ipdata.client.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.ToString; import lombok.experimental.Accessors; @@ -10,6 +11,7 @@ public class TimeZone { private String name; private String abbr; private String offset; - private String isDst; - private String currencyTime; + @JsonProperty("is_dst") + private boolean dst; + private String currentTime; } diff --git a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java index 9f68fe4..e526eb7 100644 --- a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java +++ b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java @@ -7,12 +7,15 @@ import io.ipdata.client.error.RateLimitException; import io.ipdata.client.error.RemoteIpdataException; import io.ipdata.client.model.Error; -import java.io.IOException; -import java.net.URL; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; +import java.io.IOException; +import java.net.URL; + @RequiredArgsConstructor +@Slf4j public class ApiErrorDecoder implements ErrorDecoder { private static final int RATE_LIMIT_STATUS = 403; private final ObjectMapper mapper; @@ -35,7 +38,7 @@ public Exception decode(String methodKey, Response response) { return new RemoteIpdataException(error.getMessage(), response.status()); } } catch (IOException ioException) { - ioException.printStackTrace(); + log.error(ioException.getMessage(), ioException); return new RemoteIpdataException(message, response.status()); } } diff --git a/src/main/java/io/ipdata/client/service/ApiKeyRequestInterceptor.java b/src/main/java/io/ipdata/client/service/ApiKeyRequestInterceptor.java index 134ad4b..a361660 100644 --- a/src/main/java/io/ipdata/client/service/ApiKeyRequestInterceptor.java +++ b/src/main/java/io/ipdata/client/service/ApiKeyRequestInterceptor.java @@ -3,29 +3,35 @@ import com.google.common.io.CharStreams; import feign.RequestInterceptor; import feign.RequestTemplate; -import java.io.InputStreamReader; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; @RequiredArgsConstructor +@Slf4j class ApiKeyRequestInterceptor implements RequestInterceptor { private static final String API_KEY_PARAM = "api-key"; private static final String ACCEPT_HEADER = "Accept"; - private static final String JSON_CONTENT = "application/json"; + private static final String JSON_CONTENT = "application/json"; private static final String API_CLIENT_HEADER = "User-Agent"; private static final String API_CLIENT_VALUE; + static { String version; try { version = CharStreams.toString(new InputStreamReader(ApiKeyRequestInterceptor.class - .getResourceAsStream("/VERSION"))).replaceAll("\\n",""); - version = String.format("io.ipdata.client.java.%s", version); - }catch (Exception e){ - e.printStackTrace(); - version = "io.ipdata.client.java.UNKNOWN"; + .getResourceAsStream("/io/ipdata/client/VERSION"), StandardCharsets.UTF_8)).replaceAll("\\n", ""); + version = String.format("io.ipdata.client.java.%s", version); + } catch (Exception e) { + log.error(e.getMessage(), e); + version = "io.ipdata.client.java.UNKNOWN"; } API_CLIENT_VALUE = version; } + private final String key; @Override diff --git a/src/main/java/io/ipdata/client/service/CacheConfig.java b/src/main/java/io/ipdata/client/service/CacheConfig.java index 75b2f1b..714f7aa 100644 --- a/src/main/java/io/ipdata/client/service/CacheConfig.java +++ b/src/main/java/io/ipdata/client/service/CacheConfig.java @@ -1,11 +1,12 @@ package io.ipdata.client.service; -import java.util.concurrent.TimeUnit; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; +import java.util.concurrent.TimeUnit; + @RequiredArgsConstructor @Builder @Getter diff --git a/src/main/java/io/ipdata/client/service/CachingInternalClient.java b/src/main/java/io/ipdata/client/service/CachingInternalClient.java index 5fcfdff..08de093 100644 --- a/src/main/java/io/ipdata/client/service/CachingInternalClient.java +++ b/src/main/java/io/ipdata/client/service/CachingInternalClient.java @@ -1,59 +1,57 @@ package io.ipdata.client.service; -import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.LoadingCache; import io.ipdata.client.error.IpdataException; -import io.ipdata.client.model.Asn; -import io.ipdata.client.model.Currency; -import io.ipdata.client.model.IpdataModel; -import io.ipdata.client.model.Threat; -import io.ipdata.client.model.TimeZone; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +import io.ipdata.client.model.*; import lombok.Builder; import lombok.experimental.Delegate; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; @Builder -@VisibleForTesting - -public class CachingInternalClient implements IpdataInternalClient, IpdataInternalSingleFieldClient { +class CachingInternalClient implements IpdataInternalClient, IpdataInternalSingleFieldClient { @Builder.Default - @SuppressWarnings("UnusedAssignment") //required by lombok builder - private int expiry = 4; + private final int expiry = 4; @Builder.Default - @SuppressWarnings("UnusedAssignment") //required by lombok builder - private TimeUnit unit = TimeUnit.HOURS; + private final TimeUnit unit = TimeUnit.HOURS; @Builder.Default - @SuppressWarnings("UnusedAssignment") //required by lombok builder - private Long maxSize = Long.MAX_VALUE; - private IpdataInternalClient ipdataInternalClient; - private IpdataInternalSingleFieldClient ipdataInternalSingleFieldClient; - - private LoadingCache ipdataCache; - private LoadingCache, IpdataModel> fieldsCache; - private LoadingCache asnCache; - private LoadingCache tzCache; - private LoadingCache currencyCache; - private LoadingCache threatCache; + private final Long maxSize = Long.MAX_VALUE; + private final IpdataInternalClient ipdataInternalClient; + private final IpdataInternalSingleFieldClient ipdataInternalSingleFieldClient; + + private final LoadingCache ipdataCache; + private final LoadingCache, IpdataModel> fieldsCache; + private final LoadingCache asnCache; + private final LoadingCache tzCache; + private final LoadingCache currencyCache; + private final LoadingCache threatCache; + + private static IpdataException unwrap(ExecutionException e) throws IpdataException { + Throwable cause = e.getCause(); + if (cause instanceof IpdataException) { + throw (IpdataException) cause; + } + throw new IpdataException(cause != null ? cause.getMessage() : e.getMessage(), cause != null ? cause : e); + } @Override public IpdataModel getFields(String ip, String fields) throws IpdataException { try { return fieldsCache.get(HashPair.of(ip, fields)); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @Override - public Asn asn(String ip) throws IpdataException { + public AsnModel asn(String ip) throws IpdataException { try { return asnCache.get(ip); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @@ -62,7 +60,7 @@ public TimeZone timeZone(String ip) throws IpdataException { try { return tzCache.get(ip); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @@ -71,16 +69,16 @@ public Currency currency(String ip) throws IpdataException { try { return currencyCache.get(ip); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @Override - public Threat threat(String ip) throws IpdataException { + public ThreatModel threat(String ip) throws IpdataException { try { return threatCache.get(ip); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @@ -89,28 +87,28 @@ public IpdataModel ipdata(String ip) throws IpdataException { try { return ipdataCache.get(ip); } catch (ExecutionException e) { - throw new IpdataException(e.getMessage(), e); + throw unwrap(e); } } @Override - public List bulkIpdata(List ips) throws IpdataException { - return ipdataInternalClient.bulkIpdata(ips); + public List bulk(List ips) throws IpdataException { + return ipdataInternalClient.bulk(ips); } - @Delegate(types = IpdataInternalSingleFieldClient.class, excludes = $DelegateExcludes.class) + @Delegate(types = IpdataInternalSingleFieldClient.class, excludes = DelegateExcludes.class) IpdataInternalSingleFieldClient getIpdataInternalSingleFieldClient() { return ipdataInternalSingleFieldClient; } - private interface $DelegateExcludes { - Asn asn(String ip); + private interface DelegateExcludes { + AsnModel asn(String ip); TimeZone timeZone(String ip); Currency currency(String ip); - Threat threat(String ip); + ThreatModel threat(String ip); } } diff --git a/src/main/java/io/ipdata/client/service/FieldDecoder.java b/src/main/java/io/ipdata/client/service/FieldDecoder.java index d6a5eae..1eb1b58 100644 --- a/src/main/java/io/ipdata/client/service/FieldDecoder.java +++ b/src/main/java/io/ipdata/client/service/FieldDecoder.java @@ -4,9 +4,10 @@ import com.google.common.io.CharStreams; import feign.Response; import feign.codec.Decoder; +import lombok.RequiredArgsConstructor; + import java.io.IOException; import java.lang.reflect.Type; -import lombok.RequiredArgsConstructor; /** * The API returns usable but invalid(/unquoted) strings for String fields diff --git a/src/main/java/io/ipdata/client/service/IpdataField.java b/src/main/java/io/ipdata/client/service/IpdataField.java index 91cd21c..1b960da 100644 --- a/src/main/java/io/ipdata/client/service/IpdataField.java +++ b/src/main/java/io/ipdata/client/service/IpdataField.java @@ -1,10 +1,6 @@ package io.ipdata.client.service; -import io.ipdata.client.model.Asn; -import io.ipdata.client.model.Carrier; -import io.ipdata.client.model.Currency; -import io.ipdata.client.model.Language; -import io.ipdata.client.model.TimeZone; +import io.ipdata.client.model.*; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -20,12 +16,15 @@ public class IpdataField { public static final IpdataField IS_EU = new IpdataField("is_eu", Boolean.class); public static final IpdataField CITY = new IpdataField("city", String.class); public static final IpdataField REGION = new IpdataField("region", String.class); + public static final IpdataField REGION_CODE = new IpdataField("region_code", String.class); + public static final IpdataField REGION_TYPE = new IpdataField("region_type", String.class); public static final IpdataField COUNTRY_NAME = new IpdataField("country_name", String.class); public static final IpdataField COUNTRY_CODE = new IpdataField("country_code", String.class); public static final IpdataField CONTINENT_CODE = new IpdataField("continent_code", String.class); + public static final IpdataField CONTINENT_NAME = new IpdataField("continent_name", String.class); public static final IpdataField LATITUDE = new IpdataField("latitude", Double.class); public static final IpdataField LONGITUDE = new IpdataField("longitude", Double.class); - public static final IpdataField ASN = new IpdataField("asn", Asn.class); + public static final IpdataField ASN = new IpdataField("asn", AsnModel.class); public static final IpdataField ORGANISATION = new IpdataField("organisation", String.class); public static final IpdataField POSTAL = new IpdataField("postal", String.class); public static final IpdataField CALLING_CODE = new IpdataField("calling_code", String.class); @@ -33,13 +32,14 @@ public class IpdataField { public static final IpdataField EMOJI_FLAG = new IpdataField("emoji_flag", String.class); public static final IpdataField EMOJI_UNICODE = new IpdataField("emoji_unicode", String.class); public static final IpdataField CARRIER = new IpdataField("carrier", Carrier.class); + public static final IpdataField COMPANY = new IpdataField("company", Company.class); public static final IpdataField LANGUAGES = new IpdataField("languages", Language.class); public static final IpdataField CURRENCY = new IpdataField("currency", Currency.class); public static final IpdataField TIME_ZONE = new IpdataField("time_zone", TimeZone.class); - public static final IpdataField THREAT = new IpdataField("threat", TimeZone.class); + public static final IpdataField THREAT = new IpdataField("threat", ThreatModel.class); public static final IpdataField COUNT = new IpdataField("count", Integer.class); private final String name; - private final Class type; + private final Class type; @Override public String toString() { diff --git a/src/main/java/io/ipdata/client/service/IpdataInternalClient.java b/src/main/java/io/ipdata/client/service/IpdataInternalClient.java index c02e7ab..8a79750 100644 --- a/src/main/java/io/ipdata/client/service/IpdataInternalClient.java +++ b/src/main/java/io/ipdata/client/service/IpdataInternalClient.java @@ -4,18 +4,44 @@ import feign.RequestLine; import io.ipdata.client.error.IpdataException; import io.ipdata.client.model.IpdataModel; + import java.util.List; -@SuppressWarnings("RedundantThrows") +/* + +For http protocol, the ':' character is actually tolerated in a path segment. feign library seems to encode all reserved +characters in the same way, i.e. regardless of their usage (path param or query param), according to global restrictions. +For IPV6 addresses, the path parameter includes colons ':' that gets encoded according to global restrictions rules, +while they are still tolerated in a path segment. + +In order to by pass this restrictive behavior, encoding is disabled for the ip path as validation is performed +server-side for it. + +From RFC 1738: + +Section 3.3, Page 9 +'Within the and components, "/", ";", "?" are reserved.' + +Section 5, Page 20 : globally reserved characters +reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" + +Section 5, Page 18 : +; HTTP + httpurl = "http://" hostport [ "/" hpath [ "?" search ]] + hpath = hsegment *[ "/" hsegment ] + hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ] <---- ':' is tolerated for a path segment + search = *[ uchar | ";" | ":" | "@" | "&" | "=" ] + +*/ interface IpdataInternalClient { @Cacheable @RequestLine("GET /{ip}") - IpdataModel ipdata(@Param("ip") String ip) throws IpdataException; + IpdataModel ipdata(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("POST /bulk") - List bulkIpdata(List ips) throws IpdataException; + List bulk(List ips) throws IpdataException; @Cacheable @RequestLine("GET /{ip}?fields={fields}") - IpdataModel getFields(@Param("ip") String ip, @Param("fields") String fields) throws IpdataException; + IpdataModel getFields(@Param(value = "ip", encoded = true) String ip, @Param("fields") String fields) throws IpdataException; } diff --git a/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java b/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java index 2d39366..5f55f7a 100644 --- a/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java +++ b/src/main/java/io/ipdata/client/service/IpdataInternalSingleFieldClient.java @@ -3,70 +3,69 @@ import feign.Param; import feign.RequestLine; import io.ipdata.client.error.IpdataException; -import io.ipdata.client.model.Asn; +import io.ipdata.client.model.AsnModel; import io.ipdata.client.model.Currency; -import io.ipdata.client.model.Threat; +import io.ipdata.client.model.ThreatModel; import io.ipdata.client.model.TimeZone; -@SuppressWarnings("RedundantThrows") interface IpdataInternalSingleFieldClient { @RequestLine("GET /{ip}/ip") - String getIp(@Param("ip") String ip) throws IpdataException; + String getIp(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/is_eu") - boolean isEu(@Param("ip") String ip) throws IpdataException; + boolean isEu(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/city") - String getCity(@Param("ip") String ip) throws IpdataException; + String getCity(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/country_name") - String getCountryName(@Param("ip") String ip) throws IpdataException; + String getCountryName(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/country_code") - String getCountryCode(@Param("ip") String ip) throws IpdataException; + String getCountryCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/continent_code") - String getContinentCode(@Param("ip") String ip) throws IpdataException; + String getContinentCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/longitude") - double getLongitude(@Param("ip") String ip) throws IpdataException; + double getLongitude(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/latitude") - double getLatitude(@Param("ip") String ip) throws IpdataException; + double getLatitude(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/organisation") - String getOrganisation(@Param("ip") String ip) throws IpdataException; + String getOrganisation(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/postal") - String getPostal(@Param("ip") String ip) throws IpdataException; + String getPostal(@Param(value = "ip", encoded = true) String ip) throws IpdataException; - @RequestLine("GET /{ip}/asn") - String getCallingCode(@Param("ip") String ip) throws IpdataException; + @RequestLine("GET /{ip}/calling_code") + String getCallingCode(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/flag") - String getFlag(@Param("ip") String ip) throws IpdataException; + String getFlag(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/emoji_flag") - String getEmojiFlag(@Param("ip") String ip) throws IpdataException; + String getEmojiFlag(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @RequestLine("GET /{ip}/emoji_unicode") - String getEmojiUnicode(@Param("ip") String ip) throws IpdataException; + String getEmojiUnicode(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @Cacheable @RequestLine("GET /{ip}/asn") - Asn asn(@Param("ip") String ip) throws IpdataException; + AsnModel asn(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @Cacheable @RequestLine("GET /{ip}/time_zone") - TimeZone timeZone(@Param("ip") String ip) throws IpdataException; + TimeZone timeZone(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @Cacheable @RequestLine("GET /{ip}/currency") - Currency currency(@Param("ip") String ip) throws IpdataException; + Currency currency(@Param(value = "ip", encoded = true) String ip) throws IpdataException; @Cacheable @RequestLine("GET /{ip}/threat") - Threat threat(@Param("ip") String ip) throws IpdataException; + ThreatModel threat(@Param(value = "ip", encoded = true) String ip) throws IpdataException; } diff --git a/src/main/java/io/ipdata/client/service/IpdataService.java b/src/main/java/io/ipdata/client/service/IpdataService.java index 6e13069..d83d522 100644 --- a/src/main/java/io/ipdata/client/service/IpdataService.java +++ b/src/main/java/io/ipdata/client/service/IpdataService.java @@ -2,16 +2,51 @@ import io.ipdata.client.error.IpdataException; import io.ipdata.client.model.IpdataModel; + import java.util.List; -@SuppressWarnings("RedundantThrows") +/** + * Primary interface for accessing the ipdata.co API. + *

+ * Provides methods for looking up geolocation, threat intelligence, and other + * metadata for IP addresses. Supports single IP lookups, bulk lookups, and + * selective field retrieval. + *

+ * Also exposes single-field accessors (e.g. {@code getCountryName}, {@code getCity}) + * inherited from {@link IpdataInternalSingleFieldClient}. + * + * @see io.ipdata.client.Ipdata#builder() + */ public interface IpdataService extends IpdataInternalSingleFieldClient { + /** + * Retrieves the full IP data model for the given IP address. + * + * @param ip an IPv4 or IPv6 address + * @return the full geolocation and metadata for the IP + * @throws IpdataException if the API call fails + */ IpdataModel ipdata(String ip) throws IpdataException; - List bulkIpdata(List ips) throws IpdataException; - - IpdataModel[] bulkIpdataAsArray(List ips) throws IpdataException; + /** + * Retrieves IP data for multiple IP addresses in a single request. + * + * @param ips list of IPv4 or IPv6 addresses + * @return a list of IP data models, one per input address + * @throws IpdataException if the API call fails + */ + List bulk(List ips) throws IpdataException; + /** + * Retrieves only the specified fields for the given IP address. + *

+ * Fields are sorted before querying to maximize cache hit rates when caching is enabled. + * + * @param ip an IPv4 or IPv6 address + * @param fields one or more fields to retrieve (e.g. {@code IpdataField.ASN}, {@code IpdataField.CURRENCY}) + * @return a partial IP data model containing only the requested fields + * @throws IpdataException if the API call fails + * @throws IllegalArgumentException if no fields are specified + */ IpdataModel getFields(String ip, IpdataField... fields) throws IpdataException; } diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java index 65d6cdf..f294cc2 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java @@ -1,7 +1,5 @@ package io.ipdata.client.service; -import static com.fasterxml.jackson.databind.PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES; - import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -11,19 +9,17 @@ import com.google.common.cache.CacheLoader; import feign.Client; import feign.Feign; -import feign.httpclient.ApacheHttpClient; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; -import io.ipdata.client.model.Asn; -import io.ipdata.client.model.Currency; -import io.ipdata.client.model.IpdataModel; -import io.ipdata.client.model.Threat; -import io.ipdata.client.model.TimeZone; -import java.net.URL; +import io.ipdata.client.model.*; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; + @RequiredArgsConstructor(staticName = "of") public class IpdataServiceBuilder { @@ -35,7 +31,7 @@ public class IpdataServiceBuilder { public IpdataService build() { final ObjectMapper mapper = new ObjectMapper(); - mapper.setPropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + mapper.setPropertyNamingStrategy(SNAKE_CASE); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); @@ -45,7 +41,7 @@ public IpdataService build() { final ApiErrorDecoder apiErrorDecoder = new ApiErrorDecoder(mapper, customLogger); final IpdataInternalClient client = Feign.builder() - .client(httpClient == null ? new ApacheHttpClient() : httpClient) + .client(new Ipv6SafeClient(httpClient == null ? new Client.Default(null, null) : httpClient)) .decoder(new JacksonDecoder(mapper)) .encoder(new JacksonEncoder(mapper)) .requestInterceptor(keyRequestInterceptor) @@ -53,7 +49,7 @@ public IpdataService build() { .target(IpdataInternalClient.class, url.toString()); final IpdataInternalSingleFieldClient singleFieldClient = Feign.builder() - .client(httpClient == null ? new ApacheHttpClient() : httpClient) + .client(new Ipv6SafeClient(httpClient == null ? new Client.Default(null, null) : httpClient)) .decoder(new FieldDecoder(mapper)) .encoder(new JacksonEncoder(mapper)) .requestInterceptor(keyRequestInterceptor) @@ -91,9 +87,9 @@ public IpdataModel load(HashPair key) throws Exception { CacheBuilder.newBuilder() .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) .maximumSize(cacheConfig.maxSize()) - .build(new CacheLoader() { + .build(new CacheLoader() { @Override - public Asn load(String key) throws Exception { + public AsnModel load(String key) throws Exception { return singleFieldClient.asn(key); } }) @@ -124,9 +120,9 @@ public Currency load(String key) throws Exception { CacheBuilder.newBuilder() .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) .maximumSize(cacheConfig.maxSize()) - .build(new CacheLoader() { + .build(new CacheLoader() { @Override - public Threat load(String key) throws Exception { + public ThreatModel load(String key) throws Exception { return singleFieldClient.threat(key); } }) diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java index bac4378..7531ba8 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java @@ -3,13 +3,14 @@ import com.google.common.base.Joiner; import io.ipdata.client.error.IpdataException; import io.ipdata.client.model.IpdataModel; -import java.util.Arrays; -import java.util.List; import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; +import java.util.Arrays; +import java.util.List; + @RequiredArgsConstructor @Builder @Slf4j @@ -29,15 +30,10 @@ private IpdataInternalSingleFieldClient getApi() { return singleFieldClient; } - @Override - public IpdataModel[] bulkIpdataAsArray(List ips) throws IpdataException { - return bulkIpdata(ips).toArray(new IpdataModel[0]); - } - @Override public IpdataModel getFields(String ip, IpdataField... fields) throws IpdataException { if (fields.length == 0) { - return null; + throw new IllegalArgumentException("At least one field must be specified"); } //sorting here to improve the likelihood of a cache hit, otherwise a permutation of the same //array would result into a different cache key, and thus a cache miss diff --git a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java new file mode 100644 index 0000000..f8f6f38 --- /dev/null +++ b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java @@ -0,0 +1,44 @@ +package io.ipdata.client.service; + +import feign.Client; +import feign.Request; +import feign.Response; + +import java.io.IOException; + +/** + * A Feign Client wrapper that prevents percent-encoding of colons in the request path. + *

+ * Feign's template engine may percent-encode colons in path parameters (e.g., IPv6 addresses), + * converting {@code 2001:4860:4860::8888} to {@code 2001%3A4860%3A4860%3A%3A8888}. + * Colons are valid in URI path segments per RFC 3986 section 3.3, so this wrapper + * decodes them before forwarding to the underlying HTTP client. + * + * @see Issue #10 + */ +class Ipv6SafeClient implements Client { + + private final Client delegate; + + Ipv6SafeClient(Client delegate) { + this.delegate = delegate; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + String url = request.url(); + int queryIndex = url.indexOf('?'); + String path = queryIndex >= 0 ? url.substring(0, queryIndex) : url; + + if (path.contains("%3A") || path.contains("%3a")) { + String fixedPath = path.replace("%3A", ":").replace("%3a", ":"); + String query = queryIndex >= 0 ? url.substring(queryIndex) : ""; + String fixedUrl = fixedPath + query; + request = Request.create( + request.httpMethod(), fixedUrl, request.headers(), + request.body(), request.charset() + ); + } + return delegate.execute(request, options); + } +} diff --git a/src/main/resources/VERSION b/src/main/resources/io/ipdata/client/VERSION similarity index 100% rename from src/main/resources/VERSION rename to src/main/resources/io/ipdata/client/VERSION diff --git a/src/test/java/io/ipdata/client/AsnTest.java b/src/test/java/io/ipdata/client/AsnTest.java new file mode 100644 index 0000000..5ae0102 --- /dev/null +++ b/src/test/java/io/ipdata/client/AsnTest.java @@ -0,0 +1,61 @@ +package io.ipdata.client; + +import feign.httpclient.ApacheHttpClient; +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.model.AsnModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +@RunWith(Parameterized.class) +public class AsnTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public TestFixture fixture; + + @Test + @SneakyThrows + public void testASN() { + IpdataService ipdataService = fixture.service(); + AsnModel asn = ipdataService.asn(fixture.target()); + assertNotNull(asn.type()); /* See: https://github.com/ipdata/java/issues/2 */ + String actual = TEST_CONTEXT.mapper().writeValueAsString(asn); + String expected = TEST_CONTEXT.get("/"+fixture.target()+"/asn", null); + TEST_CONTEXT.assertEqualJson(actual, expected); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + asn = ipdataService.asn(fixture.target()); + assertNotNull(asn.type()); + actual = TEST_CONTEXT.mapper().writeValueAsString(asn); + TEST_CONTEXT.assertEqualJson(actual, expected); + } + } + + @SneakyThrows + @Test(expected = IpdataException.class) + public void testAsnError() { + IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url()) + .key("THIS_IS_AN_INVALID_KEY") + .withDefaultCache() + .feignClient(new ApacheHttpClient(HttpClientBuilder.create() + .setConnectionTimeToLive(10, TimeUnit.SECONDS) + .build()) + ).get(); + serviceWithInvalidKey.asn(fixture.target()); + } + + @Parameterized.Parameters + public static Iterable data() { + return TEST_CONTEXT.fixtures(); + } + +} diff --git a/src/test/java/io/ipdata/client/BulkTest.java b/src/test/java/io/ipdata/client/BulkTest.java new file mode 100644 index 0000000..916def4 --- /dev/null +++ b/src/test/java/io/ipdata/client/BulkTest.java @@ -0,0 +1,48 @@ +package io.ipdata.client; + +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Arrays.asList; + +@RunWith(Parameterized.class) +public class BulkTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public IpdataService ipdataService; + + @SneakyThrows + @Test + public void testBulkResponse() { + List ipdataModels = ipdataService.bulk(Arrays.asList("8.8.8.8", "1.1.1.1")); + String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModels); + String expected = TEST_CONTEXT.post("/bulk", "[\"8.8.8.8\",\"1.1.1.1\"]", null); + expected = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(expected, IpdataModel[].class)); + TEST_CONTEXT.assertEqualJson(expected, actual, TEST_CONTEXT.configuration().whenIgnoringPaths("[0].time_zone.current_time", "[1].time_zone.current_time", "[0].count", "[1].count")); + } + + @SneakyThrows + @Test + public void testBulkResponseIpv6() { + List ipdataModels = ipdataService.bulk(Arrays.asList("2001:4860:4860::8888", "2001:4860:4860::8844")); + String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModels); + String expected = TEST_CONTEXT.post("/bulk", "[\"2001:4860:4860::8888\",\"2001:4860:4860::8844\"]", null); + expected = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(expected, IpdataModel[].class)); + TEST_CONTEXT.assertEqualJson(expected, actual, TEST_CONTEXT.configuration().whenIgnoringPaths("[0].time_zone.current_time", "[1].time_zone.current_time", "[0].count", "[1].count")); + } + + @Parameterized.Parameters + public static Iterable data() { + return asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService()); + } + +} diff --git a/src/test/java/io/ipdata/client/CurrencyTest.java b/src/test/java/io/ipdata/client/CurrencyTest.java new file mode 100644 index 0000000..78ea1c6 --- /dev/null +++ b/src/test/java/io/ipdata/client/CurrencyTest.java @@ -0,0 +1,57 @@ +package io.ipdata.client; + +import feign.httpclient.ApacheHttpClient; +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.model.Currency; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.concurrent.TimeUnit; + +@RunWith(Parameterized.class) +public class CurrencyTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public TestFixture fixture; + + @Test + @SneakyThrows + public void testCurrency() { + IpdataService ipdataService = fixture.service(); + Currency currency = ipdataService.currency(fixture.target()); + String actual = TEST_CONTEXT.mapper().writeValueAsString(currency); + String expected = TEST_CONTEXT.get("/"+fixture.target()+"/currency", null); + TEST_CONTEXT.assertEqualJson(actual, expected); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + currency = ipdataService.currency(fixture.target()); + actual = TEST_CONTEXT.mapper().writeValueAsString(currency); + TEST_CONTEXT.assertEqualJson(actual, expected); + } + } + + @SneakyThrows + @Test(expected = IpdataException.class) + public void testCurrencyError() { + IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url()) + .key("THIS_IS_AN_INVALID_KEY") + .withDefaultCache() + .feignClient(new ApacheHttpClient(HttpClientBuilder.create() + .setConnectionTimeToLive(10, TimeUnit.SECONDS) + .build()) + ).get(); + serviceWithInvalidKey.currency(fixture.target()); + } + + @Parameterized.Parameters + public static Iterable data() { + return TEST_CONTEXT.fixtures(); + } + +} diff --git a/src/test/java/io/ipdata/client/EdgeCaseTest.java b/src/test/java/io/ipdata/client/EdgeCaseTest.java new file mode 100644 index 0000000..1874198 --- /dev/null +++ b/src/test/java/io/ipdata/client/EdgeCaseTest.java @@ -0,0 +1,73 @@ +package io.ipdata.client; + +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.error.RemoteIpdataException; +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; + +import static io.ipdata.client.service.IpdataField.ASN; +import static io.ipdata.client.service.IpdataField.CURRENCY; + +public class EdgeCaseTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Test(expected = IllegalArgumentException.class) + @SneakyThrows + public void testGetFieldsWithNoFieldsThrows() { + IpdataService service = TEST_CONTEXT.ipdataService(); + service.getFields("8.8.8.8"); + } + + @Test(expected = IllegalArgumentException.class) + @SneakyThrows + public void testGetFieldsWithNoFieldsThrowsCaching() { + IpdataService service = TEST_CONTEXT.cachingIpdataService(); + service.getFields("8.8.8.8"); + } + + @Test + @SneakyThrows + public void testInvalidKeyReturnsRemoteException() { + IpdataService service = Ipdata.builder() + .url(TEST_CONTEXT.url()) + .key("INVALID_KEY") + .noCache() + .get(); + try { + service.ipdata("8.8.8.8"); + Assert.fail("Expected RemoteIpdataException"); + } catch (RemoteIpdataException e) { + Assert.assertEquals(401, e.getStatus()); + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + @SneakyThrows + public void testCachedInvalidKeyUnwrapsException() { + IpdataService service = Ipdata.builder() + .url(TEST_CONTEXT.url()) + .key("INVALID_KEY") + .withDefaultCache() + .get(); + try { + service.ipdata("8.8.8.8"); + Assert.fail("Expected RemoteIpdataException"); + } catch (RemoteIpdataException e) { + Assert.assertEquals(401, e.getStatus()); + } + } + + @Test + @SneakyThrows + public void testGetFieldsReturnsSelectedFields() { + IpdataService service = TEST_CONTEXT.ipdataService(); + IpdataModel model = service.getFields("8.8.8.8", ASN, CURRENCY); + Assert.assertNotNull(model.asn()); + Assert.assertNotNull(model.currency()); + } +} diff --git a/src/test/java/io/ipdata/client/FullModelTest.java b/src/test/java/io/ipdata/client/FullModelTest.java new file mode 100644 index 0000000..55424e8 --- /dev/null +++ b/src/test/java/io/ipdata/client/FullModelTest.java @@ -0,0 +1,69 @@ +package io.ipdata.client; + +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.error.RemoteIpdataException; +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + + +@RunWith(Parameterized.class) +public class FullModelTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public TestFixture fixture; + + @Test + @SneakyThrows + public void testFullResponse() { + IpdataService ipdataService = fixture.service(); + IpdataModel ipdataModel = ipdataService.ipdata(fixture.target()); + String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel); + String expected = TEST_CONTEXT.get("/"+fixture.target(), null); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("time_zone.current_time")); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + ipdataModel = ipdataService.ipdata(fixture.target()); + actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("time_zone.current_time")); + } + } + + + @SneakyThrows + @Test + public void testSingleFields() { + IpdataService ipdataService = fixture.service(); + String field = ipdataService.getCountryName(fixture.target()); + String expected = TEST_CONTEXT.get("/" + fixture.target() + "/country_name", null); + Assert.assertEquals(expected, field); + } + + + @SneakyThrows + @Test + public void testError() { + IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url()) + .key("THIS_IS_AN_INVALID_KEY") + .noCache() + .get(); + try { + serviceWithInvalidKey.ipdata(fixture.target()); + Assert.fail("Expected RemoteIpdataException"); + } catch (RemoteIpdataException e) { + Assert.assertEquals(401, e.getStatus()); + } + } + + @Parameterized.Parameters + public static Iterable data() { + return TEST_CONTEXT.fixtures(); + } + +} diff --git a/src/test/java/io/ipdata/client/IpdataFunctionalTest.java b/src/test/java/io/ipdata/client/IpdataFunctionalTest.java deleted file mode 100644 index 94ffc4d..0000000 --- a/src/test/java/io/ipdata/client/IpdataFunctionalTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.ipdata.client; - -import static com.fasterxml.jackson.databind.PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES; -import static io.ipdata.client.TestUtils.KEY; -import static org.junit.runners.Parameterized.Parameters; -import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; -import feign.httpclient.ApacheHttpClient; -import io.ipdata.client.error.RateLimitException; -import io.ipdata.client.model.Asn; -import io.ipdata.client.model.Currency; -import io.ipdata.client.model.IpdataModel; -import io.ipdata.client.model.Threat; -import io.ipdata.client.service.IpdataField; -import io.ipdata.client.service.IpdataService; -import java.net.URL; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import lombok.SneakyThrows; -import org.apache.http.client.HttpClient; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.impl.client.HttpClientBuilder; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -@RunWith(Parameterized.class) -public class IpdataFunctionalTest { - - private static IpdataService IPDATA_SERVICE; - private static IpdataService CACHING_IPDATA_SERVICE; - private static ObjectMapper MAPPER; - private static HttpClient HTTP_CLIENT; - - @Parameterized.Parameter - public IpdataService ipdataService; - - @Test - @SneakyThrows - public void testFullresponse() { - IpdataModel ipdataModel = ipdataService.ipdata("8.8.8.8"); - String serialized = MAPPER.writeValueAsString(ipdataModel); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8", null); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel.class)); - assertEquals(serialized, expected, false); - if (ipdataService == CACHING_IPDATA_SERVICE) { - //value will be returned from cache now - ipdataModel = ipdataService.ipdata("8.8.8.8"); - serialized = MAPPER.writeValueAsString(ipdataModel); - assertEquals(serialized, expected, false); - } - } - - @Test - @SneakyThrows - public void testASN() { - Asn asn = ipdataService.asn("8.8.8.8"); - String serialized = MAPPER.writeValueAsString(asn); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/asn", null); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Asn.class)); - assertEquals(serialized, expected, false); - if (ipdataService == CACHING_IPDATA_SERVICE) { - //value will be returned from cache now - asn = ipdataService.asn("8.8.8.8"); - serialized = MAPPER.writeValueAsString(asn); - assertEquals(serialized, expected, false); - } - } - - @Test - @SneakyThrows - public void testThreat() { - Threat threat = ipdataService.threat("8.8.8.8"); - String serialized = MAPPER.writeValueAsString(threat); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/threat", null); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Threat.class)); - assertEquals(serialized, expected, false); - if (ipdataService == CACHING_IPDATA_SERVICE) { - //value will be returned from cache now - threat = ipdataService.threat("8.8.8.8"); - serialized = MAPPER.writeValueAsString(threat); - assertEquals(serialized, expected, false); - } - } - - @Test - @SneakyThrows - public void testCurrency() { - Currency currency = ipdataService.currency("8.8.8.8"); - String serialized = MAPPER.writeValueAsString(currency); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/currency", null); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, Currency.class)); - assertEquals(serialized, expected, false); - if (ipdataService == CACHING_IPDATA_SERVICE) { - //value will be returned from cache now - currency = ipdataService.currency("8.8.8.8"); - serialized = MAPPER.writeValueAsString(currency); - assertEquals(serialized, expected, false); - } - } - - @Test - @SneakyThrows - public void testFieldSelection() { - IpdataModel ipdataModel = ipdataService.getFields("8.8.8.8", IpdataField.ASN, IpdataField.CURRENCY); - String serialized = MAPPER.writeValueAsString(ipdataModel); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8", ImmutableMap.of("fields", "asn,currency")); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel.class)); - assertEquals(serialized, expected, false); - Assert.assertNull(ipdataModel.threat()); - if (ipdataService == CACHING_IPDATA_SERVICE) { - //value will be returned from cache now - ipdataModel = ipdataService.getFields("8.8.8.8", IpdataField.ASN, IpdataField.CURRENCY); - serialized = MAPPER.writeValueAsString(ipdataModel); - assertEquals(serialized, expected, false); - } - } - - @SneakyThrows - @Test - public void testSingleFields() { - String field = ipdataService.getCountryName("8.8.8.8"); - String expected = TestUtils.get(HTTP_CLIENT, "/8.8.8.8/country_name", null); - Assert.assertEquals(field, expected); - } - - @SneakyThrows - @Test - public void testBulkResponse() { - List ipdataModels = ipdataService.bulkIpdata(Arrays.asList("8.8.8.8", "1.1.1.1")); - String serialized = MAPPER.writeValueAsString(ipdataModels); - String expected = TestUtils.post(HTTP_CLIENT, "/bulk", "[\"8.8.8.8\",\"1.1.1.1\"]", null); - expected = MAPPER.writeValueAsString(MAPPER.readValue(expected, IpdataModel[].class)); - assertEquals(serialized, expected, false); - } - - @SneakyThrows - @Test(expected = RateLimitException.class) - public void testError() { - URL url = new URL("https://api.ipdata.co"); - IpdataService serviceWithInvalidKey = Ipdata.builder().url(url) - .key("THIS_IS_AN_INVALID_KEY") - .feignClient(new ApacheHttpClient(HttpClientBuilder.create() - .setSSLHostnameVerifier(new NoopHostnameVerifier()).setConnectionTimeToLive(10, TimeUnit.SECONDS) - .build())).get(); - serviceWithInvalidKey.ipdata("8.8.8.8"); - } - - @Parameters - public static Iterable data() { - init(); - return Arrays.asList(IPDATA_SERVICE, CACHING_IPDATA_SERVICE); - } - - @SneakyThrows - public static void init() { - URL url = new URL("https://api.ipdata.co"); - MAPPER = new ObjectMapper(); - MAPPER.setPropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); - MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); - MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - HTTP_CLIENT = HttpClientBuilder.create().setSSLHostnameVerifier(new NoopHostnameVerifier()) - .build(); - IPDATA_SERVICE = Ipdata.builder().url(url).key(KEY) - .feignClient(new ApacheHttpClient(HttpClientBuilder.create() - .setSSLHostnameVerifier(new NoopHostnameVerifier()) - .build())).get(); - CACHING_IPDATA_SERVICE = Ipdata.builder().url(url) - .feignClient(new ApacheHttpClient(HttpClientBuilder.create() - .setSSLHostnameVerifier(new NoopHostnameVerifier()) - .build())) - .withDefaultCache().key(KEY).get(); - } - -} diff --git a/src/test/java/io/ipdata/client/Ipv6EncodingTest.java b/src/test/java/io/ipdata/client/Ipv6EncodingTest.java new file mode 100644 index 0000000..fddbac7 --- /dev/null +++ b/src/test/java/io/ipdata/client/Ipv6EncodingTest.java @@ -0,0 +1,36 @@ +package io.ipdata.client; + +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Assert; +import org.junit.Test; + +/** + * Regression test for https://github.com/ipdata/java/issues/10 + * Verifies that IPv6 colons are not percent-encoded (%3A) in HTTP requests. + */ +public class Ipv6EncodingTest { + + private static final MockIpdataServer MOCK = MockIpdataServer.getInstance(); + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MOCK.getUrl()); + + @Test + @SneakyThrows + public void testIpv6ColonsAreNotEncoded() { + String ipv6 = "2001:4860:4860::8888"; + IpdataService service = TEST_CONTEXT.ipdataService(); + IpdataModel model = service.ipdata(ipv6); + Assert.assertNotNull(model); + + String rawPath = MOCK.getLastRawPath(); + Assert.assertFalse( + "IPv6 colons should not be percent-encoded in the request path, but got: " + rawPath, + rawPath.contains("%3A") + ); + Assert.assertTrue( + "Request path should contain the IPv6 address with literal colons", + rawPath.contains(ipv6) + ); + } +} diff --git a/src/test/java/io/ipdata/client/MappingTest.java b/src/test/java/io/ipdata/client/MappingTest.java new file mode 100644 index 0000000..0d5c5fd --- /dev/null +++ b/src/test/java/io/ipdata/client/MappingTest.java @@ -0,0 +1,23 @@ +package io.ipdata.client; + +import com.google.common.io.CharStreams; +import io.ipdata.client.model.IpdataModel; +import lombok.SneakyThrows; +import net.javacrumbs.jsonunit.core.Configuration; +import net.javacrumbs.jsonunit.core.Option; +import org.junit.Test; + +import java.io.InputStreamReader; + +public class MappingTest { + + private static final TestContext TEST_CONTEXT = new TestContext("dummy", "https://api.ipdata.co"); + + @Test @SneakyThrows + public void testMapping(){ + String actual = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(getClass().getResourceAsStream("fixture.json"), IpdataModel.class)); + String expected = CharStreams.toString(new InputStreamReader(getClass().getResourceAsStream("fixture.json"))); + TEST_CONTEXT.assertEqualJson(actual, expected, Configuration.empty().withOptions(Option.TREATING_NULL_AS_ABSENT).whenIgnoringPaths("languages[0].rtl")); + } + +} diff --git a/src/test/java/io/ipdata/client/MockIpdataServer.java b/src/test/java/io/ipdata/client/MockIpdataServer.java new file mode 100644 index 0000000..527f7c1 --- /dev/null +++ b/src/test/java/io/ipdata/client/MockIpdataServer.java @@ -0,0 +1,207 @@ +package io.ipdata.client; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.io.CharStreams; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MockIpdataServer { + + public static final String API_KEY = "test-api-key"; + + private static MockIpdataServer instance; + + private final HttpServer server; + private final String url; + private final Map fixtures = new HashMap<>(); + private final ObjectMapper mapper = new ObjectMapper(); + private volatile String lastRawPath; + + private MockIpdataServer() { + try { + server = HttpServer.create(new InetSocketAddress(0), 0); + int port = server.getAddress().getPort(); + url = "http://localhost:" + port; + loadFixtures(); + server.createContext("/", this::handleRequest); + server.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to start mock server", e); + } + } + + public static synchronized MockIpdataServer getInstance() { + if (instance == null) { + instance = new MockIpdataServer(); + } + return instance; + } + + public String getUrl() { + return url; + } + + public String getLastRawPath() { + return lastRawPath; + } + + private void loadFixtures() { + String[] ips = {"8.8.8.8", "2001:4860:4860::8888", "1.1.1.1", "2001:4860:4860::8844", "41.128.21.123"}; + for (String ip : ips) { + String resourceName = "fixtures/" + ip.replace(":", "-") + ".json"; + try (InputStream is = getClass().getResourceAsStream(resourceName)) { + if (is != null) { + fixtures.put(ip, mapper.readTree(is)); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load fixture: " + resourceName, e); + } + } + } + + private void handleRequest(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod(); + String path = exchange.getRequestURI().getPath(); + lastRawPath = exchange.getRequestURI().getRawPath(); + String rawQuery = exchange.getRequestURI().getRawQuery(); + Map params = parseQuery(rawQuery); + + String apiKey = params.get("api-key"); + if (!API_KEY.equals(apiKey)) { + sendError(exchange, 401, "Invalid API key"); + return; + } + + if ("POST".equals(method) && "/bulk".equals(path)) { + handleBulk(exchange); + return; + } + + if ("GET".equals(method)) { + handleGet(exchange, path, params); + return; + } + + sendError(exchange, 404, "Not found"); + } catch (Exception e) { + sendError(exchange, 500, e.getMessage()); + } + } + + private void handleGet(HttpExchange exchange, String path, Map params) throws IOException { + String trimmed = path.startsWith("/") ? path.substring(1) : path; + + String ip = null; + String field = null; + + // Match against known IPs (longest first to avoid prefix conflicts) + List sortedIps = new ArrayList<>(fixtures.keySet()); + sortedIps.sort((a, b) -> b.length() - a.length()); + + for (String knownIp : sortedIps) { + if (trimmed.equals(knownIp)) { + ip = knownIp; + break; + } + if (trimmed.startsWith(knownIp + "/")) { + ip = knownIp; + field = trimmed.substring(knownIp.length() + 1); + break; + } + } + + if (ip == null) { + sendError(exchange, 404, "IP not found"); + return; + } + + JsonNode fixture = fixtures.get(ip); + + if (field != null) { + // Sub-field request: GET /{ip}/{field} + JsonNode fieldNode = fixture.get(field); + if (fieldNode == null) { + sendError(exchange, 404, "Field not found: " + field); + return; + } + if (fieldNode.isTextual()) { + sendResponse(exchange, 200, fieldNode.asText()); + } else { + sendResponse(exchange, 200, mapper.writeValueAsString(fieldNode)); + } + } else if (params.containsKey("fields")) { + // Selected fields request: GET /{ip}?fields=a,b + String fieldsStr = params.get("fields"); + String[] fields = fieldsStr.split(","); + ObjectNode result = mapper.createObjectNode(); + for (String f : fields) { + JsonNode fieldNode = fixture.get(f.trim()); + if (fieldNode != null) { + result.set(f.trim(), fieldNode); + } + } + sendResponse(exchange, 200, mapper.writeValueAsString(result)); + } else { + // Full model request: GET /{ip} + sendResponse(exchange, 200, mapper.writeValueAsString(fixture)); + } + } + + private void handleBulk(HttpExchange exchange) throws IOException { + String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody())); + String[] ips = mapper.readValue(body, String[].class); + ArrayNode result = mapper.createArrayNode(); + for (String ip : ips) { + JsonNode fixture = fixtures.get(ip); + if (fixture != null) { + result.add(fixture); + } + } + sendResponse(exchange, 200, mapper.writeValueAsString(result)); + } + + private void sendResponse(HttpExchange exchange, int status, String body) throws IOException { + byte[] bytes = body.getBytes("UTF-8"); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(status, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.getResponseBody().close(); + } + + private void sendError(HttpExchange exchange, int status, String message) throws IOException { + String body = "{\"message\":\"" + message.replace("\"", "\\\"") + "\"}"; + sendResponse(exchange, status, body); + } + + private Map parseQuery(String rawQuery) { + Map params = new HashMap<>(); + if (rawQuery != null) { + for (String param : rawQuery.split("&")) { + String[] pair = param.split("=", 2); + if (pair.length == 2) { + try { + params.put(URLDecoder.decode(pair[0], "UTF-8"), URLDecoder.decode(pair[1], "UTF-8")); + } catch (UnsupportedEncodingException e) { + params.put(pair[0], pair[1]); + } + } + } + } + return params; + } +} diff --git a/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java b/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java new file mode 100644 index 0000000..41e1381 --- /dev/null +++ b/src/test/java/io/ipdata/client/MultipleFieldsSelectionTest.java @@ -0,0 +1,42 @@ +package io.ipdata.client; + +import com.google.common.collect.ImmutableMap; +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static io.ipdata.client.service.IpdataField.*; +import static java.util.Arrays.asList; + +@RunWith(Parameterized.class) +public class MultipleFieldsSelectionTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public IpdataService ipdataService; + + @Test + @SneakyThrows + public void testMultipleFieldsSelection() { + IpdataModel ipdataModel = ipdataService.getFields("41.128.21.123", ASN, CURRENCY); + String actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel); + String expected = TEST_CONTEXT.get("/41.128.21.123", ImmutableMap.of("fields", "asn,currency")); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration()); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + ipdataModel = ipdataService.getFields("41.128.21.123", ASN, CURRENCY, CARRIER); + actual = TEST_CONTEXT.mapper().writeValueAsString(ipdataModel); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration()); + } + } + + @Parameterized.Parameters + public static Iterable data() { + return asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService()); + } + +} diff --git a/src/test/java/io/ipdata/client/TestContext.java b/src/test/java/io/ipdata/client/TestContext.java new file mode 100644 index 0000000..c367d3b --- /dev/null +++ b/src/test/java/io/ipdata/client/TestContext.java @@ -0,0 +1,115 @@ +package io.ipdata.client; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.CharStreams; +import io.ipdata.client.service.IpdataService; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.experimental.Accessors; +import net.javacrumbs.jsonunit.core.Configuration; +import net.javacrumbs.jsonunit.core.Option; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; + +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Map; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; +import static java.lang.System.getenv; +import static java.util.Arrays.asList; +import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals; + +@Getter +@Accessors(fluent = true) +public class TestContext { + + static final String DEFAULT_KEY_ENV_VAR = "IPDATACO_KEY"; + + private final ObjectMapper mapper; + private final HttpClient httpClient; + private final IpdataService ipdataService; + private final IpdataService cachingIpdataService; + private final String key; + private final URL url; + private final Configuration configuration = Configuration.empty().withOptions(Option.IGNORING_EXTRA_FIELDS, Option.TREATING_NULL_AS_ABSENT); + + public TestContext(String url) { + this(getenv(DEFAULT_KEY_ENV_VAR), url); + } + + @SneakyThrows + public TestContext(String key, String url) { + this.key = key; + this.url = new URL(url); + mapper = new ObjectMapper(); + mapper.setPropertyNamingStrategy(SNAKE_CASE); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + httpClient = HttpClientBuilder.create().build(); + ipdataService = Ipdata.builder().url(this.url).key(this.key) + .noCache() + .get(); + cachingIpdataService = Ipdata.builder().url(this.url) + .withDefaultCache().key(key).get(); + } + + void assertEqualJson(String actual, String expected) { + assertJsonEquals(expected, actual, configuration); + } + + void assertEqualJson(String actual, String expected, Configuration configuration) { + assertJsonEquals(expected, actual, configuration); + } + + /* + Used to get responses of a raw http call, without logical proxying + */ + @SneakyThrows + String get(String path, Map params) { + URIBuilder builder = new URIBuilder(url.toString() + path); + builder.setParameter("api-key", this.key); + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + builder.setParameter(entry.getKey(), entry.getValue()); + } + } + HttpGet httpget = new HttpGet(builder.build()); + HttpResponse response = httpClient.execute(httpget); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + } + + @SneakyThrows + String post(String path, String content, Map params) { + URIBuilder builder = new URIBuilder(url.toString() + path); + builder.setParameter("api-key", this.key); + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + builder.setParameter(entry.getKey(), entry.getValue()); + } + } + HttpPost httpPost = new HttpPost(builder.build()); + httpPost.setEntity(new StringEntity(content)); + HttpResponse response = httpClient.execute(httpPost); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + } + + + public Iterable fixtures() { + return asList( + new TestFixture("8.8.8.8", ipdataService()), + new TestFixture("8.8.8.8", cachingIpdataService()), + new TestFixture("2001:4860:4860::8888", ipdataService()), + new TestFixture("2001:4860:4860::8888", cachingIpdataService()) + ); + } + +} diff --git a/src/test/java/io/ipdata/client/TestFixture.java b/src/test/java/io/ipdata/client/TestFixture.java new file mode 100644 index 0000000..1ef6d41 --- /dev/null +++ b/src/test/java/io/ipdata/client/TestFixture.java @@ -0,0 +1,12 @@ +package io.ipdata.client; + +import io.ipdata.client.service.IpdataService; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; + +@RequiredArgsConstructor @Accessors(fluent = true) @Getter +public class TestFixture { + private final String target; + private final IpdataService service; +} diff --git a/src/test/java/io/ipdata/client/TestUtils.java b/src/test/java/io/ipdata/client/TestUtils.java deleted file mode 100644 index ce11457..0000000 --- a/src/test/java/io/ipdata/client/TestUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.ipdata.client; - -import com.google.common.io.CharStreams; -import java.io.InputStreamReader; -import java.util.Map; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.entity.StringEntity; - -@UtilityClass -public class TestUtils { - - public static final String KEY = System.getenv("IPDATACO_KEY"); - - /* - Used to get responses of a raw http call, without client proxying - */ - @SneakyThrows - public static String get(HttpClient client, String path, Map params) { - URIBuilder builder = new URIBuilder("https://api.ipdata.co" + path); - builder.setParameter("api-key", KEY); - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - builder.setParameter(entry.getKey(), entry.getValue()); - } - } - HttpGet httpget = new HttpGet(builder.build()); - HttpResponse response = client.execute(httpget); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); - } - - @SneakyThrows - public static String post(HttpClient client, String path, String content, Map params) { - URIBuilder builder = new URIBuilder("https://api.ipdata.co" + path); - builder.setParameter("api-key", KEY); - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - builder.setParameter(entry.getKey(), entry.getValue()); - } - } - HttpPost httpPost = new HttpPost(builder.build()); - httpPost.setEntity(new StringEntity(content)); - HttpResponse response = client.execute(httpPost); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); - } - -} diff --git a/src/test/java/io/ipdata/client/ThreatTest.java b/src/test/java/io/ipdata/client/ThreatTest.java new file mode 100644 index 0000000..f462abd --- /dev/null +++ b/src/test/java/io/ipdata/client/ThreatTest.java @@ -0,0 +1,56 @@ +package io.ipdata.client; + +import feign.httpclient.ApacheHttpClient; +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.model.ThreatModel; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.concurrent.TimeUnit; + +@RunWith(Parameterized.class) +public class ThreatTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public TestFixture fixture; + + @Test + @SneakyThrows + public void testThreat() { + IpdataService ipdataService = fixture.service(); + ThreatModel threat = ipdataService.threat(fixture.target()); + String actual = TEST_CONTEXT.mapper().writeValueAsString(threat); + String expected = TEST_CONTEXT.get("/"+fixture.target()+"/threat", null); + TEST_CONTEXT.assertEqualJson(actual, expected); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + threat = ipdataService.threat(fixture.target()); + actual = TEST_CONTEXT.mapper().writeValueAsString(threat); + TEST_CONTEXT.assertEqualJson(actual, expected); + } + } + + @SneakyThrows + @Test(expected = IpdataException.class) + public void testThreatError() { + IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url()) + .key("THIS_IS_AN_INVALID_KEY") + .withDefaultCache() + .feignClient(new ApacheHttpClient(HttpClientBuilder.create() + .setConnectionTimeToLive(10, TimeUnit.SECONDS) + .build())).get(); + serviceWithInvalidKey.threat(fixture.target()); + } + + @Parameterized.Parameters + public static Iterable data() { + return TEST_CONTEXT.fixtures(); + } + +} diff --git a/src/test/java/io/ipdata/client/TimeZoneTest.java b/src/test/java/io/ipdata/client/TimeZoneTest.java new file mode 100644 index 0000000..2ef776f --- /dev/null +++ b/src/test/java/io/ipdata/client/TimeZoneTest.java @@ -0,0 +1,60 @@ +package io.ipdata.client; + +import feign.httpclient.ApacheHttpClient; +import io.ipdata.client.error.IpdataException; +import io.ipdata.client.model.TimeZone; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; + +@RunWith(Parameterized.class) +public class TimeZoneTest { + + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MockIpdataServer.getInstance().getUrl()); + + @Parameterized.Parameter + public TestFixture fixture; + + @Test + @SneakyThrows + public void testTimeZone() { + IpdataService ipdataService = fixture.service(); + TimeZone timeZone = fixture.service().timeZone(fixture.target()); + String expected = TEST_CONTEXT.get("/"+fixture.target()+"/time_zone", null); + String actual = TEST_CONTEXT.mapper().writeValueAsString(timeZone); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("current_time")); + assertNotNull(timeZone.currentTime()); + if (ipdataService == TEST_CONTEXT.cachingIpdataService()) { + //value will be returned from cache now + timeZone = ipdataService.timeZone(fixture.target()); + actual = TEST_CONTEXT.mapper().writeValueAsString(timeZone); + TEST_CONTEXT.assertEqualJson(actual, expected, TEST_CONTEXT.configuration().whenIgnoringPaths("current_time")); + assertNotNull(timeZone.currentTime()); + } + } + + @SneakyThrows + @Test(expected = IpdataException.class) + public void testTimeZoneError() { + IpdataService serviceWithInvalidKey = Ipdata.builder().url(TEST_CONTEXT.url()) + .key("THIS_IS_AN_INVALID_KEY") + .withDefaultCache() + .feignClient(new ApacheHttpClient(HttpClientBuilder.create() + .setConnectionTimeToLive(10, TimeUnit.SECONDS) + .build())).get(); + serviceWithInvalidKey.timeZone(fixture.target()); + } + + @Parameterized.Parameters + public static Iterable data() { + return TEST_CONTEXT.fixtures(); + } + +} diff --git a/src/test/resources/io/ipdata/client/fixture.json b/src/test/resources/io/ipdata/client/fixture.json new file mode 100644 index 0000000..3275fb2 --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixture.json @@ -0,0 +1,77 @@ +{ + "ip": "69.78.70.144", + "is_eu": false, + "city": "test-city", + "region": "Illinois", + "region_code": "IL", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 41.8483, + "longitude": -87.6517, + "postal": "test-postal", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", + "asn": { + "asn": "AS6167", + "name": "Cellco Partnership DBA Verizon Wireless", + "domain": "verizonwireless.com", + "route": "69.78.0.0/16", + "type": "business" + }, + "carrier": { + "name": "Verizon", + "mcc": "310", + "mnc": "004" + }, + "company": { + "name": "Verizon Wireless", + "domain": "verizonwireless.com", + "network": "69.78.0.0/16", + "type": "business" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Chicago", + "abbr": "CDT", + "offset": "-0500", + "is_dst": true, + "current_time": "2020-06-12T06:37:16.595612-05:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "236" +} diff --git a/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json new file mode 100644 index 0000000..11e2002 --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json @@ -0,0 +1,77 @@ +{ + "ip": "1.1.1.1", + "is_eu": false, + "city": "Los Angeles", + "region": "California", + "region_code": "CA", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 34.0522, + "longitude": -118.2437, + "postal": "90001", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", + "asn": { + "asn": "AS13335", + "name": "Cloudflare Inc", + "domain": "cloudflare.com", + "route": "1.1.1.0/24", + "type": "hosting" + }, + "carrier": { + "name": "Cloudflare", + "mcc": "310", + "mnc": "000" + }, + "company": { + "name": "Cloudflare Inc", + "domain": "cloudflare.com", + "network": "1.1.1.0/24", + "type": "hosting" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", + "is_dst": true, + "current_time": "2020-06-12T06:37:16.595612-07:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "1500" +} diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json new file mode 100644 index 0000000..62b19c9 --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json @@ -0,0 +1,77 @@ +{ + "ip": "2001:4860:4860::8844", + "is_eu": false, + "city": "Mountain View", + "region": "California", + "region_code": "CA", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 37.386, + "longitude": -122.0838, + "postal": "94035", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", + "asn": { + "asn": "AS15169", + "name": "Google LLC", + "domain": "google.com", + "route": "2001:4860::/32", + "type": "hosting" + }, + "carrier": { + "name": "Google", + "mcc": "310", + "mnc": "000" + }, + "company": { + "name": "Google LLC", + "domain": "google.com", + "network": "2001:4860::/32", + "type": "hosting" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", + "is_dst": true, + "current_time": "2020-06-12T06:37:16.595612-07:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "1500" +} diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json new file mode 100644 index 0000000..766ef8c --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json @@ -0,0 +1,77 @@ +{ + "ip": "2001:4860:4860::8888", + "is_eu": false, + "city": "Mountain View", + "region": "California", + "region_code": "CA", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 37.386, + "longitude": -122.0838, + "postal": "94035", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", + "asn": { + "asn": "AS15169", + "name": "Google LLC", + "domain": "google.com", + "route": "2001:4860::/32", + "type": "hosting" + }, + "carrier": { + "name": "Google", + "mcc": "310", + "mnc": "000" + }, + "company": { + "name": "Google LLC", + "domain": "google.com", + "network": "2001:4860::/32", + "type": "hosting" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", + "is_dst": true, + "current_time": "2020-06-12T06:37:16.595612-07:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "1500" +} diff --git a/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json new file mode 100644 index 0000000..58c661a --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json @@ -0,0 +1,77 @@ +{ + "ip": "41.128.21.123", + "is_eu": false, + "city": "Cairo", + "region": "Cairo Governorate", + "region_code": "C", + "region_type": "governorate", + "country_name": "Egypt", + "country_code": "EG", + "continent_name": "Africa", + "continent_code": "AF", + "latitude": 30.0444, + "longitude": 31.2357, + "postal": "11511", + "calling_code": "20", + "flag": "https://ipdata.co/flags/eg.png", + "emoji_flag": "\ud83c\uddea\ud83c\uddec", + "emoji_unicode": "U+1F1EA U+1F1EC", + "asn": { + "asn": "AS8452", + "name": "TE Data", + "domain": "tedata.net", + "route": "41.128.0.0/16", + "type": "isp" + }, + "carrier": { + "name": "TE Data", + "mcc": "602", + "mnc": "001" + }, + "company": { + "name": "TE Data", + "domain": "tedata.net", + "network": "41.128.0.0/16", + "type": "isp" + }, + "languages": [ + { + "name": "Arabic", + "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629" + } + ], + "currency": { + "name": "Egyptian Pound", + "code": "EGP", + "symbol": "E\u00a3", + "native": "\u062c.\u0645.\u200f", + "plural": "Egyptian pounds" + }, + "time_zone": { + "name": "Africa/Cairo", + "abbr": "EET", + "offset": "+0200", + "is_dst": false, + "current_time": "2020-06-12T15:37:16.595612+02:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "1500" +} diff --git a/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json new file mode 100644 index 0000000..8ad870f --- /dev/null +++ b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json @@ -0,0 +1,77 @@ +{ + "ip": "8.8.8.8", + "is_eu": false, + "city": "Mountain View", + "region": "California", + "region_code": "CA", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 37.386, + "longitude": -122.0838, + "postal": "94035", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", + "asn": { + "asn": "AS15169", + "name": "Google LLC", + "domain": "google.com", + "route": "8.8.8.0/24", + "type": "hosting" + }, + "carrier": { + "name": "Google", + "mcc": "310", + "mnc": "000" + }, + "company": { + "name": "Google LLC", + "domain": "google.com", + "network": "8.8.8.0/24", + "type": "hosting" + }, + "languages": [ + { + "name": "English", + "native": "English" + } + ], + "currency": { + "name": "US Dollar", + "code": "USD", + "symbol": "$", + "native": "$", + "plural": "US dollars" + }, + "time_zone": { + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", + "is_dst": true, + "current_time": "2020-06-12T06:37:16.595612-07:00" + }, + "threat": { + "is_tor": false, + "is_vpn": false, + "is_proxy": false, + "is_anonymous": false, + "is_known_attacker": false, + "is_known_abuser": false, + "is_threat": false, + "is_bogon": false, + "is_icloud_relay": false, + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } + }, + "count": "1500" +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties deleted file mode 100644 index 67f7a69..0000000 --- a/src/test/resources/log4j.properties +++ /dev/null @@ -1,9 +0,0 @@ -log4j.rootLogger=DEBUG, A1 -log4j.appender.A1=org.apache.log4j.ConsoleAppender -log4j.appender.A1.layout=org.apache.log4j.PatternLayout - -# Print the date in ISO 8601 format -log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n - -# Print only messages of level WARN or above in the package com.foo. -log4j.logger.io=DEBUG diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..572da4d --- /dev/null +++ b/src/test/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.log.org.apache.http=info +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss,SSS +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showShortLogName=true