diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..90d05f0
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,10 @@
+# Configuration for the GitHub feature to automatically request reviews from the code owners when a pull request changes any owned files.
+#
+# [References]
+# https://github.com/blog/2392-introducing-code-owners
+# https://help.github.com/articles/about-codeowners
+
+# +--------------------+
+# + Default Code Owner +
+# +--------------------+
+* @arcticicestudio
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..64a9896
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,81 @@
+
+
+> **Please read the [contribution guidelines](https://github.com/arcticicestudio/icecore-hashids/blob/develop/CONTRIBUTING.md) before filling out this issue template**.
+
+## Prerequisites
+
+This section and the instructions in the sections below are only part of this issue template. Please ensure to **delete this whole section, all pre-filled instructions of the sections below and sections you have not filled out before submitting** to ensure a clear structure and overview.
+
+Please do your best to provide as much information as possible and use a clear and descriptive title for your enhancement suggestion or bug report to help maintainers and the community understand and reproduce the behavior, find related reports and to resolve the ticket faster.
+
+* **Ensure the issue has not already been reported by using the [GitHub Issues search](https://github.com/arcticicestudio/icecore-hashids/issues)** — if it has **and the issue is still open**, add a comment to the existing issue instead of opening this new one. If you find a closed issue that seems to be similar to this one, include a link to the original issue in the [metadata head](#metadata-head) section of this issue.
+* **Ensure the issue is reproducible** — try to use the [latest version](https://github.com/arcticicestudio/icecore-hashids/releases/latest) and [`develop`](https://github.com/arcticicestudio/icecore-hashids/tree/develop) branch.
+
+## Metadata Head
+
+The metadata head should be added to the top of the issue as [Markdown text quote](https://help.github.com/articles/basic-writing-and-formatting-syntax) containing the [issue type](#issue-type) and if necessary the ID of other related issues.
+
+> Issue type:
+Related issues:
+
+### Issue Type
+
+Set the *type* of this issue. It determines which information will be required in the following sections when it is an [bug report](https://github.com/arcticicestudio/icecore-hashids/blob/develop/CONTRIBUTING.md#bug-reports) or an [enhancement suggestion](https://github.com/arcticicestudio/icecore-hashids/blob/develop/CONTRIBUTING.md#enhancement-suggestions).
+
+* *feature*
+* *improvement*
+* *bug*
+* *test*
+* *task*
+* *subtask*
+
+## Description
+
+Describe the enhancement or bug as in many relevant details as possible. If this is a enhancement suggestion add specific use-cases and explain why this feature or improvement would be useful. If this is a bug provide ensure to fill in the [steps to reproduce](#steps-to-reproduce) it.
+
+### Steps to Reproduce
+
+1. Step One
+2. Step Two
+3. ...
+
+### Expected Behavior
+
+What you expect to happen?
+
+### Actual Behavior
+
+What actually happens?
+
+## Example
+
+Provide a [MCVE - The Minimal, Complete, and Verifiable Example](https://github.com/arcticicestudio/icecore-hashids/blob/develop/CONTRIBUTING.md#mcve)
+
+**This is a optional section, but it can drastically increase the speed at which this issue can be processed since it takes away the time-consuming reconstruction to reproduce the enhancement or bug.**
+
+The recommened way is to upload it as [Gist](https://gist.github.com) or new repository to GitHub, but of course you can [attach it to this issue](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests), use any free file hosting service or paste the code in [Markdown code blocks](https://help.github.com/articles/basic-writing-and-formatting-syntax) into this issue.
+
+## Environment and Versions
+
+* What is the version of IceCore Hashids you are using?
+* What is the name and the version of the OS you're using?
+ * Have you tried to reproduce it on different OS environments and if yes is the behavior the same for all?
+* Which Java JDK/JRE distribution and version are you using? (*Oracle*, *OpenJDK*, ...)
+* Are you running the project with your IDE or through Maven?
+ * Are you using any additional CLI arguments for Java or Maven?
+
+## Stack Trace and Error Messages
+
+```
+Paste the full stack trace, error messages or the logfile here ...
+```
+
+... or [attach them as files](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests) to this issue.
+
+## References
+
+Add any other references and links which are relevant for this issue.
+
+## Potential Solution
+
+If this is a bug report this might include the lines of code that you have identified as causing the bug or in case of an enhancement suggestion references to other projects where this enhancement already exists.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..8f387d1
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,27 @@
+
+
+> **Please read the [contribution guidelines](https://github.com/arcticicestudio/icecore-hashids/blob/develop/CONTRIBUTING.md) before filling out this pull request template**.
+
+## Prerequisites
+
+This section and the instructions in the sections below are only part of this pull request template. Please ensure to **delete this whole section, all pre-filled instructions of the sections below and sections you have not filled out before submitting** to ensure a clear structure and overview.
+
+Please do your best to provide as much information as possible and use a clear and descriptive title for your enhancement suggestion or bug fix to help maintainers and the community understand and reproduce the behavior, find related pull requests and to merge it faster.
+
+* **Ensure the pull request has not already been reported by using the [GitHub Pull Request search](https://github.com/arcticicestudio/icecore-hashids/pulls)** — if it has **and the pull request is still open**, add a comment to the existing pull request instead of opening this new one. If you find a closed pull request that seems to be similar to this one, include a link to it in the [metadata head](#metadata-head) section of this pull request.
+* **Ensure to adhere to the [pull request contribution guidelines](https://github.com/arcticicestudio/icecore-hashids/blob/feature/gh-12-github-open-source-community-standards/CONTRIBUTING.md#pull-requests)**, especially the one for tests and documentations.
+
+## Metadata Head
+
+The metadata head should be added to the top of the pull request as [Markdown text quote](https://help.github.com/articles/basic-writing-and-formatting-syntax) containing the [GitHub issue keywords][gh-help-issue-keywords] to link to the related enhancements suggestions (`Closes`) or bug reports (`Fixes`). You can add additional details like dependencies to other pull requests and the order it needs to be merged.
+
+> Closes ISSUE_ID
+Must be merged **after**/**before** ISSUE_ID
+
+## Description
+
+Describe the changes as in many relevant details as possible. If this is a enhancement suggestion add specific use-cases and explain why this feature or improvement would be useful. If this is a bug fix ensure to provide a *before/after* comparison by describing the current behavior and the new behavior.
+
+## References
+
+Add any other references and links which are relevant for this issue.
diff --git a/.gitignore b/.gitignore
index 9cb88b8..512e5fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ out/
!.idea/runConfigurations
!.idea/compiler.xml
!.idea/encodings.xml
+!.idea/icon_dark.png
+!.idea/icon.png
!.idea/misc.xml
!.idea/modules.xml
!.idea/vcs.xml
@@ -25,3 +27,9 @@ out/
# JVM crash logs
# http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+
+# +-----+
+# + NPM +
+# +-----+
+node_modules/
+npm-debug.log
diff --git a/.idea/icon.png b/.idea/icon.png
new file mode 100644
index 0000000..94d4617
Binary files /dev/null and b/.idea/icon.png differ
diff --git a/.idea/icon_dark.png b/.idea/icon_dark.png
new file mode 100644
index 0000000..b53f6cc
Binary files /dev/null and b/.idea/icon_dark.png differ
diff --git a/.idea/runConfigurations/Interop_Tests.xml b/.idea/runConfigurations/Interop_Tests.xml
new file mode 100644
index 0000000..6064f80
--- /dev/null
+++ b/.idea/runConfigurations/Interop_Tests.xml
@@ -0,0 +1,30 @@
+
- Release Date: 2017-07-17
- Milestone
- Project Board
-
A lightweight generator for short, unique, non-sequential and decodable Hashids from positive unsigned (long) integer numbers.
-Implementation of the Hashids project.
A lightweight generator for short, unique, non-sequential and decodable Hashids from non-negative numbers.
-Designed for websites to use in URL shortening, tracking stuff, or making pages private or at least unguessable. -Can be used to create e.g obfuscated database ids, invitation codes, store shard numbers and much more. - -It converts numbers like `347` into strings like `yr8`, or array of numbers like `[27, 986]` into `3kTMd`. -You can also decode those ids back. -This is useful in bundling several parameters into one or simply using them as short UIDs. - -This algorithm tries to satisfy the following requirements: - 1. Hashids must be unique and decodable - 2. Hashids should be able to contain more than one integer to use them in complex or clustered systems - 3. Ability to specify minimum hash length - 4. Hashids should not contain basic English curse words since they are meant to appear in public places like a URL - -**NOTE**: All (long) integers must be greater than or equal to zero. - -## Getting started -### Setup -To use icecore-hashids it must be available on your classpath. -You can use it as a dependency for your favorite build tool or [download the latest version](https://github.com/arcticicestudio/icecore-hashids/releases/latest). - - Apache Maven -```xml -Implementation of the Hashids algorithm.
--- -## Development -[![](https://img.shields.io/badge/Changelog-0.3.0-blue.svg)](https://github.com/arcticicestudio/icecore-hashids/blob/v0.3.0/CHANGELOG.md) [![](https://img.shields.io/badge/Workflow-gitflow_Branching_Model-blue.svg)](http://nvie.com/posts/a-successful-git-branching-model) [![](https://img.shields.io/badge/Versioning-ArcVer_0.8.0-blue.svg)](https://github.com/arcticicestudio/arcver) +[Hashids][hashids] are obfuscated unique hashes of non-negative (long) integer numbers, but in contrast to cryptographic one-way hash algorithms they are can be decoded again. The algorithm can be used to either generate a hash from a single number or bundling several numbers into one to be stored as simple short UIDs. This design allows to use them for many use-cases like + +* URL shortening +* database ID protection +* shard numbers storage +* invitation-, authorization- and gift codes +* complex- or clustered system parameters + +Numbers like `347` are converted into strings like `yr8`, or an array of numbers like `[27, 986]` into `3kTMd`. + +## Features + +The **algorithm** provides the following features: + +* Generation of short, unique, case-sensitive and non-sequential [decodable][docs-api-guide-decoding] hashes of natural numbers +* Additional entropy through [salt][docs-api-guide-config-salt] usage +* Configurable [minimum hash length][docs-api-guide-config-min-hash-length] and [alphabet][docs-api-guide-config-alphabet] +* Combining of [several numbers to one hash][docs-api-guide-encoding] +* Deterministic hash computation given the same input and parametrization/[instance configuration][docs-api-instances] +* [Prevention of curse words][docs-api-curse-word-prevention] through separator characters + +In addition, the **library** provides features to + +* pass `0x` or `0X` [prefixed hexadecimal numbers][docs-api-guide-config-feature-hex-prefix] to the public API methods +* [handle exceptions][docs-api-guide-config-feature-exception-handling] instead of returning empty values when invalid parameters are passed to any public API method +* [disable the maximum number size limit][docs-api-guide-config-feature-no-max-number-size] which ensures the interoperability with the [algorithm reference implementation][hashids-js] and allows the usage of the Java `Long` [maximum value][long-max-value] + +> Please note that most features will break the interoperability with the [algorithm reference implementation][hashids-js]! + + +## Getting Started + +The [project documentation][docs] contains chapters to learn about the [installation][docs-getting-started-installation] and [requirements][docs-getting-started-requirements], get an [overview][docs-api] of the API and [learn how to use it][docs-api-guide], and [build][docs-dev-building] the project and [running the tests][docs-dev-testing]. + +## Contributing + +Read the [contributing guide][docs-dev-contributing] to learn about the development process and how to propose [enhancement suggestions][docs-dev-contributing-enhancements] and [report bugs][docs-dev-contributing-bug-reports], how to [submit pull requests][docs-dev-contributing-pr] and the project's [styleguides][docs-dev-contributing-styleguides], [branch organization][docs-dev-contributing-branch-org] and [versioning][docs-dev-contributing-versioning] model. -### Contribution -Please report issues/bugs, feature requests and suggestions for improvements to the [issue tracker](https://github.com/arcticicestudio/icecore-hashids/issues). +The guide also includes information about [minimal, complete, and verifiable examples][docs-dev-contributing-mcve] and other ways to contribute to the project like [improving existing issues][docs-dev-contributing-other-improve-issues] and [giving feedback on issues and pull requests][docs-dev-contributing-other-feedback]. ---Copyright © 2017 Arctic Ice Studio
- + + +[docs]: https://arcticicestudio.github.io/icecore-hashids +[docs-api]: https://arcticicestudio.github.io/icecore-hashids/api +[docs-api-curse-word-prevention]: https://arcticicestudio.github.io/icecore-hashids/api/curse-word-prevention.html +[docs-api-guide]: https://arcticicestudio.github.io/icecore-hashids/api/guide +[docs-api-guide-config-alphabet]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/#determine-a-custom-alphabet +[docs-api-guide-config-feature-exception-handling]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/features.html#exception-handling +[docs-api-guide-config-feature-hex-prefix]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/features.html#allow-hexadecimal-number-prefixes +[docs-api-guide-config-feature-no-max-number-size]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/features.html#no-number-size-limit +[docs-api-guide-config-min-hash-length]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/#defining-a-minimum-hash-length +[docs-api-guide-config-salt]: https://arcticicestudio.github.io/icecore-hashids/api/guide/configuration/#using-a-salt +[docs-api-guide-decoding]: https://arcticicestudio.github.io/icecore-hashids/api/guide/decoding.html +[docs-api-guide-encoding]: https://arcticicestudio.github.io/icecore-hashids/api/guide/encoding.html +[docs-api-instances]: https://arcticicestudio.github.io/icecore-hashids/api/instances.html +[docs-dev-building]: https://arcticicestudio.github.io/icecore-hashids/development/building.html +[docs-dev-contributing]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html +[docs-dev-contributing-branch-org]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#branch-organization +[docs-dev-contributing-bug-reports]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#bug-reports +[docs-dev-contributing-enhancements]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#enhancement-suggestions +[docs-dev-contributing-mcve]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#mcve +[docs-dev-contributing-other-feedback]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#give-feedback-on-issues-and-pull-requests +[docs-dev-contributing-other-improve-issues]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#improve-issues +[docs-dev-contributing-pr]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#pull-requests +[docs-dev-contributing-styleguides]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#styleguides +[docs-dev-contributing-versioning]: https://arcticicestudio.github.io/icecore-hashids/development/contributing.html#versioning +[docs-dev-testing]: https://arcticicestudio.github.io/icecore-hashids/development/testing.html +[docs-getting-started-installation]: https://arcticicestudio.github.io/icecore-hashids/getting-started/installation.html +[docs-getting-started-requirements]: https://arcticicestudio.github.io/icecore-hashids/getting-started/requirements.html +[hashids]: http://hashids.org +[hashids-js]: https://github.com/ivanakimov/hashids.js +[long-max-value]: https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html#MAX_VALUE diff --git a/circle.yml b/circle.yml index dd652e4..537aafd 100644 --- a/circle.yml +++ b/circle.yml @@ -9,7 +9,7 @@ # # [References] # Circle CI -# (https://circleci.com/docs) +# https://circleci.com/docs machine: java: version: oraclejdk8 @@ -17,11 +17,18 @@ machine: general: artifacts: - ./target/*.jar + - ./target/docs + +dependencies: + pre: + - mvn --version test: post: - mkdir -p $CIRCLE_TEST_REPORTS/junit/ - find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \; override: - - mvn clean verify assembly:single -P code-coverage,assemble - - find . -type f -regextype posix-egrep -regex ".*/target/(.*asc|.*jar|.*md5|.*pom|.*sha1|.*tar.gz|.*zip)" -exec cp {} $CIRCLE_ARTIFACTS \; + - mvn -B clean verify -P node,code-coverage + - find . -type f -regextype posix-egrep -regex ".*/target/(.*asc|.*jar|.*md5|.*pom|.*sha1)" -exec cp {} $CIRCLE_ARTIFACTS \; + - npm install + - npm run docs:build diff --git a/icecore-hashids.iml b/icecore-hashids.iml index 3360b15..671b4c7 100644 --- a/icecore-hashids.iml +++ b/icecore-hashids.iml @@ -5,15 +5,15 @@A lightweight generator for short, unique, non-sequential and decodable Hashids from non-negative numbers.
+ +Implementation of the Hashids algorithm.
+ +--- + +[Hashids][hashids] are obfuscated unique hashes of non-negative (long) integer numbers, but in contrast to cryptographic one-way hash algorithms they are can be decoded again. The algorithm can be used to either generate a hash from a single number or bundling several numbers into one to be stored as simple short UIDs. This design allows to use them for many use-cases like + +* URL shortening +* database ID protection +* shard numbers storage +* invitation-, authorization- and gift codes +* complex- or clustered system parameters + +Numbers like `347` are converted into strings like `yr8`, or an array of numbers like `[27, 986]` into `3kTMd`. + +## Features + +The **algorithm** provides the following features: + +* Generation of short, unique, case-sensitive and non-sequential [decodable][api-guide-decoding] hashes of natural numbers +* Additional entropy through [salt][api-guide-config-salt] usage +* Configurable [minimum hash length][api-guide-config-min-hash-length] and [alphabet][api-guide-config-alphabet] +* Combining of [several numbers to one hash][api-guide-encoding] +* Deterministic hash computation given the same input and parametrization/[instance configuration][api-instances] +* [Prevention of curse words][api-curse-word-prevention] through separator characters + +In addition, the **library** provides features to + +* pass `0x` or `0X` [prefixed hexadecimal numbers][api-guide-config-feature-hex-prefix] to the public API methods +* [handle exceptions][api-guide-config-feature-exception-handling] instead of returning empty values when invalid parameters are passed to any public API method +* [disable the maximum number size limit][api-guide-config-feature-no-max-number-size] which ensures the interoperability with the [algorithm reference implementation][hashids-js] and allows the usage of the Java `Long` [maximum value][long-max-value] + +> Please note that most features will break the interoperability with the [algorithm reference implementation][hashids-js]! + +--- + +Copyright © 2017 +Arctic Ice Studio
+ + + +[api-curse-word-prevention]: api/curse-word-prevention.md +[api-guide-config-alphabet]: api/guide/configuration/index.md#determine-a-custom-alphabet +[api-guide-config-feature-exception-handling]: api/guide/configuration/features.md#exception-handling +[api-guide-config-feature-hex-prefix]: api/guide/configuration/features.md#allow-hexadecimal-number-prefixes +[api-guide-config-feature-no-max-number-size]: api/guide/configuration/features.md#no-number-size-limit +[api-guide-config-min-hash-length]: api/guide/configuration/index.md#defining-a-minimum-hash-length +[api-guide-config-salt]: api/guide/configuration/index.md#using-a-salt +[api-guide-decoding]: api/guide/decoding.md +[api-guide-encoding]: api/guide/encoding.md +[api-instances]: api/instances.md +[hashids]: http://hashids.org +[hashids-js]: https://github.com/ivanakimov/hashids.js +[long-max-value]: https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html#MAX_VALUE diff --git a/src/main/docs/SUMMARY.md b/src/main/docs/SUMMARY.md new file mode 100644 index 0000000..93d19ae --- /dev/null +++ b/src/main/docs/SUMMARY.md @@ -0,0 +1,63 @@ +# Summary + +### Introduction + - [Overview](README.md) + - [Features](README.md#features) + +### Getting Started + - [Requirements](getting-started/requirements.md) + - [Installation](getting-started/installation.md) + +### API + - [Overview](api/index.md) + - [Imports](api/imports.md) + - [Instances](api/instances.md) + - [Configuration](api/instances.md#configuration) + - [Features](api/instances.md#features) + - [Curse Word Prevention](api/curse-word-prevention.md) + - [Guide](api/guide/index.md) + - [Configuration](api/guide/configuration/index.md) + - [Using A Salt](api/guide/configuration/index.md#using-a-salt) + - [Defining A Minimum Hash Length](api/guide/configuration/index.md#defining-a-minimum-hash-length) + - [Determine A Custom Alphabet](api/guide/configuration/index.md#determine-a-custom-alphabet) + - [Enable Additional Features](api/guide/configuration/features.md) + - [Hexadecimal Number Prefixes](api/guide/configuration/features.md#allow-hexadecimal-number-prefixes) + - [Exception Handling](api/guide/configuration/features.md#exception-handling) + - [No Number Size Limit](api/guide/configuration/features.md#no-number-size-limit) + - [Encoding](api/guide/encoding.md) + - [Natural Numbers](api/guide/encoding.md#natural-numbers) + - [Hexadecimal Numbers](api/guide/encoding.md#hexadecimal-numbers) + - [Decoding](api/guide/decoding.md) + - [Natural Numbers](api/guide/decoding.md#natural-numbers) + - [Hexadecimal Numbers](api/guide/decoding.md#hexadecimal-numbers) + - [Single Number As Optional](api/guide/decoding.md#single-number-as-optional) + +### Development + - [Building](development/building.md) + - [From Source](development/building.md#from-source) + - [Documentations](development/building.md#documentations) + - [Testing](development/testing.md) + - [Unit Tests](development/testing.md#unit-tests) + - [Interoperability Tests](development/testing.md#interoperability-tests) + - [Contributing](development/contributing.md) + - [Getting Started](development/contributing.md#getting-started) + - [Bug Reports](development/contributing.md#bug-reports) + - [Enhancement Suggestions](development/contributing.md#enhancement-suggestions) + - [Pull Requests](development/contributing.md#pull-requests) + - [Documentations](development/contributing.md#documentations) + - [Branch Organization](development/contributing.md#branch-organization) + - [How else can I help?](development/contributing.md#how-else-can-i-help) + - [Improve Issues](development/contributing.md#improve-issues) + - [Give Feedback On Issues and Pull Requests](development/contributing.md#give-feedback-on-issues-and-pull-requests) + - [Styleguides](development/contributing.md#styleguides) + - [Java Code Style](development/contributing.md#java-code-style) + - [Git Commit Messages](development/contributing.md#git-commit-messages) + - [MCVE](development/contributing.md#mcve) + - [Versioning](development/contributing.md#versioning) + - [Credits](development/contributing.md#credits) + - [Changelog](development/changelog.md) + - [0.4.0](development/changelog.md#040) + - [0.3.0](development/changelog.md#030) + - [0.2.0](development/changelog.md#020) + - [0.1.0](development/changelog.md#010) + - [Code of Conduct](development/code-of-conduct.md) diff --git a/src/main/docs/api/curse-word-prevention.md b/src/main/docs/api/curse-word-prevention.md new file mode 100644 index 0000000..de6ba05 --- /dev/null +++ b/src/main/docs/api/curse-word-prevention.md @@ -0,0 +1,11 @@ +# Curse Word Prevention + +The algorithm was written with the intent of placing hashes in visible places like the URLs. It is possible that the created hash ends up accidentally contaning a bad or offensive word(s). + +Therefore, it tries to avoid generating most common english curse words with the [default alphabet][javadoc-gh-pages-default-alphabet] by using separators which will never be placed next to each other. + +``` +c, C, f, F, h, H, i, I, s, S, t, T, u, U +``` + +[javadoc-gh-pages-default-alphabet]: https://arcticicestudio.github.io/icecore-hashids/javadoc/com/arcticicestudio/icecore/hashids/Hashids.html#DEFAULT_ALPHABET diff --git a/src/main/docs/api/guide/configuration/features.md b/src/main/docs/api/guide/configuration/features.md new file mode 100644 index 0000000..2b4ebf5 --- /dev/null +++ b/src/main/docs/api/guide/configuration/features.md @@ -0,0 +1,48 @@ +# Enable Additional Features + +> Please note that most features will break the interoperability with the [algorithm reference implementation][hashids-js]! + +## Allow Hexadecimal Number Prefixes + +When this feature is enabled the public API methods [`encodeHex(String)`][guide-encode-hex] and [`decodeHex(String)`][guide-decode-hex] will allow to pass `0x` or `0X` prefixed hexadecimal numbers. + +```java +final Hashids hashids = new Hashids.Builder() + .features(HashidsFeature.ALLOW_HEXADECIMAL_NUMBER_PREFIX) + .build(); + +// Both will be "j2g9K4y" +final String hashHexPrefixLowercase = hashids.encodeHex("0x75bcd15"); +final String hashHexPrefixUppercase = hashids.encodeHex("0X75bcd15"); +``` + +## Exception Handling + +When this feature is enabled the instance will handle errors by throwing specific exceptions instead of returning empty values when invalid parameters are passed to any public API method. This allows a more accurate analysis and treatment of the error by using language specific advantages. + +This example will throw a `IllegalArgumentException` to handle negative numbers: + +```java +final Hashids hashids = new Hashids.Builder() + .features(HashidsFeature.EXCEPTION_HANDLING) + .build(); + +final long invalidNumber = -123L; +final String hash = hashids.encode(invalidNumber); +``` + +## No Number Size Limit + +This instance feature disables the maximum number size limit which ensures the interoperability with the [algorithm reference implementation][hashids-js] and allows the usage of the Java `Long` [maximum value][long-max-value]. + +```java +final Hashids hashids = new Hashids.Builder() + .features(HashidsFeature.NO_MAX_INTEROP_NUMBER_SIZE) + .build(); + +final String hash = hashids.encode(Hashids.MAX_INTEROP_NUMBER_SIZE + 1L); +``` + +[guide-decode-hex]: ../decoding.md#hexadecimal-numbers +[guide-encode-hex]: ../encoding.md#hexadecimal-numbers +[hashids-js]: https://github.com/ivanakimov/hashids.js diff --git a/src/main/docs/api/guide/configuration/index.md b/src/main/docs/api/guide/configuration/index.md new file mode 100644 index 0000000..c7ba86e --- /dev/null +++ b/src/main/docs/api/guide/configuration/index.md @@ -0,0 +1,49 @@ +# Configuration + +Instances are configured by using the `Hashids.Builder` which can be simply imported statically as described in the [imports][api-overview-imports] chapter. + +All configurations are compatible and can be combined. + +* [`salt(String)`](#using-a-salt) - Sets the salt to be used as entropy +* [`minLength(int)`](#defining-a-minimum-hash-length) - Sets the minimum hash length +* [`alphabet(String)`](#determine-a-custom-alphabet) - Sets the alphabet to be used for the hash generation +* [`features(HashidsFeature...)`][guide-config-features] - Enables the given instance feature + +## Using A Salt + +A salt adds additional entropy to the hash generation and can be used to increase the protection against unintentional hash decoding by third parties. + +```java +final Hashids hashids = new Hashids.Builder() + .salt("salt and pepper") + .build(); + +final String hash = hashids.encode(1234567L); // Result: "BbVrQ" +``` + +## Defining A Minimum Hash Length + +This configuration allows to determine the *minimum* length of the generated hash. By default the length is set to `0` which will generate hashes with the required length only depending on the given number(s). + +```java +final Hashids hashids = new Hashids.Builder() + .minHashLength(8) + .build(); + +final String hash = hashids.encode(42L); // Result: "WPe9xdLy" +``` + +## Determine A Custom Alphabet + +The alphabet for the hash generation can be customized but **must contain at least 16 unique characters**. + +```java +final Hashids hashids = new Hashids.Builder() + .alphabet("abcdefgh12345678") + .build(); + +final String hash = hashids.encode(1234567L); // Result: "edd6185" +``` + +[api-overview-imports]: ../../imports.md +[guide-config-features]: features.md diff --git a/src/main/docs/api/guide/decoding.md b/src/main/docs/api/guide/decoding.md new file mode 100644 index 0000000..8fd7150 --- /dev/null +++ b/src/main/docs/api/guide/decoding.md @@ -0,0 +1,72 @@ +# Decoding + +> In order to decode a hash the [instance algorithm configuration][algorithm-config] **must be equal to the configuration the hash has been encoded with**, otherwise the returned value will be empty! + +If the [exception handling][feature-exception-handling] feature is enabled a `IllegalArgumentException` will be thrown instead when the passed hash is not valid or a `NullPointerException` if the passed argument is `null`. + +This example will throw a `IllegalArgumentException` due to instance incompatibility: + +```java +final Hashids saltyHashids = new Hashids.Builder() + .salt("salt and pepper") + .features(HashidsFeature.EXCEPTION_HANDLING) + .build(); + +final Hashids fruityHashids = new Hashids.Builder() + .salt("coconut and yogurt") + .features(HashidsFeature.EXCEPTION_HANDLING) + .build(); + +final String hash = saltyHashids.encode(123L); +final long[] invalidNumbers = fruityHashids.decode(hash); // IllegalArgumentException +``` + +## Natural Numbers + +To decode a hash use the public API method `decode(String)`. + +```java +final Hashids hashids = new Hashids.Builder() + .salt("salt and pepper") + .build(); + +final long[] number = hashids.decode("Blk"); // Result: [123] +final long[] numbers = hashids.decode("9dTLhR"); // Result: [42, 5, 17] +``` + +## Hexadecimal Numbers + +Hexadecimal numbers can be decoded by using the public API method `decodeHex(String)`. + +```java +final Hashids hashids = new Hashids.Builder() + .salt("salt and pepper") + .build(); + +final String hexNumber = hashids.decodeHex("j2g9K4y"); // Result: "75bcd15" +``` + +## Single Number As Optional + +The `decodeOne(String)` public API method simplifies the use-case where the amount of resulting numbers is known before to handle the return value as single value instead of an array. + +It decodes into a single numeric representation and returns it as `Optional` if the given hash is valid, otherwise `Optional.EMPTY`. + +> The given hash **must resolve into a one number only**, otherwise the [`decode(String)`](#natural-numbers) public API method must be used. + +```java +final Hashids hashids = new Hashids.Builder() + .salt("salt and pepper") + .build(); + +final String singleNumberHash = hashids.encode(123L); +final String multipleNumberHash = hashids.encode(42L, 5L, 17L); + +final Optional- * This class is immutable. - *
- * - * @author Arctic Ice Studio <development@arcticicestudio.com> - * @see Hashids - * @since 0.1.0 - */ -public final class Hashid { - - private final long[] numbers; - private final String hash; - - /** - * A empty hashid with no numbers and no hash. - */ - public static final Hashid EMPTY = new Hashid(new long[0], ""); - - /** - * Constructs a new hashid with the specified numbers and hash. - * - * @param longs the numbers of this hashid - * @param hash the hash of this hashid - */ - public Hashid(long[] longs, String hash) { - this.numbers = longs; - this.hash = hash; - } - - /** - * All numbers of this hashid. - * - * @return a new array with all numbers of this hashid - */ - public long[] numbers() { - long[] values = new long[numbers.length]; - System.arraycopy(numbers, 0, values, 0, numbers.length); - return values; - } - - /** - * Returns the hash of this hashid. - * - * @return the hash of this hashid - */ - @Override - public String toString() { - return hash; - } - - @Override - public int hashCode() { - int hashCode = 7; - hashCode = 97 * hashCode + Arrays.hashCode(this.numbers); - hashCode = 97 * hashCode + Objects.hashCode(this.hash); - return hashCode; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Hashid other = (Hashid) obj; - return Arrays.equals(this.numbers, other.numbers) && Objects.equals(this.hash, other.hash); - } -} diff --git a/src/main/java/com/arcticicestudio/icecore/hashids/Hashids.java b/src/main/java/com/arcticicestudio/icecore/hashids/Hashids.java index dc1dab9..5d640c1 100644 --- a/src/main/java/com/arcticicestudio/icecore/hashids/Hashids.java +++ b/src/main/java/com/arcticicestudio/icecore/hashids/Hashids.java @@ -1,630 +1,758 @@ -/* -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -title Hashids Public API + -project icecore-hashids + -repository https://github.com/arcticicestudio/icecore-hashids + -author Arctic Ice Studio + -email development@arcticicestudio.com + -copyright Copyright (C) 2017 + -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -*/ package com.arcticicestudio.icecore.hashids; -import java.util.ArrayList; -import java.util.List; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.reducing; +import static java.util.stream.Collectors.toSet; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; /** - * Generates short, unique, non-sequential and decodable hashids from positive unsigned (long) integer numbers. - *- * Serves as the entry point to the - * IceCore Hashids public API. - *
- *- * The Hashids's {@code salt} is used as a secret to generate unique strings using a given {@code alphabet}. - * Generated strings can have a {@code minHashLength}. - *
- *- * If used to obfuscates identities, make sure to not expose your {@code salt}, {@code alphabet} nor - * {@code separators} to a client, client-side is not safe. - *
- *- * Only positive numbers are supported. - * All methods in this class will throw an {@link IllegalArgumentException} if a negative number is given. - * To use negative numbers prepend a hyphen character to the hash string. - * - * Note that this method is limited to single number hashes only and breaks the official Hashids definition! - * - * Example: - *
- * Hashids hashids = new Hashids("salt"); - * long number = -1234567890; - * String enc = (Math.abs(number) != number ? "-" : "") + hashids.encodeToString(Math.abs(number)); - * long dec = enc.startsWith("-") ? hashids.decodeLongNumbers(enc.substring(1))[0] : hashids.decodeLongNumbers(enc)[0]; - *+ * A lightweight generator for short, unique, case-sensitive and non-sequential decodable hashes from positive unsigned (long) integer numbers. * - *
- * {@code Hashids} instances are thread-safe. - *
+ *Implementation of the Hashids algorithm: + *
An instance with the default interoperable configurations is available through the {@link Hashids#Hashids() default constructor}. + * + *
Configured instances can be created via {@link Hashids.Builder} to set a {@link Hashids.Builder#salt salt}, define a + * ·{@link Hashids.Builder#minLength minimum hash length} or use a {@link Hashids.Builder#alphabet custom alphabet}. Optional {@link HashidsFeature features} + * can be enabled via the {@link Hashids.Builder#features features(HashidsFeature)} method. Please note that most features will break the + * interoperability with the origin algorithm implementation! + * + *
Instances of this class are thread-safe. * * @author Arctic Ice Studio <development@arcticicestudio.com> - * @see IceCore Hashids - * @see Hashids - * @version 0.3.0 + * @see IceCore Hashids GitHub Repository + * @version 0.4.0 */ public final class Hashids { - /** - * Holds the default alphabet. - */ - public static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + private static final String VERSION = "0.4.0"; + private static final String VERSION_INTEROP = "1.0.0"; - /** - * Holds the default separators. - *
- * Used to prevent the generation of strings that contain bad, offensive and rude words. - *
- */ - public static final String DEFAULT_SEPARATORS = "cfhistuCFHISTU"; + private static final int LOTTERY_MOD = 100; + private static final double GUARD_THRESHOLD = 12; + private static final double SEPARATOR_THRESHOLD = 3.5; + private static final Pattern HEX_VALUES_PATTERN = Pattern.compile("[\\w\\W]{1,12}"); + private static final Pattern HEX_FORMAT_PATTERN = Pattern.compile("^[0-9a-fA-F]+$"); /** - * Holds the maximum number value. - *- * This limit is mandatory in order to ensure interoperability. - *
- *- * JavaScript equivalents used in hashids.js: - *
This limit is based on the + * JavaScript Number.MAX_SAFE_INTEGER + * constant which represents the maximum safe integer. * - * @since 0.3.0 + *
The limit can be canceled by enabling the {@link HashidsFeature#NO_MAX_INTEROP_NUMBER_SIZE NO_MAX_INTEROP_NUMBER_SIZE} feature. Please + * note that this will break the interoperability with the origin algorithm implementation! + * + * @see ECMAScript 2016 Language Specification */ - public static final String VERSION = "0.3.0"; - - private static final int GUARD_DIV = 12; - private static final int MIN_ALPHABET_LENGTH = 16; - private static final double SEP_DIV = 3.5; - private static final Pattern PATTERN_ENCODE_HEX = Pattern.compile("^[0-9a-fA-F]+$"); - private static final Pattern PATTERN_ALPHABET_REPLACE = Pattern.compile("\\s+"); - - private final String alphabet; - private final String guards; - private final int minHashLength; - private final String salt; - private final String separators; + public static final long MAX_INTEROP_NUMBER_SIZE = (long) Math.pow(2, 53) - 1; /** - * Constructs a new Hashid with all default values. - *
- *
- *
A customized alphabet can be set via the instance builder {@link Hashids.Builder#alphabet alphabet} method. */ - public Hashids(String salt) { - this(salt, 0); - } + public static final char[] DEFAULT_ALPHABET = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' + }; /** - * Constructs a new Hashid with the specified salt and the minimal hash length and the default alphabet and - * separators. - *
- *
- *
- *
- * Each method returns a new builder instance. - *
- *- * Defaults to - *
By default the salt is empty. * - * @param salt The string to use as salt - * @return The builder instance with the specified salt + * @param salt the salt to be used as entropy + * @return a new builder instance with the given salt */ - public Builder salt(String salt) { - return new Builder(salt, alphabet, separators, minHashLength); + public Builder salt(final String salt) { + this.salt = salt.toCharArray(); + return this; } /** - * Sets the custom alphabet string. + * Sets the minimum hash length. * - * @param alphabet The string to use as custom alphabet - * @return The builder instance with the specified custom alphabet + *
The default value is {@code 0}. + * + * @param minLength the minimum hash length + * @return a new builder instance with the given minimum hash length */ - public Builder alphabet(String alphabet) { - return new Builder(salt, alphabet, separators, minHashLength); + public Builder minLength(final int minLength) { + this.minLength = minLength; + return this; } /** - * Sets the custom separators string. + * Sets the alphabet to be used for the hash generation. + * + *
The default value is the {@link #DEFAULT_ALPHABET default alphabet}. * - * @param separators The string to use as custom alphabet - * @return The builder instance with the specified custom separators + * @param alphabet the alphabet to be used for the hash generation + * @return a new builder instance with the given alphabet */ - public Builder separators(String separators) { - return new Builder(salt, alphabet, separators, minHashLength); + public Builder alphabet(final String alphabet) { + this.alphabet = alphabet.toCharArray(); + return this; } /** - * Sets the minimum hash length. + * Enables the given instance {@link HashidsFeature features}. * - * @param minHashLength The minimum length of the hash - * @return The builder instance with the minimum hash length + *
By default no features are enabled. + * + * @param features the features to be enabled for this instance + * @return a new builder instance with the enabled features + * @since 0.4.0 */ - public Builder minHashLength(int minHashLength) { - return new Builder(salt, alphabet, separators, minHashLength); + public Builder features(final HashidsFeature... features) { + this.features.addAll(Arrays.asList(features)); + return this; } /** - * Builds the {@link Hashids}. + * Builds a new configured {@link Hashids} instance. * - * @return The {@link Hashids} instance + * @return a new configured instance */ public Hashids build() { - return new Hashids(salt, minHashLength, alphabet, separators); + return new Hashids(salt, minLength, alphabet, features); } } /** - * Encode number(s). + * Encodes the given positive numbers based on this instance configuration. + * + *
The given numbers size must not be larger than the {@link #MAX_INTEROP_NUMBER_SIZE maximum interoperability size} unless the + * {@link HashidsFeature#NO_MAX_INTEROP_NUMBER_SIZE NO_MAX_INTEROP_NUMBER_SIZE} feature is enabled. Please + * note that this will break the interoperability with the origin algorithm implementation! * - * @param numbers the number(s) to encode - * @return the Hashid instance with the number(s) and the encoded string + * @param numbers the positive numbers to be encoded + * @return the resultant hash of the encoding of the numbers, empty otherwise + * @throws IllegalArgumentException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the total length of + * numbers is zero, any numbers size is invalid or larger than the {@link #MAX_INTEROP_NUMBER_SIZE maximum interoperability size} + * @throws NullPointerException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the given numbers are {@code null} */ - public Hashid encode(long... numbers) { - if (numbers.length == 0) { - return Hashid.EMPTY; + public String encode(final long... numbers) { + if (numbers == null) { + if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) { + throw new NullPointerException("numbers must not be null!"); + } + return ""; } - return doEncode(numbers); - } - /** - * Encode number(s) to string. - * - * @param numbers the number(s) to encode - * @return the encoded string - */ - public String encodeToString(long... numbers) { if (numbers.length == 0) { + if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) { + throw new IllegalArgumentException("length of numbers must be greater than or equal to one!"); + } return ""; } - return encode(numbers).toString(); + + for (long number : numbers) { + if (number < 0) { + if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) { + throw new IllegalArgumentException("number must not be less than zero: " + number); + } + return ""; + } + + if (number > MAX_INTEROP_NUMBER_SIZE && !features.contains(HashidsFeature.NO_MAX_INTEROP_NUMBER_SIZE)) { + if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) { + throw new IllegalArgumentException("number must not exceed the maximum number size: " + number + " > " + MAX_INTEROP_NUMBER_SIZE); + } + return ""; + } + } + + final char[] currentAlphabet = Arrays.copyOf(alphabet, alphabet.length); + + // Determine the lottery number + final long lotteryId = LongStream.range(0, numbers.length) + .reduce(0, (state, idx) -> state + numbers[(int) idx] % (idx + LOTTERY_MOD)); + final char lottery = currentAlphabet[(int) (lotteryId % currentAlphabet.length)]; + + // Encode each number + final StringBuilder global = new StringBuilder(); + IntStream.range(0, numbers.length) + .forEach(idx -> { + deriveNewAlphabet(currentAlphabet, salt, lottery); + final int initialLength = global.length(); + transform(numbers[idx], currentAlphabet, global, initialLength); + // Append the separator + if (idx + 1 < numbers.length) { + long n = numbers[idx] % (global.charAt(initialLength) + idx); + global.append(separators[(int) (n % separators.length)]); + } + }); + + // Prepend the lottery + global.insert(0, lottery); + + // Add the guards if there is any space left + if (minLength > global.length()) { + int guardIdx = (int) ((lotteryId + lottery) % guards.length); + global.insert(0, guards[guardIdx]); + if (minLength > global.length()) { + guardIdx = (int) ((lotteryId + global.charAt(2)) % guards.length); + global.append(guards[guardIdx]); + } + } + + // Add the necessary padding + int paddingLeft = minLength - global.length(); + while (paddingLeft > 0) { + shuffle(currentAlphabet, Arrays.copyOf(currentAlphabet, currentAlphabet.length)); + + final int alphabetHalfSize = currentAlphabet.length / 2; + final int initialSize = global.length(); + if (paddingLeft > currentAlphabet.length) { + int offset = alphabetHalfSize + (currentAlphabet.length % 2 == 0 ? 0 : 1); + + global.insert(0, currentAlphabet, alphabetHalfSize, offset); + global.insert(offset + initialSize, currentAlphabet, 0, alphabetHalfSize); + + paddingLeft -= currentAlphabet.length; + } else { + // Calculate the excess + final int excess = currentAlphabet.length + global.length() - minLength; + final int secondHalfStartOffset = alphabetHalfSize + Math.floorDiv(excess, 2); + final int secondHalfLength = currentAlphabet.length - secondHalfStartOffset; + final int firstHalfLength = paddingLeft - secondHalfLength; + + global.insert(0, currentAlphabet, secondHalfStartOffset, secondHalfLength); + global.insert(secondHalfLength + initialSize, currentAlphabet, 0, firstHalfLength); + + paddingLeft = 0; + } + } + + return global.toString(); } /** - * Encode number(s) to string. + * Encodes the given numbers in hexadecimal format based on this instance configuration. * - * @param numbers the number(s) to encode - * @return the encoded string + *
The given hexadecimal numbers must not be prefixed ({@code 0x} or {@code 0X}) unless the
+ * {@link HashidsFeature#ALLOW_HEXADECIMAL_NUMBER_PREFIX ALLOW_HEXADECIMAL_NUMBER_PREFIX} feature is enabled. Please
+ * note that this will break the interoperability with the origin algorithm implementation!
+ *
+ * @param hexNumbers the numbers in hexadecimal format to be encoded
+ * @return the resultant hash of the encoding of the hexadecimal numbers, empty otherwise
+ * @throws IllegalArgumentException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and any of the numbers is invalid
+ * @throws NullPointerException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the given numbers are {@code null}
*/
- public String encodeToString(int... numbers) {
- if (numbers.length == 0) {
+ public String encodeHex(final String hexNumbers) {
+ if (hexNumbers == null) {
+ if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) {
+ throw new NullPointerException("hexNumbers must not be null!");
+ }
+ return "";
+ }
+
+ final String hex;
+ if (hexNumbers.startsWith("0x") || hexNumbers.startsWith("0X")) {
+ if (features.contains(HashidsFeature.ALLOW_HEXADECIMAL_NUMBER_PREFIX)) {
+ hex = hexNumbers.substring(2);
+ } else {
+ if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) {
+ throw new IllegalArgumentException("numbers must not contain a hexadecimal prefix: " + hexNumbers.substring(0, 2));
+ }
+ return "";
+ }
+ } else {
+ hex = hexNumbers;
+ }
+
+ if (!HEX_FORMAT_PATTERN.matcher(hex).matches()) {
+ if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) {
+ throw new IllegalArgumentException("hexNumbers must be a valid hexadecimal number!");
+ }
return "";
}
- long[] longs = new long[numbers.length];
- for (int idx = 0; idx < numbers.length; idx++) {
- longs[idx] = numbers[idx];
+ // Resolve the associated long value and encode it
+ LongStream values = LongStream.empty();
+ final Matcher matcher = HEX_VALUES_PATTERN.matcher(hex);
+ while (matcher.find()) {
+ final long value = new BigInteger("1" + matcher.group(), 16).longValue();
+ values = LongStream.concat(values, LongStream.of(value));
}
- return doEncode(longs).toString();
+
+ return encode(values.toArray());
}
/**
- * Encode an hexadecimal string to string.
+ * Decodes the given hash into its numeric representation based on this instance configuration.
*
- * @param hex the hexadecimal string to encode
- * @return the encoded string
+ * @param hash the hash to be decoded
+ * @return an array of long values with each numeric number present in the hash, empty otherwise
+ * @throws IllegalArgumentException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the hash is invalid
+ * @throws NullPointerException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the given hash is {@code null}
*/
- public String encodeHex(String hex) {
- if (!PATTERN_ENCODE_HEX.matcher(hex).matches()) {
- throw new IllegalArgumentException(String.format("%s is not a hex string", hex));
+ public long[] decode(final String hash) {
+ if (hash == null) {
+ if (features.contains(HashidsFeature.EXCEPTION_HANDLING)) {
+ throw new NullPointerException("hash must not be null!");
+ }
+ return new long[0];
}
- Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hex);
- List Simplifies the use-case where the amount of resulting numbers is known before to handle the return value as single value instead of an array.
+ * The given hash must resolve into a one number only, otherwise see the {@link #decode(String) decode} method.
+ *
+ * @param hash the valid hash to be decoded
+ * @return the decoded number if the given hash is valid, empty otherwise
+ * @throws IllegalArgumentException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the hash is invalid
+ * @throws NullPointerException if the {@link HashidsFeature#EXCEPTION_HANDLING EXCEPTION_HANDLING} feature is enabled and the given hash is {@code null}
+ * @since 0.4.0
*/
- public long[] decodeLongNumbers(String hash) {
- if (isEmpty(hash)) {
- return new long[0];
+ public Optional Please note that almost every feature will break the interoperability with the origin algorithm implementation if enabled!
+ *
+ * @author Arctic Ice Studio <development@arcticicestudio.com>
+ * @since 0.4.0
+ */
+public enum HashidsFeature {
+ /**
+ * Enables support for the {@code 0x} or {@code 0X} hexadecimal format prefixes of parameter values passed to the
+ * {@link Hashids#encodeHex(String) encodeHex(String)}and {@link Hashids#decodeHex(String) decodeHex(String)} methods.
+ *
+ * Please note that this will break the interoperability with the origin algorithm implementation!
+ *
+ * @see Hexadecimal - Written representation
+ */
+ ALLOW_HEXADECIMAL_NUMBER_PREFIX,
+
+ /**
+ * Enables the handling of exceptions instead of returning empty values when invalid parameters are passed to any public API method.
+ *
+ * Please note that this will break the interoperability with the origin algorithm implementation!
+ */
+ EXCEPTION_HANDLING,
+
+ /**
+ * Disables the {@link Hashids#MAX_INTEROP_NUMBER_SIZE maximum number size limit} which ensures the interoperability with the origin algorithm implementation
+ * hashids.js and allows the usage of the {@link Long#MAX_VALUE Java Long maxiumum value}.
+ *
+ * Please note that this will break the interoperability with the origin algorithm implementation!
+ */
+ NO_MAX_INTEROP_NUMBER_SIZE;
+
+ /**
+ * Enables all features.
+ */
+ public static final EnumSet
- * The main entry point of the package is the {@link com.arcticicestudio.icecore.hashids.Hashids} class to to generate
- * short, unique, non-sequential and decodable hashids from positive unsigned (long) integer numbers.
- *