diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..dec0ebf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea/
+*.iml
+target
+.classpath
+.project
+.settings/
+logs/
+error-reports/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..76219841
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,51 @@
+#CONTRIBUTING
+
+We welcome new contributors. Here you can read our guidelines to contribute to the IRIS project.
+
+## Found a bug
+
+If you found a bug in our software or mistakes in the documentation there are two ways to help us:
+
+1. [Submit an issue](#submitting-an-issue) which explains the bug to our [GitHub Repository](https://github.com/Contargo/iris/issues/).
+2. [Submit a pull request](#pull-requests) that fixes the bug.
+
+
+## Want a new Feature
+
+If you want a new Feature in IRIS there are two ways to get it done:
+
+1. Request the new feature by [submitting an issue](#submitting-an-issue) to our [GitHub Repository](https://github.com/Contargo/iris/issues/).
+2. [Submit a Pull Request](#pull-requests) that contains the new feature.
+
+
+## Submission Guidelines
+
+### Submitting an Issue
+
+Before you submit an issue check the [GitHub Repository](https://github.com/Contargo/iris/issues/) to see if someone else reported the same issue.
+
+If you submit a bug, provide as much information as needed for us to reproduce the bug.
+
+
+### Pull Requests
+
+Before submitting a pull request do following things:
+
+* If you are not familiar with GitHubs pull requests take a look at their documentation of [Using pull requests](https://help.github.com/articles/using-pull-requests/).
+* Search our [GitHub Repository](https://github.com/Contargo/iris) for pull requests doing the same, so you don't have to put effort in something that's already done.
+* If there is no active pull request implementing your feature then fork [IRIS](https://github.com/Contargo/iris)
+* Create a new branch:
+```
+ git checkout -b my-branch master
+```
+* Make your changes.
+* Write Unit-Tests for all your changes.
+* Run the full test suite to verify you did not broke anything:
+```
+ mvn clean verify
+```
+* Commit your changes. Write a commit message that explains what your changes are for, so everyone can understand what it does. See [this](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) explanation for how to right commit message. If your changes are related to an open issue, reference the issue number in the last line of your commit message. Example: ```References #12```.
+* Push the branch with your changes to GitHub. ```git push origin my-branch```
+* In GitHub, send a pull request to ```iris:master```
+
+If you want to learn more about, how to write a good pull request, read [this](https://github.com/blog/1943-how-to-write-the-perfect-pull-request) blog post of GitHub.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..7e61f5c7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,126 @@
+IRIS - Intermodal Routing Information System
+=========
+
+The goal of IRIS is to create a centralized and transparent database for distance calculations in short-haul goods transport.
+IRIS aims to provide such a platform, for both clients and service providers.
+
+IRIS is used to calculate truckings for transports from a hinterland terminal to the place of loading and the other way around.
+The place of loading can be set very detailed by adjusting its coordinates.
+IRIS uses these coordinates to calculate the truck route, distance, toll kilometers and the duration of the trucking.
+An extension for barge and rail routings is already planned.
+
+Example: A freight container arrives in Mannheim via cargo ship and needs to be transported to 48 Berliner Str., Heidelberg, Germany.
+After arriving at the Contargo terminal in Mannheim it needs to be loaded on a truck to be transported to its destination.
+There, the recipient unloads the content and the truck transports the empty container back to the terminal.
+
+# Features
+* Management of seaports, terminals and connections between them (GUI)
+* Automatic creation of truck and barge routes between seaports, terminals and destinations (GUI & REST-API)
+* Provision of detailed information for each route like distance, toll distance, duration, CO2 output (REST-API)
+* Support for different container sizes and states (REST-API)
+* Open Street Map Address resolution for destination points (GUI & REST-API)
+* Management and resolution of custom static addresses as destination points (GUI & REST-API)
+* Possibility to implement any authentication mechanism via Spring Security
+
+Further documentation about the IRIS-terminology can be found here: [terminology](docs/terminology.md).
+
+
+#Prerequisites
+ - MySQL/MariaDb 5.5 or higher
+ - Maven 3 or higher
+ - JDK 8 or higher
+
+
+#Getting started
+
+Simply clone this repository
+```sh
+$ git clone https://github.com/Contargo/iris.git
+```
+
+## Configuration
+
+Configuration is located in ```src/main/resources/```
+
+The "general" configuration file is ```src/main/resources/application.properties```
+
+
+### Environment Properties
+
+Environment (System) specific configurations go to ```src/main/resources/application-.properties```
+
+The environment-specific (e.g. ```application-dev.properties```) file overrides and adds to properties defined in the "general" properties-file (```application.properties```). Not overridden properties of ```application.properties``` remain valid.
+
+Default Environment is "dev" (so ```application-dev.properties``` is loaded by default). Environments can be set using System-Property "environment"
+
+```sh
+mvn jetty:run -Denvironment=myenv # -> leads to use application-myenv.properties
+```
+or environment-parameter environment:
+```sh
+export environment=superdev
+mvn jetty:run # -> leads to use application-superdev.properties
+```
+
+#### Database
+
+All database connection settings are configured in ```application-.properties```. Adapt the corresponding properties to match your MySQL/MariaDb database connection settings.
+
+All needed database tables are created on application start using [Liquibase](http://www.liquibase.org/).
+
+
+### Roles
+
+There are two different roles defined in IRIS:
+
+- ROLE_ADMIN
+ - can do anything
+- ROLE_USER
+ - no GUI access
+ - limited REST Api access
+
+
+### User Credentials
+
+User Credentials are are located in ```src/main/resources/usercredentials-.properties```.
+
+For development IRIS ships with two predefined users:
+- admin@example.com with password admin: ROLE_ADMIN
+- user@example.com with password user: ROLE_USER
+
+These are located in ```src/main/resources/usercredentials-dev.properties```.
+
+To add own user credentials do the following:
+```sh
+$ echo -n "yourpassword" | sha256sum
+e3c652f0ba0b4801205814f8b6bc49672c4c74e25b497770bb89b22cdeb4e951 -
+```
+Edit the file ```src/main/resources/usercredentials-.properties``` and add your user like this:
+```
+youruser@example.com=e3c652f0ba0b4801205814f8b6bc49672c4c74e25b497770bb89b22cdeb4e951,ROLE_USER,enabled
+```
+
+You can specify login, password, role and set the enabled flag.
+
+
+## Application Start
+
+In order to build the application you need Maven 3 and Oracle JDK 7. You also need your MySQL/MariaDb database set up as described above. You can then start the local web server:
+```sh
+$ mvn jetty:run
+```
+You can run a full build including all tests with
+```sh
+$ mvn clean install
+```
+Finally, point your browser to the url http://localhost:8082/. IRIS has both a basic user interface and a JSON API. Documentation for the API is located at http://localhost:8082/api/docs.html.
+
+
+# Contributing
+
+If you want to contribute to IRIS, see our [contribution guidelines](CONTRIBUTING.md).
+
+
+# Licensing
+
+IRIS is licensed under the GNU Affero General Public License, Version 3. See [LICENCE](LICENSE) for the full license text.
diff --git a/docs/terminology.md b/docs/terminology.md
new file mode 100644
index 00000000..58f45e21
--- /dev/null
+++ b/docs/terminology.md
@@ -0,0 +1,50 @@
+#Terminology
+
+##Terminal
+
+*inland (hinterland) container terminal*
+
+A facility where cargo containers are handled between different transport vehicles for onward transportation.
+The handling of containers is typically between barges or cargo trains and land vehicles (trucks).
+
+* **Region:** An area defined by specific parameters.
+Usually geographical division. (*Oberrhein, Mittelrhein, Niederrhein*) / (*Rhein-Neckar, Rhein-Main*).
+Contargo uses three regions among other things to assign and store the CO₂-parameter of our barge fleet.
+
+
+##Seaport
+Location on a coast where sea vessels and barges can dock and transfer cargo from or to land.
+Container seaports handle cargo in containers by different mechanical means. (crane, AGV, reach stacker)
+
+
+##Connections
+This shows the possibilities to connect a seaport and an inland terminal.
+Different and multiple connections are possible. For example barge or rail or both.
+
+* **Diesel-km:** The kilometers traveled on the train route or the barge route by use of diesel fuel.
+* **Electrical-km:** The kilometers on the train route traveled by use of electricity only.
+
+
+##Static Addresses
+A static address is a city with its corresponding postal code and country. For example *68159 Mannheim, Germany*.
+
+
+##Cloud distance
+A previously defined area (radius) around a static address.
+
+
+##Route types
+
+* **Barge:** Transport of goods / cargo on barge.
+Barge transport only happens between seaports and inland terminals or between inland terminals that are connected by a major river or canals.
+* **Rail:** Transport of goods / cargo between seaports and inland terminals on a freight train using rail roads.
+* **Truck:** Transport of goods / cargo from and to seaports and inland terminals and the loading / unloading site.
+
+
+##Route Combination
+
+* **Waterway:** Transport per barge between seaport and inland terminal and additional transport per truck to the loading / unloading site.
+* **Railway:** Transport per rail between seaport and inland terminal and additional transport per truck to the loading / unloading site.
+* **Direct Truck:** Transport only per truck from or to the seaport and the loading site.
+* **Roundtrip:** Waterway, Railway or Direct Truck transport from seaport to loading / unloading site and back to seaports.
+* **All:** A list of all the possibilities for transport for a given loading site.
\ No newline at end of file
diff --git a/iris-api-tests/pom.xml b/iris-api-tests/pom.xml
new file mode 100644
index 00000000..283a935b
--- /dev/null
+++ b/iris-api-tests/pom.xml
@@ -0,0 +1,118 @@
+
+
+
+4.0.0
+
+net.contargo
+iris-api-tests
+1.0-SNAPSHOT
+
+jar
+
+IRIS API Tests
+
+
+ UTF-8
+ UTF-8
+
+ 0.7-groovy-2.0
+ 2.2.2
+ 1.5
+ 2.0
+ 4.11
+ 2.3
+ 1.0.6
+ 0.7.1
+
+
+
+
+
+ org.codehaus.gmaven
+ gmaven-plugin
+ ${gmavenVersion}
+
+ ${gmavenProviderSelection}
+ UTF-8
+
+
+
+
+
+ testCompile
+
+
+
+
+
+ org.codehaus.groovy
+ groovy-all
+ ${groovyVersion}
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.10
+
+ true
+
+
+
+ exec-specs
+ test
+
+ test
+
+
+
+ **/*Spec.java
+
+ false
+
+
+
+
+
+
+
+
+
+ junit
+ junit-dep
+ ${junitVersion}
+
+
+ org.codehaus.groovy
+ groovy-all
+ ${groovyVersion}
+
+
+ org.spockframework
+ spock-core
+ ${spockCore}
+ test
+
+
+ org.codehaus.groovy
+ groovy-all
+
+
+ junit
+ junit-dep
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
+
+ org.codehaus.groovy.modules.http-builder
+ http-builder
+ ${httpBuilderVersion}
+
+
+
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/address/AddressByAddressDetailsSpec.groovy b/iris-api-tests/src/test/groovy/address/AddressByAddressDetailsSpec.groovy
new file mode 100644
index 00000000..7974d934
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/AddressByAddressDetailsSpec.groovy
@@ -0,0 +1,80 @@
+package address
+
+import spock.lang.Specification
+import util.ClientFactory
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+class AddressByAddressDetailsSpec extends Specification {
+
+ def "request for addresses by details like street, city etc. "() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "address details city, postal code and street"
+ def urlParams = [
+ "city": "karlsruhe",
+ "postalcode": "76131",
+ "street": "hirtenweg",
+ "country": "DE"
+ ]
+
+ when: "addresses by city, postal code and street are requested"
+ def response = client.get(path: "/api/geocodes", query: urlParams);
+ def listOfAddressLists = response.data.geoCodeResponse.addresses
+ def staticAddressesObject = listOfAddressLists[0]
+ def nominatimAddressesObject = listOfAddressLists[1]
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "the staticAddressesObject contains a parent address and a list of static addresses"
+ def parentAddressStatic = staticAddressesObject.parentAddress
+ def staticaddresses = staticAddressesObject.addresses
+ staticaddresses.size() >= 1
+
+ and: "the parent address object has certain attributes"
+ parentAddressStatic.size() == 10
+ parentAddressStatic.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+ parentAddressStatic.type == "ADDRESS"
+
+ and: "a static address contains certain attributes"
+ def staticaddress = staticaddresses[0]
+ staticaddress.size() == 10
+ staticaddress.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+
+ and: "the type of a static address is ADDRESS"
+ staticaddress.type == "ADDRESS"
+
+ and: "a static address contains a address map with certain attributes"
+ def addressmapStatic = staticaddress.address
+ addressmapStatic.size() == 6
+ addressmapStatic.keySet().containsAll("suburb", "static_id", "postcode", "country_code", "city", "hashkey")
+
+ and: "the nominatimAddressesObject contains a parent address and a list of static addresses"
+ def parentAddressNominatim = nominatimAddressesObject.parentAddress
+ def nominatimaddresses = nominatimAddressesObject.addresses
+ nominatimaddresses.size() >= 1
+
+ and: "the parent address object has certain attributes"
+ parentAddressNominatim.size() == 10
+ parentAddressNominatim.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+ parentAddressNominatim.type == "ADDRESS"
+
+ and: "a static address contains certain attributes"
+ def nominatimaddress = staticaddresses[0]
+ nominatimaddress.size() == 10
+ nominatimaddress.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+
+ and: "the type of a static address is ADDRESS"
+ nominatimaddress.type == "ADDRESS"
+
+ and: "a static address contains a address map with certain attributes"
+ def addressmapNominatim = staticaddress.address
+ addressmapNominatim.size() == 6
+ addressmapNominatim.keySet().containsAll("suburb", "static_id", "postcode", "country_code", "city", "hashkey")
+
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/address/AddressByGeolocationSpec.groovy b/iris-api-tests/src/test/groovy/address/AddressByGeolocationSpec.groovy
new file mode 100644
index 00000000..6b6ae8ed
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/AddressByGeolocationSpec.groovy
@@ -0,0 +1,39 @@
+package address
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class AddressByGeolocationSpec extends Specification {
+
+ def "request for geolocation"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "geolocation with latitude 49.123 and longituide 8.12 is requested"
+ def response = client.get(path: "/api/reversegeocode/49.123:8.12/")
+ def address = response.data.reverseGeocodeResponse.address
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "the address should have certain attributes"
+ def addressAttributes = address.keySet()
+ addressAttributes.size() == 10
+ addressAttributes.containsAll("niceName", "address", "placeId", "countryCode", "osmId", "longitude", "latitude",
+ "type", "shortName", "displayName");
+
+ and: "the address is of type ADDRESS"
+ address.type == "ADDRESS"
+
+ and: "the address has the requested longitude and latitude"
+ address.latitude == 49.123
+ address.longitude == 8.12
+
+ and: "the address's actual address has certain attributes"
+ address.address.keySet().size() == 7
+ println address.address.keySet()
+ address.address.keySet().containsAll("country", "country_code", "farmyard", "county", "postcode",
+ "state", "village")
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/address/AddressByOsmIdSpec.groovy b/iris-api-tests/src/test/groovy/address/AddressByOsmIdSpec.groovy
new file mode 100644
index 00000000..f9ae4e77
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/AddressByOsmIdSpec.groovy
@@ -0,0 +1,49 @@
+package address
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class AddressByOsmIdSpec extends Specification {
+
+ def "request for addresses with osm id "() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "address with osm id 90085697 is requested"
+ def response = client.get(path: "/api/osmaddresses/90085697")
+ def addresses = response.data.geoCodeResponse.addresses.addresses
+ def parentAddress = response.data.geoCodeResponse.addresses.parentAddress
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "there should be a parent address"
+ parentAddress.size() == 1
+
+ and: "that parent address should have certain attributes"
+ def parentAddressAttributes = parentAddress.get(0).keySet()
+ parentAddressAttributes.size() == 10
+ parentAddressAttributes.containsAll("niceName", "address", "placeId", "countryCode", "osmId", "longitude",
+ "latitude", "type", "shortName", "displayName")
+
+ and: "there should be exactly one address in the response"
+ def innerAddresses = addresses.get(0)
+ innerAddresses.size() == 1
+
+ and: "that address should have certain attributes"
+ def address = innerAddresses.get(0)
+ def addressAttributes = address.keySet()
+ addressAttributes.size() == 10
+ addressAttributes.containsAll("address", "displayName", "countryCode", "osmId", "latitude", "placeId",
+ "niceName", "shortName", "type", "longitude");
+
+ and: "that address has the requested osmid"
+ address.osmId == 90085697
+
+ and: "that address's actual address has certain attributes"
+ address["address"].keySet().size() == 10
+ address["address"].keySet().containsAll("country", "country_code", "road", "city", "state_district",
+ "postcode", "suburb", "house_number", "address29", "state")
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/address/AddressWherePlaceIsInSpec.groovy b/iris-api-tests/src/test/groovy/address/AddressWherePlaceIsInSpec.groovy
new file mode 100644
index 00000000..5aa1721b
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/AddressWherePlaceIsInSpec.groovy
@@ -0,0 +1,34 @@
+package address
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class AddressWherePlaceIsInSpec extends Specification {
+
+ def "request for addresses containing place"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "addresses with place id 50350574 are requested"
+ def response = client.get(path: "/api/places/50350574/addresses")
+ def addresses = response.data.addresses
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "there should be fifteen addresses in the response"
+ addresses.size() == 15
+
+ and: "an address should have certain attributes"
+ def address = addresses.get(0)
+ def addressAttributes = address.keySet()
+ addressAttributes.size() == 10
+ addressAttributes.containsAll("niceName", "address", "placeId", "countryCode", "osmId", "longitude", "latitude",
+ "type", "shortName", "displayName");
+
+ and: "that address has the requested placeid"
+ address.placeId == 50350574
+
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByBoundingBoxSpec.groovy b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByBoundingBoxSpec.groovy
new file mode 100644
index 00000000..b1e6aa0a
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByBoundingBoxSpec.groovy
@@ -0,0 +1,34 @@
+package address.staticsearch
+
+import spock.lang.Specification
+import util.ClientFactory
+
+/**
+ * @author Sandra Thieme - thieme@synyx.de
+ */
+class StaticAddressByBoundingBoxSpec extends Specification{
+
+ def "request for Static Addresses matching the given bounding box"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "a bounding box definition"
+ def urlParams = [
+ "lat": "49.0126538640",
+ "lon": "8.3770751953",
+ "distance": "10"
+ ]
+
+ when: "static addresses are requested"
+ def response = client.get(path: "/api/staticaddresses", query: urlParams)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "response consists of a list of static address uids"
+ def uidList = response.data.uids
+ uidList.size() == 1
+
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByGeolocationSpec.groovy b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByGeolocationSpec.groovy
new file mode 100644
index 00000000..6d89c634
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByGeolocationSpec.groovy
@@ -0,0 +1,57 @@
+package address.staticsearch
+
+import spock.lang.Specification
+import util.ClientFactory
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+class StaticAddressByGeolocationSpec extends Specification{
+
+ def "request for Static Addresses matching the given coordinates"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "a geolocation (latitude, longitude)"
+ def urlParams = [
+ "lat": "49.0126538640",
+ "lon": "8.3770751953"
+ ]
+
+ when: "static addresses are requested"
+ def response = client.get(path: "/api/staticaddresses", query: urlParams)
+ print(response.data)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "response consists of a list of one addresslist object"
+ def addressListList = response.data.geoCodeResponse.addresses
+ addressListList.size() == 1
+
+ and: "the addresslistobject contains a parent address and a list of static addresses"
+ def addressListObject = addressListList[0]
+ def parentAddress = addressListObject.parentAddress
+ def staticaddresses = addressListObject.addresses
+ staticaddresses.size() == 1
+
+ and: "the parent address object has certain attributes"
+ parentAddress.size() == 10
+ parentAddress.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+ parentAddress.type == "ADDRESS"
+
+ and: "a static address contains certain attributes"
+ def staticaddress = staticaddresses[0]
+ staticaddress.size() == 10
+ staticaddress.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+
+ and: "the type of a static address is ADDRESS"
+ staticaddress.type == "ADDRESS"
+
+ and: "a static address contains a address map with certain attributes"
+ def addressmap = staticaddress.address
+ addressmap.size() == 6
+ addressmap.keySet().containsAll("suburb", "static_id", "postcode", "country_code", "city", "hashkey")
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByPostalCodeAndCityAndCountrySpec.groovy b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByPostalCodeAndCityAndCountrySpec.groovy
new file mode 100644
index 00000000..4a1f3a7d
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/address/staticsearch/StaticAddressByPostalCodeAndCityAndCountrySpec.groovy
@@ -0,0 +1,47 @@
+package address.staticsearch
+
+import spock.lang.Specification
+import util.ClientFactory
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+class StaticAddressByPostalCodeAndCityAndCountrySpec extends Specification {
+
+ def "request for Static Addresses matching the given details"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "address details (postalCode, city, country)"
+ def urlParams = [
+ "postalCode": "76131",
+ "city": "Karlsruhe",
+ "country": "DE"
+ ]
+
+ when: "static addresses are requested"
+ def response = client.get(path: "/api/staticaddresses", query: urlParams)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "response consists of one address"
+ def staticAddresses = response.responseData
+ staticAddresses.size() == 1
+
+ and: "a static address contains certain attributes"
+ def staticaddress = staticAddresses[0]
+ staticaddress.keySet().size() == 10
+ staticaddress.keySet().containsAll("countryCode", "niceName", "displayName", "osmId", "placeId", "shortName", "address", "type", "longitude", "latitude")
+
+ and: "a static address contains a address map with certain attributes"
+ def addressmap = staticaddress.address
+ addressmap.size() == 6
+ addressmap.keySet().containsAll("suburb", "static_id", "postcode", "country_code", "city", "hashkey")
+
+ and: "the type of a static address is ADDRESS"
+ staticaddress.type == "ADDRESS"
+
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/authorization/AuthorizationSpec.groovy b/iris-api-tests/src/test/groovy/authorization/AuthorizationSpec.groovy
new file mode 100644
index 00000000..337c3aaf
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/authorization/AuthorizationSpec.groovy
@@ -0,0 +1,71 @@
+package authorization
+
+import groovyx.net.http.ContentType
+import groovyx.net.http.HttpResponseException
+import spock.lang.Specification
+import util.ClientFactory
+
+class AuthorizationSpec extends Specification {
+
+ def "request for api from non-authorized user"() {
+
+ given: "a REST client without login credentials"
+ def client = ClientFactory.newUnauthorizedClient()
+
+ when: "api is requested"
+ client.get(path: "/api/")
+
+ then: "the request fails"
+ def e = thrown(HttpResponseException)
+ def response = e.getResponse();
+
+ then: "response status code should be 401 (Unauthorized)"
+ response.status == 401
+
+ and: "response should have correct authentication header"
+ response.getFirstHeader("WWW-Authenticate").getValue() == "Basic realm=\"IRIS API\""
+ }
+
+ def "request for seaport synchronisation for non allowed user"() {
+
+ given: "a REST client with user login credentials"
+ def client = ClientFactory.newUserClient()
+
+ when: "seaport creation is requested"
+ def seaportUid = System.nanoTime()
+ def content = [
+ name: "seaport-${seaportUid}".toString(),
+ longitude: "8.${seaportUid}".toString(),
+ latitude: "49.${seaportUid}".toString()
+ ]
+ client.put(path: "/api/seaports/$seaportUid", body: content, contentType: ContentType.JSON)
+
+ then: "the request fails"
+ def e = thrown(HttpResponseException)
+
+ then: "response status code should be 403 (Forbidden)"
+ e.getResponse().status == 403
+ }
+
+ def "request for terminal synchronisation for non allowed user"() {
+
+ given: "a REST client with user login credentials"
+ def client = ClientFactory.newUserClient()
+
+ when: "terminal creation is requested"
+ def terminalUid = System.nanoTime()
+ def content = [
+ name: "terminal-${terminalUid}".toString(),
+ longitude: "8.${terminalUid}".toString(),
+ latitude: "49.${terminalUid}".toString(),
+ region: "NOT_SET"
+ ]
+ client.put(path: "/api/terminals/$terminalUid", body: content, contentType: ContentType.JSON)
+
+ then: "the request fails"
+ def e = thrown(HttpResponseException)
+
+ then: "response status code should be 403 (Forbidden)"
+ e.getResponse().status == 403
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/connection/ConnectionSpec.groovy b/iris-api-tests/src/test/groovy/connection/ConnectionSpec.groovy
new file mode 100644
index 00000000..8c315afa
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/ConnectionSpec.groovy
@@ -0,0 +1,25 @@
+package connection
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class ConnectionSpec extends Specification {
+
+ def "request for connections"() {
+
+ given: "a REST client"
+ def client = new ClientFactory().newAdminClient()
+
+ when: "connections for a specified terminal are requested"
+ def response = client.get(path: '/api/connections', query: [terminalUid: '1301000000000001'])
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "response body is as expected"
+ response.data[0].keySet() == ['routeType', 'terminalUid', 'seaportUid'] as Set
+ response.data[0].terminalUid == '1301000000000001'
+ response.data[0].seaportUid == '1301000000000002'
+ response.data[0].routeType == 'BARGE'
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/connection/ConnectionsForTerminalSpec.groovy b/iris-api-tests/src/test/groovy/connection/ConnectionsForTerminalSpec.groovy
new file mode 100644
index 00000000..d7641b53
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/ConnectionsForTerminalSpec.groovy
@@ -0,0 +1,36 @@
+package connection
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class ConnectionsForTerminalSpec extends Specification {
+
+ def "request for Connections belonging to specific Terminal"() {
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "a Terminal Unique ID"
+ def urlParams = ["terminalUid": 1301000000000001]
+
+ when: "connections are requested"
+ def response = client.get(path: "/api/connections", query: urlParams)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "response consists at least one connection"
+ response.responseData.size() >= 1
+
+ and: "a connection contains certain attributes"
+ def connection = response.responseData[0]
+ connection.keySet().size() == 3
+ connection.keySet().containsAll("seaportUid", "terminalUid", "routeType")
+ connection.terminalUid == "1301000000000001"
+
+ and: "the attributes have certain types"
+ connection.seaportUid.isNumber()
+ connection.terminalUid.isNumber()
+ !connection.routeType.isNumber()
+
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/connection/EnrichedRouteSpec.groovy b/iris-api-tests/src/test/groovy/connection/EnrichedRouteSpec.groovy
new file mode 100644
index 00000000..6340ac96
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/EnrichedRouteSpec.groovy
@@ -0,0 +1,64 @@
+package connection
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class EnrichedRouteSpec extends Specification {
+
+ def "request for enriched route"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "a route specification"
+ def routeParams = [
+ "data.parts[0].origin.longitude": 8.544177,
+ "data.parts[0].origin.latitude": 50.08162,
+ "data.parts[0].destination.longitude": 8.4232,
+ "data.parts[0].destination.latitude": 49.01179,
+ "data.parts[0].routeType": "TRUCK",
+ "data.parts[0].containerType": "FORTY",
+ "data.parts[0].containerState": "EMPTY",
+ "data.parts[1].origin.longitude": 8.4232,
+ "data.parts[1].origin.latitude": 49.01179,
+ "data.parts[1].destination.longitude": 8.544177,
+ "data.parts[1].destination.latitude": 50.08162,
+ "data.parts[1].routeType": "TRUCK",
+ "data.parts[1].containerType": "FORTY",
+ "data.parts[1].containerState": "FULL",
+ "data.parts[2].origin.longitude": 8.544177,
+ "data.parts[2].origin.latitude": 50.08162,
+ "data.parts[2].destination.longitude": 4.3,
+ "data.parts[2].destination.latitude": 51.36833,
+ "data.parts[2].routeType": "BARGE",
+ "data.parts[2].containerType": "FORTY",
+ "data.parts[2].containerState": "FULL",
+ ]
+
+ when: "details are requested"
+ def response = client.get(path: "/api/routedetails", query: routeParams)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "the route has certain attributes"
+ def route = response.data.response.route
+ route.keySet().size() == 9
+ route.keySet().containsAll("product", "errors", "responsibleTerminal", "direction", "name", "roundTrip", "data", "shortName", "url")
+
+ and: "the route's data has certain attribures"
+ route.data.keySet().size() == 8
+ route.data.keySet().containsAll("co2", "parts", "totalRealTollDistance", "totalDuration", "totalDistance",
+ "totalTollDistance", "co2DirectTruck", "totalOnewayTruckDistance")
+
+ and: "each route part has certain attributes"
+ route.data.parts.each {
+ assert it.keySet().size() == 8
+ assert it.keySet().containsAll("containerType", "routeType", "direction", "name", "containerState", "origin",
+ "data", "destination")
+ assert it.data.keySet().size() == 7
+ assert it.data.keySet().containsAll("electricDistance", "duration", "distance", "dieselDistance",
+ "tollDistance", "airlineDistance", "co2")
+ }
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/connection/SeaportRouteSpec.groovy b/iris-api-tests/src/test/groovy/connection/SeaportRouteSpec.groovy
new file mode 100644
index 00000000..537e338f
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/SeaportRouteSpec.groovy
@@ -0,0 +1,45 @@
+package connection
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class SeaportRouteSpec extends Specification {
+
+ def "request for seaport routes"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "routes for a specified seaport are requested"
+ def requestUrl = String.format('/api/connections/%s/%s:%s/%s', 1301000000000002, 49.01179, 8.4232, false)
+ def params = [containerType: "TWENTY_LIGHT", isImport: false, combo: "WATERWAY"]
+ def response = client.get(path: requestUrl, query: params)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "there should be at least one route"
+ def routes = response.data.response.routes
+ !routes.isEmpty()
+
+ routes.each {
+ and: "each route has certain fields"
+ def routeAttributes = it.keySet()
+ assert routeAttributes.size() == 9
+ assert routeAttributes.containsAll("roundTrip", "product", "shortName", "name", "data", "errors", "direction", "url", "responsibleTerminal")
+
+ and: "each route's data field has certain fields"
+ def data = it.data
+ def dataAttributes = data.keySet()
+ assert dataAttributes.size() == 8
+ assert dataAttributes.containsAll("co2", "co2DirectTruck", "totalDistance", "totalOnewayTruckDistance",
+ "totalRealTollDistance", "totalTollDistance", "totalDuration", "parts")
+
+ and: "the route parts have certain fields"
+ def partAttributes = data.parts.first().keySet()
+ assert partAttributes.size() == 8
+ assert partAttributes.containsAll("name", "data", "origin", "containerState", "containerType", "direction",
+ "routeType", "destination")
+ }
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/connection/SeaportsInConnectionsSpec.groovy b/iris-api-tests/src/test/groovy/connection/SeaportsInConnectionsSpec.groovy
new file mode 100644
index 00000000..bc4066c0
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/SeaportsInConnectionsSpec.groovy
@@ -0,0 +1,49 @@
+package connection
+
+import spock.lang.*
+import util.ClientFactory
+import org.apache.http.client.HttpResponseException
+
+/**
+ * @author Oliver Messner - messner@synyx.de
+ */
+class SeaportsInConnectionsSpec extends Specification {
+
+ def "request for connected seaport"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "request is sent to server"
+ def response = client.get(path: "/api/connections/seaports")
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "the seaport object has certain attributes"
+ def seaports = response.data.seaports
+ !seaports.isEmpty()
+ seaports.each {
+ assert it.keySet().size() == 6
+ assert it.keySet().containsAll("latitude", "longitude", "name", "enabled", "type", "uniqueId")
+ }
+
+ and: "links are provided"
+ def links = response.data.links
+ !links.isEmpty()
+ }
+
+ def "request for undefined combo type"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "an invalid combotype is requested"
+ client.get(path: "/api/connections/seaports", query: [combo: 'INVALID COMBO TYPE'])
+
+ then: "response status type is bad request"
+ def e = thrown(HttpResponseException)
+ e.message == 'Bad Request'
+ e.statusCode == 400
+ }
+}
\ No newline at end of file
diff --git a/iris-api-tests/src/test/groovy/connection/TerminalsConnectedToSeaportSpec.groovy b/iris-api-tests/src/test/groovy/connection/TerminalsConnectedToSeaportSpec.groovy
new file mode 100644
index 00000000..7b689dea
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/connection/TerminalsConnectedToSeaportSpec.groovy
@@ -0,0 +1,34 @@
+package connection
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class TerminalsConnectedToSeaportSpec extends Specification {
+
+ def "request for terminals connected to a seaport with route type BARGE"() {
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ and: "a seaport unique ID and a route type"
+ def urlParams = [
+ "seaportUid": 1301000000000002,
+ "routeType": "BARGE"
+ ]
+
+ when: "terminals for this combination are requested"
+ def response = client.get(path: "/api/terminals", query: urlParams)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "the response contains terminals"
+ def terminals = response.data.response.terminals
+ !terminals.isEmpty()
+
+ and: "the terminals contains certain attributes"
+ terminals.each {
+ assert it.keySet().size() == 7
+ assert it.keySet().containsAll("latitude", "longitude", "name", "enabled", "uniqueId", "type", "region")
+ }
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/countries/CountriesSpec.groovy b/iris-api-tests/src/test/groovy/countries/CountriesSpec.groovy
new file mode 100644
index 00000000..f4fe84f6
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/countries/CountriesSpec.groovy
@@ -0,0 +1,30 @@
+package countries
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class CountriesSpec extends Specification {
+
+ def "request for countries"() {
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "countries are requested"
+ def response = client.get(path: "/api/countries/")
+ def countries = response.data.countriesResponse.countries
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "countries amount should be 12"
+ countries.size() == 12
+
+ and: "Belgium should be in the list"
+ countries.contains([name: 'Belgium', value: 'BE']);
+
+ and: "Germany should be in the list"
+ countries.contains([name: 'Germany', value: 'DE']);
+ }
+
+}
diff --git a/iris-api-tests/src/test/groovy/distance/AirlineDistanceSpec.groovy b/iris-api-tests/src/test/groovy/distance/AirlineDistanceSpec.groovy
new file mode 100644
index 00000000..d1e16eb0
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/distance/AirlineDistanceSpec.groovy
@@ -0,0 +1,35 @@
+package distance
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class AirlineDistanceSpec extends Specification {
+
+ def "request for airline distance between karlsruhe and duisburg"() {
+
+ def latKA = 49.008085
+ def lonKA = 8.403756
+
+ def latDU = 51.435146
+ def lonDU = 6.762691
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "airline distance is requested"
+ def params = [alat:latKA, alon:lonKA, blat:latDU, blon:lonDU]
+ def response = client.get(path: "/api/airlineDistance", query: params)
+ def airlineDistance = response.data.airlineDistance
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ and: "airlineDistance should consist of unit and distance and links"
+ airlineDistance.keySet().size() == 3
+ airlineDistance.keySet().containsAll("unit", "distance", "links")
+
+ and: "unit is meter"
+ airlineDistance.unit == "meter"
+
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/distance/CloudDistanceSpec.groovy b/iris-api-tests/src/test/groovy/distance/CloudDistanceSpec.groovy
new file mode 100644
index 00000000..d2e82aa7
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/distance/CloudDistanceSpec.groovy
@@ -0,0 +1,38 @@
+package distance
+
+import spock.lang.Specification
+import util.ClientFactory
+
+class CloudDistanceSpec extends Specification {
+
+ def "request for cloud distance between karlsruhe and terminal one"() {
+
+ def idKarlsruhe = 1301000000000001
+ def idFrankfurt = 1301000000000001
+
+ given: "a REST client"
+ def client = ClientFactory.newAdminClient()
+
+ when: "distance is requested"
+ def params = [
+ terminal: idFrankfurt,
+ address: idKarlsruhe
+ ]
+ def response = client.get(path: "/api/distancecloudaddress", query: params)
+
+ then: "response status code should be 200 (OK)"
+ response.status == 200
+
+ def responseData = response.data
+
+ and: "responseData should consist of address and links"
+ responseData.keySet().size() == 2
+ responseData.keySet().containsAll("address", "links")
+
+ and: "address should consist of certain attributes"
+ def address = responseData.address
+ address.keySet().size() == 10
+ address.keySet().containsAll("country", "hashKey", "distance", "city", "postalcode", "airLineDistanceMeter",
+ "errorMessage", "suburb", "tollDistance", "uniqueId")
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/seaport/SeaportSpec.groovy b/iris-api-tests/src/test/groovy/seaport/SeaportSpec.groovy
new file mode 100644
index 00000000..8d8a79ef
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/seaport/SeaportSpec.groovy
@@ -0,0 +1,49 @@
+package seaport
+
+import groovyx.net.http.ContentType
+import spock.lang.Specification
+import util.ClientFactory
+
+class SeaportSpec extends Specification {
+
+ def 'create new seaport'() {
+
+ given: "a REST client"
+ def client = new ClientFactory().newAdminClient()
+
+ when: 'new seaport is created'
+ def seaportUid = System.nanoTime()
+ def content = [
+ name: "seaport-${seaportUid}".toString(),
+ longitude: "8.${seaportUid}".toString(),
+ latitude: "49.${seaportUid}".toString()
+ ]
+
+ def response = client.put(path: "/api/seaports/$seaportUid", body: content, contentType: ContentType.JSON)
+
+ then: 'response status code should be 201 (CREATED)'
+ response.status == 201
+ }
+
+ def 'create and update seaport'() {
+
+ given: "a REST client"
+ def client = new ClientFactory().newAdminClient()
+
+ and: 'a new seaport'
+ def seaportUid = System.nanoTime()
+ def content = [
+ name: "seaport-${seaportUid}".toString(),
+ longitude: "8.${seaportUid}".toString(),
+ latitude: "49.${seaportUid}".toString()
+ ]
+
+ client.put(path: "/api/seaports/$seaportUid", body: content, contentType: ContentType.JSON)
+
+ when: 'seaport is updated'
+ def response = client.put(path: "/api/seaports/$seaportUid", body: content, contentType: ContentType.JSON)
+
+ then: 'response status code should be 204 (NO CONTENT)'
+ response.status == 204
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/terminal/TerminalSpec.groovy b/iris-api-tests/src/test/groovy/terminal/TerminalSpec.groovy
new file mode 100644
index 00000000..6ec5c115
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/terminal/TerminalSpec.groovy
@@ -0,0 +1,51 @@
+package terminal
+
+import groovyx.net.http.ContentType
+import spock.lang.Specification
+import util.ClientFactory
+
+class TerminalSpec extends Specification {
+
+ def 'create new terminal'() {
+
+ given: "a REST client"
+ def client = new ClientFactory().newAdminClient()
+
+ when: 'new terminal is created'
+ def terminalUid = System.nanoTime()
+ def content = [
+ name: "terminal-${terminalUid}".toString(),
+ longitude: "8.${terminalUid}".toString(),
+ latitude: "49.${terminalUid}".toString(),
+ region: "NOT_SET"
+ ]
+
+ def response = client.put(path: "/api/terminals/$terminalUid", body: content, contentType: ContentType.JSON)
+
+ then: 'response status code should be 201 (CREATED)'
+ response.status == 201
+ }
+
+ def 'create and update terminal'() {
+
+ given: "a REST client"
+ def client = new ClientFactory().newAdminClient()
+
+ and: 'a new terminal'
+ def terminalUid = System.nanoTime()
+ def content = [
+ name: "terminal-${terminalUid}".toString(),
+ longitude: "8.${terminalUid}".toString(),
+ latitude: "49.${terminalUid}".toString(),
+ region: "NOT_SET"
+ ]
+
+ client.put(path: "/api/terminals/$terminalUid", body: content, contentType: ContentType.JSON)
+
+ when: 'terminal is updated'
+ def response = client.put(path: "/api/terminals/$terminalUid", body: content, contentType: ContentType.JSON)
+
+ then: 'response status code should be 204 (NO CONTENT)'
+ assert response.status == 204
+ }
+}
diff --git a/iris-api-tests/src/test/groovy/util/ClientFactory.groovy b/iris-api-tests/src/test/groovy/util/ClientFactory.groovy
new file mode 100644
index 00000000..a52a553e
--- /dev/null
+++ b/iris-api-tests/src/test/groovy/util/ClientFactory.groovy
@@ -0,0 +1,26 @@
+package util
+
+import groovyx.net.http.RESTClient
+
+class ClientFactory {
+
+ static final ENDPOINT = System.getProperty("endpoint") == null ? "http://localhost:8082" : System.getProperty("endpoint")
+
+ static def newAdminClient() {
+ def client = newUnauthorizedClient()
+
+ // prepare for preemptive authentication
+ client.headers['Authorization'] = 'Basic ' + "admin@example.com:admin".getBytes('UTF-8').encodeBase64()
+ return client
+ }
+
+ static def newUserClient() {
+ def client = newUnauthorizedClient()
+ client.headers['Authorization'] = 'Basic ' + "user@example.com:user".getBytes('UTF-8').encodeBase64()
+ return client
+ }
+
+ static def newUnauthorizedClient() {
+ return new RESTClient(ENDPOINT)
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..80e4605f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,816 @@
+
+ 4.0.0
+
+ net.contargo
+ iris
+ 1.6
+ war
+
+ ${project.groupId}:${project.artifactId}
+
+ Intermodal Routing Information System
+ https://github.com/Contargo/iris
+
+
+
+ GNU Affero General Public License, Version 3
+ http://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+ Contargo GmbH & Co. KG
+ http://www.contargo.net
+
+
+
+
+ IRIS Development
+ iris.development@contargo.net
+
+
+
+
+ 1.8
+ ${maven.build.timestamp}
+
+ UTF-8
+ UTF-8
+
+ 4.1.6.RELEASE
+ 3.2.7.RELEASE
+ 1.8.0.RELEASE
+
+ 1.7.6
+
+ 8.1.14.v20131031
+
+ 1.1.2
+
+ jacoco
+ target/jacoco.exec
+ target/surefire-reports
+ reuseReports
+ target/jasmine/total-coverage.dat
+
+ src/main/java
+
+
+
+ https://github.com/Contargo/iris
+ scm:git:https://github.com/Contargo/iris.git
+ scm:git:https://github.com/Contargo/iris.git
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ nexus.synyx.org
+ Synyx OpenSource Repository
+ http://repo.synyx.org
+
+
+ mapfish
+ mapfish geotools
+ http://dev.mapfish.org/maven/repository/org/geotools//
+
+
+ osgeo
+ Open Source Geospatial Foundation Repository
+ http://download.osgeo.org/webdav/geotools/
+
+
+ oss-jfrog-artifactory
+ oss-jfrog-artifactory-releases
+ http://oss.jfrog.org/artifactory/oss-release-local
+
+
+
+
+
+ org.springframework
+ spring-core
+ ${org.springframework-version}
+
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ org.springframework
+ spring-context
+ ${org.springframework-version}
+
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ org.springframework
+ spring-context-support
+ ${org.springframework-version}
+
+
+
+ org.springframework
+ spring-webmvc
+ ${org.springframework-version}
+
+
+
+ org.springframework
+ spring-web
+ ${org.springframework-version}
+
+
+
+ org.springframework
+ spring-orm
+ ${org.springframework-version}
+
+
+
+ org.springframework.security
+ spring-security-web
+ ${org.springframework.security-version}
+
+
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ org.springframework.security
+ spring-security-config
+ ${org.springframework.security-version}
+
+
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ org.springframework.security
+ spring-security-taglibs
+ ${org.springframework.security-version}
+
+
+
+
+ org.springframework.data
+ spring-data-jpa
+ ${org.springframework.data.jpa-version}
+
+
+
+ org.hibernate
+ hibernate-entitymanager
+ 4.3.9.Final
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ org.hibernate
+ hibernate-validator
+ 4.3.2.Final
+
+
+
+ mysql
+ mysql-connector-java
+
+ 5.1.29
+
+
+
+ org.liquibase
+ liquibase-core
+ 3.3.2
+
+
+
+ commons-dbcp
+ commons-dbcp
+ 1.4
+
+
+
+ commons-lang
+ commons-lang
+ 2.6
+
+
+
+ org.geotools
+ gt-api
+ 2.7.4
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.5.2
+
+
+
+ joda-time
+ joda-time
+ 2.3
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ javax.servlet.jsp
+ jsp-api
+ 2.2
+ provided
+
+
+ javax.servlet
+ jstl
+ 1.2
+
+
+
+
+ displaytag
+ displaytag
+ 1.2
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+ org.slf4j
+ jcl104-over-slf4j
+
+
+
+
+
+ opensymphony
+ sitemesh
+ 2.4.2
+
+
+
+ org.synyx
+ spring-helper-sitemesh
+ 0.1
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ net.contargo
+ big-decimal-validator
+ 1.2.1
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${org.slf4j-version}
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+ ${org.slf4j-version}
+
+
+
+ org.slf4j
+ jul-to-slf4j
+ ${org.slf4j-version}
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+
+
+ ch.qos.logback
+ logback-core
+ ${logback.version}
+
+
+ ch.qos.logback
+ logback-access
+ ${logback.version}
+
+
+ org.logback-extensions
+ logback-ext-spring
+ 0.1.2
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.3.2
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
+
+
+ org.hamcrest
+ hamcrest-all
+ 1.3
+ test
+
+
+
+ org.mockito
+ mockito-all
+ 1.9.5
+ test
+
+
+
+ org.springframework
+ spring-test
+ ${org.springframework-version}
+ test
+
+
+
+ org.unitils
+ unitils-core
+ 3.4.2
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+ com.jayway.jsonpath
+ json-path
+ 0.8.1
+ test
+
+
+
+ com.jayway.jsonpath
+ json-path-assert
+ 0.9.1
+ test
+
+
+
+ org.springframework.hateoas
+ spring-hateoas
+ 0.9.0.RELEASE
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ com.mangofactory
+ swagger-springmvc
+ 0.9.5
+
+
+
+ org.ajar
+ swagger-spring-mvc-ui
+ 0.4
+
+
+
+ net.sf.ehcache
+ ehcache
+ 2.9.0
+
+
+
+ com.google.guava
+ guava
+ 16.0.1
+
+
+
+ javax.interceptor
+ javax.interceptor-api
+ 1.2
+ provided
+
+
+
+
+ ${srcDir}
+
+
+ src/main/resources
+ true
+
+ **/*.xml
+ **/*.properties
+ **/*.json
+
+
+
+
+
+ ../iris/src/test/resources
+ true
+
+ **/*.xml
+ **/*.properties
+ **/*.json
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 2.5.1
+
+ true
+ false
+ release
+ deploy
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.7.4.201502262128
+
+
+ net.contargo.*
+
+ ${project.basedir}/target/jacoco.exec
+
+
+
+
+ prepare-agent
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.1
+
+
+ ${java-version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 2.4
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 1.3.1
+
+
+ enforce-stuff
+
+ enforce
+
+
+
+
+
+ commons-logging
+
+ true
+
+
+ [3.0.4,)
+
+
+ [1.6.0,)
+
+
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.16
+
+ true
+
+
+
+ unittests
+ test
+
+ test
+
+
+
+ **/*UnitTest.java
+
+ false
+
+
+
+ integrationtests
+ integration-test
+
+ test
+
+
+
+ **/*IntegrationTest.java
+ **/*WebTest.java
+
+ false
+
+
+
+
+
+
+ com.github.klieber
+ phantomjs-maven-plugin
+ 0.2
+
+
+
+ install
+
+
+
+
+ 1.9.2
+
+
+
+
+ com.github.searls
+ jasmine-maven-plugin
+ 1.3.1.1
+
+
+
+
+ test
+
+
+
+
+ src/main/webapp/client/js
+
+ true
+
+ org.openqa.selenium.phantomjs.PhantomJSDriver
+
+
+ ${phantomjs.binary}
+
+
+
+ jasmine-jquery-1.3.1.js
+ globalsetup.js
+ **Test.js
+ models/**Test.js
+ views/**Test.js
+
+
+ lib/jquery*.js
+ lib/select2.js
+ lib/underscore.js
+ lib/backbone.js
+ lib/bootstrap.min.js
+ lib/bootstrap-notify.js
+ lib/handlebars-1.3.0.js
+
+
+ routing/SelectableAwareCollection.js
+ routing/models/*.js
+ routing/views/*.js
+ routing/*.js
+
+
+
+
+
+
+
+ org.mortbay.jetty
+ jetty-maven-plugin
+ ${jetty.version}
+
+
+
+ 8082
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.10.1
+
+ -Xdoclint:none
+
+
+
+
+ IRIS
+
+
+
+
+
+ release
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.4
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.10.2
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+ jsSonar
+
+ src/main/webapp/client/js/routing
+ js
+ js
+
+
+
+
+
+ com.github.timurstrekalov
+ saga-maven-plugin
+ 1.4.0
+
+
+
+ coverage
+
+
+
+
+ http://localhost:${jasmine.serverPort}
+ ${project.build.directory}/saga-coverage
+
+ .*/spec/.*
+
+
+
+
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+ 1.5.2
+
+
+
+ replace
+
+
+
+
+ ${project.build.directory}/saga-coverage/total-coverage.dat
+ ${project.build.directory}/jasmine/total-coverage.dat
+ true
+
+
+ SF:http://localhost:\d+/src/routing/
+ SF:${project.build.sourceDirectory}/
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/net/contargo/iris/BoundingBox.java b/src/main/java/net/contargo/iris/BoundingBox.java
new file mode 100644
index 00000000..6c65e0a6
--- /dev/null
+++ b/src/main/java/net/contargo/iris/BoundingBox.java
@@ -0,0 +1,94 @@
+package net.contargo.iris;
+
+import java.math.BigDecimal;
+
+
+/**
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ */
+public class BoundingBox {
+
+ private static final Double EARTH_RADIUS = 6371.01d;
+ private static final Double MIN_LAT = Math.toRadians(-90d);
+ private static final Double MIN_LON = Math.toRadians(90d);
+ private static final Double MAX_LON = Math.toRadians(-180);
+ private static final Double MAX_LAT = Math.toRadians(180d);
+
+ private final GeoLocation lowerLeft;
+ private final GeoLocation upperRight;
+
+ /**
+ * Calculates the Bounding-Box with the given geolocation as center and a "radius" of given distance in kilometers.
+ *
+ *
+ *
+ * @param distanceInKm distance in km
+ *
+ * @return BoundingBox of the given distanceInKm
+ */
+ public BoundingBox getBoundingBox(Double distanceInKm) {
+
+ return new BoundingBox(this, distanceInKm);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/Message.java b/src/main/java/net/contargo/iris/Message.java
new file mode 100644
index 00000000..b66430cd
--- /dev/null
+++ b/src/main/java/net/contargo/iris/Message.java
@@ -0,0 +1,66 @@
+package net.contargo.iris;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+
+/**
+ * Bean representing a Message.
+ *
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public final class Message {
+
+ public static enum MessageType {
+
+ SUCCESS,
+ ERROR,
+ WARNING
+ }
+
+ private final String message;
+ private final MessageType type;
+
+ public Message(String message, MessageType type) {
+
+ this.message = message;
+ this.type = type;
+ }
+
+ public MessageType getType() {
+
+ return type;
+ }
+
+
+ public String getMessage() {
+
+ return message;
+ }
+
+
+ public static Message error(String message) {
+
+ return new Message(message, MessageType.ERROR);
+ }
+
+
+ public static Message success(String message) {
+
+ return new Message(message, MessageType.SUCCESS);
+ }
+
+
+ public static Message warning(String message) {
+
+ return new Message(message, MessageType.WARNING);
+ }
+
+
+ @Override
+ public String toString() {
+
+ return new ToStringBuilder(this).append("type", type).append("message", message).toString();
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/Address.java b/src/main/java/net/contargo/iris/address/Address.java
new file mode 100644
index 00000000..60bd281b
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/Address.java
@@ -0,0 +1,206 @@
+package net.contargo.iris.address;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import net.contargo.iris.GeoLocation;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Represents an address, which is delivered by an address resolution provider (nominatim).
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Tobias Schneider - schneider@synyx.de
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Address extends GeoLocation {
+
+ public static final String COUNTRY_CODE = "country_code";
+ private static final String DISPLAY_NAME = "display_name";
+ private static final String PLACE_ID = "place_id";
+ private static final String OSM_ID = "osm_id";
+ private static final String SHORT_NAME = "short_name";
+ private static final String SUBURB = "suburb";
+ private static final String CITY = "city";
+
+ @JsonProperty(DISPLAY_NAME)
+ private String displayName;
+
+ @JsonProperty(OSM_ID)
+ private long osmId;
+
+ @JsonProperty(PLACE_ID)
+ private long placeId;
+
+ @JsonProperty(SHORT_NAME)
+ private String shortName;
+
+ private Map address = new HashMap<>();
+
+ public Address(String displayName) {
+
+ super();
+ this.displayName = displayName;
+ }
+
+
+ public Address() {
+
+ // needed in test.
+ }
+
+
+ public Address(BigDecimal latitude, BigDecimal longitude) {
+
+ super(latitude, longitude);
+ }
+
+ public String getDisplayName() {
+
+ return displayName;
+ }
+
+
+ public void setDisplayName(String displayName) {
+
+ this.displayName = displayName;
+ }
+
+
+ public String getShortName() {
+
+ return shortName;
+ }
+
+
+ public void setShortName(String shortName) {
+
+ this.shortName = shortName;
+ }
+
+
+ public Map getAddress() {
+
+ return address;
+ }
+
+
+ public void setAddress(Map address) {
+
+ this.address = address;
+ }
+
+
+ public long getOsmId() {
+
+ return osmId;
+ }
+
+
+ public void setOsmId(long osmId) {
+
+ this.osmId = osmId;
+ }
+
+
+ public long getPlaceId() {
+
+ return placeId;
+ }
+
+
+ public void setPlaceId(long placeId) {
+
+ this.placeId = placeId;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final Address oth = (Address) o;
+
+ return new EqualsBuilder().append(displayName, oth.displayName).append(osmId, oth.osmId).append(placeId,
+ oth.placeId).append(shortName, oth.shortName).appendSuper(super.equals(o)).isEquals();
+ }
+
+
+ @Override
+ public int hashCode() {
+
+ return new HashCodeBuilder().append(displayName).append(osmId).append(placeId).append(shortName).appendSuper(
+ super.hashCode()).toHashCode();
+ }
+
+
+ public String getCountryCode() {
+
+ return this.address.get(COUNTRY_CODE);
+ }
+
+
+ @Override
+ public String getNiceName() {
+
+ String addressText = getAddressText();
+
+ if (StringUtils.hasLength(addressText)) {
+ return addressText;
+ }
+
+ String name = getShortName();
+
+ if (StringUtils.hasLength(name)) {
+ return name;
+ }
+
+ return getDisplayName();
+ }
+
+
+ private String getAddressText() {
+
+ String postalCode = address.get("postcode");
+
+ if (postalCode == null) {
+ postalCode = address.get("boundary");
+ }
+
+ if (address.containsKey(CITY) && postalCode != null) {
+ if (StringUtils.hasText(address.get(SUBURB))) {
+ return String.format("%s %s (%s)", postalCode, address.get(CITY), address.get(SUBURB));
+ } else {
+ return String.format("%s %s", postalCode, address.get(CITY));
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public String toString() {
+
+ return "Address [displayName=" + displayName + ", osmId=" + osmId
+ + ", placeId=" + placeId + ", shortName=" + shortName
+ + ", address=" + address + "]";
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/AddressList.java b/src/main/java/net/contargo/iris/address/AddressList.java
new file mode 100644
index 00000000..feb845b8
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/AddressList.java
@@ -0,0 +1,78 @@
+package net.contargo.iris.address;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+import java.util.List;
+
+
+/**
+ * Represents a list of addresses. Contains a list of {@link Address}es and a parent Address.
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class AddressList {
+
+ private final List addresses;
+
+ private Address parentAddress;
+
+ public AddressList(Address root, List addresses) {
+
+ this.parentAddress = root;
+ this.addresses = addresses;
+ }
+
+
+ public AddressList(String name, List addresses) {
+
+ Address address = new Address();
+ address.setDisplayName(name);
+
+ this.parentAddress = address;
+ this.addresses = addresses;
+ }
+
+ public List getAddresses() {
+
+ return addresses;
+ }
+
+
+ public Address getParentAddress() {
+
+ return parentAddress;
+ }
+
+
+ public void setParentAddress(Address parentAddress) {
+
+ this.parentAddress = parentAddress;
+ }
+
+
+ @Override
+ public int hashCode() {
+
+ return new HashCodeBuilder().append(addresses).append(parentAddress).toHashCode();
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+
+ if (!(obj instanceof AddressList)) {
+ return false;
+ }
+
+ if (this == obj) {
+ return true;
+ }
+
+ final AddressList other = (AddressList) obj;
+
+ return new EqualsBuilder().append(this.hashCode(), other.hashCode()).append(this.addresses, other.addresses)
+ .append(this.parentAddress, other.parentAddress).isEquals();
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/api/AddressApiController.java b/src/main/java/net/contargo/iris/address/api/AddressApiController.java
new file mode 100644
index 00000000..0e2aecf8
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/api/AddressApiController.java
@@ -0,0 +1,202 @@
+package net.contargo.iris.address.api;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+import com.wordnik.swagger.annotations.ApiParam;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.dto.AddressDto;
+import net.contargo.iris.address.dto.AddressDtoService;
+import net.contargo.iris.api.AbstractController;
+
+import org.slf4j.Logger;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.stereotype.Controller;
+
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+
+import java.math.BigDecimal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.*;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+
+
+/**
+ * Controller for {@link net.contargo.iris.address.dto.AddressDto}s.
+ *
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ * @author Tobias Schneider - schneider@synyx.de
+ */
+@Controller
+@Api(description = "API for querying addresses.", value = "")
+public class AddressApiController extends AbstractController {
+
+ public static final String METHOD_ADDRESS_BY_GEOLOCATION = "addressByGeolocation";
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+
+ private final AddressDtoService addressDtoService;
+
+ @Autowired
+ public AddressApiController(AddressDtoService addressDtoService) {
+
+ this.addressDtoService = addressDtoService;
+ }
+
+ @ApiOperation(
+ value = "Get addresses for a given OpenStreetMap-ID.", notes = "Get addresses for a given OpenStreetMap-ID."
+ )
+ @ModelAttribute("geoCodeResponse")
+ @RequestMapping(value = OSM_ADDRESSES + SLASH + ID_PARAM, method = RequestMethod.GET)
+ public ListOfAddressListsResponse addressByOsmId(
+ @ApiParam(value = "ID identifying a single osm-address.", required = true)
+ @PathVariable(ID)
+ long osmId) {
+
+ AddressDto address = addressDtoService.getAddressByOsmId(osmId);
+
+ ListOfAddressListsResponse response = new ListOfAddressListsResponse(addressDtoService.wrapInListOfAddressLists(
+ address));
+
+ LOG.info("API: Responding to geocode-request for OSM ID {} with {} Blocks", osmId,
+ response.getAddresses().size());
+
+ return response;
+ }
+
+
+ // do not remove the last slash of the url, or the longitude decimals will be cut off
+ @ApiOperation(value = "Get address for the given geolocation.", notes = "Get address for the given geolocation.")
+ @ModelAttribute("reverseGeocodeResponse")
+ @RequestMapping(
+ value = REVERSE_GEOCODE + SLASH + PARAM_LATITUDE + COLON + PARAM_LONGITUDE + SLASH, method = RequestMethod.GET
+ )
+ public AddressResponse addressByGeolocation(@PathVariable("lat") BigDecimal latitude,
+ @PathVariable("lon") BigDecimal longitude) throws NoSuchMethodException {
+
+ AddressDto address = addressDtoService.getAddressForGeoLocation(new GeoLocation(latitude, longitude));
+ AddressResponse response = new AddressResponse(address);
+
+ Method method = AddressApiController.class.getMethod(METHOD_ADDRESS_BY_GEOLOCATION, BigDecimal.class,
+ BigDecimal.class);
+
+ response.add(linkTo(method, latitude, longitude).slash(".").withSelfRel());
+
+ LOG.info("API: Responding to request for address by geolocation with latitude {} and longitude {}", latitude,
+ longitude);
+
+ return response;
+ }
+
+
+ @ApiOperation(
+ value = "Search list of addresses by address details.", notes = "Search list of addresses by address details."
+ )
+ @ModelAttribute("geoCodeResponse")
+ @RequestMapping(value = GEOCODES, method = RequestMethod.GET)
+ public ListOfAddressListsResponse addressesByAddressDetails(@RequestParam(required = false) String street,
+ @RequestParam(required = false) String postalCode,
+ @RequestParam(required = false) String city,
+ @RequestParam(required = false) String country,
+ @RequestParam(required = false) String name, HttpServletRequest request) {
+
+ Map addressDetails = putRequestParamsToMap(street, postalCode, city, country, name);
+
+ ListOfAddressListsResponse response = new ListOfAddressListsResponse(addressDtoService.getAddressesByDetails(
+ addressDetails));
+
+ response.add(linkTo(getClass()).slash(GEOCODES + "?" + request.getQueryString()).withSelfRel());
+
+ LOG.info("API: Responding to request for address by address details: {}", addressDetails.toString());
+
+ return response;
+ }
+
+
+ @ApiOperation(value = "Search addresses by address details.", notes = "Search addresses by address details.")
+ @ModelAttribute("simpleGeoCodeResponse")
+ @RequestMapping(value = SIMPLE_GEOCODES, method = RequestMethod.GET)
+ public AddressListResponse addressesByAddressDetailsPlain(@RequestParam(required = false) String street,
+ @RequestParam(required = false) String postalCode,
+ @RequestParam(required = false) String city,
+ @RequestParam(required = false) String country,
+ @RequestParam(required = false) String name, HttpServletRequest request) {
+
+ Map addressDetails = putRequestParamsToMap(street, postalCode, city, country, name);
+
+ List addressList = addressDtoService.getAddressesByDetailsPlain(addressDetails);
+
+ AddressListResponse response = new AddressListResponse(addressList);
+
+ response.add(linkTo(getClass()).slash(SIMPLE_GEOCODES + "?" + request.getQueryString()).withSelfRel());
+
+ LOG.info("API: Responding with " + addressList.size() + " addresses to request " + addressDetails.toString());
+
+ return response;
+ }
+
+
+ @ApiOperation(
+ value = "Get all addresses belonging to the given OpenStreetMap-Place-ID.",
+ notes = "Get all addresses belonging to the given OpenStreetMap-Place-ID."
+ )
+ @ModelAttribute("addresses")
+ @RequestMapping(value = PLACES + SLASH + ID_PARAM + SLASH + ADDRESSES, method = RequestMethod.GET)
+ public List addressesWherePlaceIsIn(
+ @ApiParam(value = "ID identifying a osm-place-id.", required = true)
+ @PathVariable(ID)
+ long placeId) {
+
+ LOG.info("API: Responding to request for addresses by place id {}", placeId);
+
+ return addressDtoService.getAddressesWherePlaceIsIn(placeId);
+ }
+
+
+ private Map putRequestParamsToMap(String street, String postalCode, String city, String country,
+ String name) {
+
+ Map addressDetails = new HashMap<>();
+
+ if (city != null) {
+ addressDetails.put(CITY.getKey(), city);
+ }
+
+ if (country != null) {
+ addressDetails.put(COUNTRY.getKey(), country);
+ }
+
+ if (postalCode != null) {
+ addressDetails.put(POSTAL_CODE.getKey(), postalCode);
+ }
+
+ if (street != null) {
+ addressDetails.put(STREET.getKey(), street);
+ }
+
+ if (name != null) {
+ addressDetails.put(NAME.getKey(), name);
+ }
+
+ return addressDetails;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/api/AddressListResponse.java b/src/main/java/net/contargo/iris/address/api/AddressListResponse.java
new file mode 100644
index 00000000..7b9a0127
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/api/AddressListResponse.java
@@ -0,0 +1,33 @@
+package net.contargo.iris.address.api;
+
+import net.contargo.iris.address.dto.AddressDto;
+
+import org.springframework.hateoas.ResourceSupport;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+class AddressListResponse extends ResourceSupport {
+
+ private final List addresses;
+
+ public AddressListResponse(List addresses) {
+
+ this.addresses = addresses;
+ }
+
+
+ public AddressListResponse() {
+
+ addresses = new ArrayList<>();
+ }
+
+ public List getAddresses() {
+
+ return addresses;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/api/AddressResponse.java b/src/main/java/net/contargo/iris/address/api/AddressResponse.java
new file mode 100644
index 00000000..6d4aa1d9
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/api/AddressResponse.java
@@ -0,0 +1,30 @@
+package net.contargo.iris.address.api;
+
+import net.contargo.iris.address.dto.AddressDto;
+
+import org.springframework.hateoas.ResourceSupport;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+class AddressResponse extends ResourceSupport {
+
+ private AddressDto address;
+
+ public AddressResponse(AddressDto address) {
+
+ this.address = address;
+ }
+
+
+ public AddressResponse() {
+
+ // Needed for Jackson Mapping
+ }
+
+ public AddressDto getAddress() {
+
+ return address;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/api/ListOfAddressListsResponse.java b/src/main/java/net/contargo/iris/address/api/ListOfAddressListsResponse.java
new file mode 100644
index 00000000..43e0bbf8
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/api/ListOfAddressListsResponse.java
@@ -0,0 +1,34 @@
+package net.contargo.iris.address.api;
+
+import net.contargo.iris.address.dto.AddressListDto;
+
+import org.springframework.hateoas.ResourceSupport;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class ListOfAddressListsResponse extends ResourceSupport {
+
+ private final List addresses;
+
+ public ListOfAddressListsResponse(List addresses) {
+
+ this.addresses = addresses;
+ }
+
+
+ public ListOfAddressListsResponse() {
+
+ addresses = new ArrayList<>();
+ }
+
+ public List getAddresses() {
+
+ return addresses;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/dto/AddressDto.java b/src/main/java/net/contargo/iris/address/dto/AddressDto.java
new file mode 100644
index 00000000..1d72c4e3
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/dto/AddressDto.java
@@ -0,0 +1,105 @@
+package net.contargo.iris.address.dto;
+
+import net.contargo.iris.address.Address;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+import java.util.Map;
+
+import static java.util.Collections.unmodifiableMap;
+
+
+/**
+ * Data Transfer Object for {@link Address}.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ */
+public final class AddressDto extends GeoLocationDto {
+
+ private static final String TYPE = "ADDRESS";
+
+ private final String displayName;
+ private final String shortName;
+ private final String niceName;
+ private final String countryCode;
+ private final long osmId;
+ private final long placeId;
+ private final Map address;
+ private final String type;
+
+ public AddressDto(Address address) {
+
+ super(address);
+ this.countryCode = address.getCountryCode();
+ this.niceName = address.getNiceName();
+ this.displayName = address.getDisplayName();
+ this.osmId = address.getOsmId();
+ this.placeId = address.getPlaceId();
+ this.shortName = address.getShortName();
+ this.address = unmodifiableMap(address.getAddress());
+ this.type = TYPE;
+ }
+
+ public String getShortName() {
+
+ return shortName;
+ }
+
+
+ public String getNiceName() {
+
+ return niceName;
+ }
+
+
+ public Map getAddress() {
+
+ return address;
+ }
+
+
+ public String getDisplayName() {
+
+ return displayName;
+ }
+
+
+ public long getOsmId() {
+
+ return osmId;
+ }
+
+
+ public long getPlaceId() {
+
+ return placeId;
+ }
+
+
+ public String getCountryCode() {
+
+ return countryCode;
+ }
+
+
+ @Override
+ public String getType() {
+
+ return type;
+ }
+
+
+ @Override
+ public boolean equals(Object obj) { // NOSONAR We don't want to change the behaviour of base class equals
+
+ return super.equals(obj); // NOSONAR We don't want to change the behaviour of base class equals
+ }
+
+
+ @Override
+ public int hashCode() {
+
+ return new HashCodeBuilder().append(super.hashCode()).toHashCode();
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java b/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java
new file mode 100644
index 00000000..62d2e546
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/dto/AddressDtoService.java
@@ -0,0 +1,77 @@
+package net.contargo.iris.address.dto;
+
+import net.contargo.iris.GeoLocation;
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Delegates to AddressService and converts Address Beans into Address DTOs.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ */
+public interface AddressDtoService {
+
+ /**
+ * @param osmId
+ *
+ * @return The address for a certain osmId
+ */
+ AddressDto getAddressByOsmId(long osmId);
+
+
+ /**
+ * Wraps the given AddressDto as first element of an {@link AddressListDto} This is needed to provide a consistent
+ * format of returned Addresses for API-Clients.
+ *
+ * @param addressDto
+ *
+ * @return
+ */
+ List wrapInListOfAddressLists(AddressDto addressDto);
+
+
+ /**
+ * Returns the corresponding {@link AddressDto} object from a given {@link GeoLocation}.
+ *
+ * @param location keeps the basis information for the {@link AddressDto} search.
+ *
+ * @return The address for the given {@link GeoLocation}.
+ */
+ AddressDto getAddressForGeoLocation(GeoLocation location);
+
+
+ /**
+ * Resolves an address (described by the given parameters) to a {@link java.util.List} of
+ * {@link net.contargo.iris.address.Address} objects with the attributes name, latitude and longitude. Uses multiple
+ * fallback strategies to find addresses if not all parameters are provided
+ *
+ * @param addressDetails The parameters describing the addresses we are looking for
+ *
+ * @return A List of Address Lists
+ */
+ List getAddressesByDetails(Map addressDetails);
+
+
+ /**
+ * Resolves an address (described by the given parameters) to a {@link java.util.List} of
+ * {@link net.contargo.iris.address.Address} objects with the attributes name, latitude and longitude. Uses multiple
+ * fallback strategies to find addresses if not all parameters are provided
+ *
+ * @param addressDetails The parameters describing the addresses we are looking for
+ *
+ * @return A List of Addresses
+ */
+ List getAddressesByDetailsPlain(Map addressDetails);
+
+
+ /**
+ * Returns all Addresses where the given place is in.
+ *
+ * @param placeId the OSM Place ID
+ *
+ * @return All addresses belonging to the OSM-Place defined by the OSM Place ID
+ */
+ List getAddressesWherePlaceIsIn(Long placeId);
+}
diff --git a/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java b/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java
new file mode 100644
index 00000000..aa21422e
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/dto/AddressDtoServiceImpl.java
@@ -0,0 +1,102 @@
+package net.contargo.iris.address.dto;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+import net.contargo.iris.address.AddressList;
+import net.contargo.iris.address.nominatim.service.AddressService;
+import net.contargo.iris.address.service.AddressServiceWrapper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class AddressDtoServiceImpl implements AddressDtoService {
+
+ private final AddressService addressService;
+ private final AddressServiceWrapper addressServiceWrapper;
+
+ public AddressDtoServiceImpl(AddressService addressService, AddressServiceWrapper addressServiceWrapper) {
+
+ this.addressService = addressService;
+ this.addressServiceWrapper = addressServiceWrapper;
+ }
+
+ @Override
+ public AddressDto getAddressByOsmId(long osmId) {
+
+ Address address = addressService.getAddressByOsmId(osmId);
+
+ return address == null ? null : new AddressDto(address);
+ }
+
+
+ @Override
+ public List wrapInListOfAddressLists(AddressDto addressDto) {
+
+ List listOfLists;
+
+ AddressListDto osmList = new AddressListDto("Result", Arrays.asList(addressDto));
+
+ listOfLists = Arrays.asList(osmList);
+
+ return listOfLists;
+ }
+
+
+ @Override
+ public AddressDto getAddressForGeoLocation(GeoLocation location) {
+
+ Address address = addressServiceWrapper.getAddressForGeoLocation(location);
+
+ return address == null ? null : new AddressDto(address);
+ }
+
+
+ @Override
+ public List getAddressesByDetails(Map addressDetails) {
+
+ List addressListList = addressServiceWrapper.getAddressesByDetails(addressDetails);
+
+ List addressListDtoList = new ArrayList<>();
+
+ for (AddressList addressList : addressListList) {
+ addressListDtoList.add(new AddressListDto(addressList));
+ }
+
+ return addressListDtoList;
+ }
+
+
+ @Override
+ public List getAddressesByDetailsPlain(Map addressDetails) {
+
+ List addressListList = getAddressesByDetails(addressDetails);
+ List addressDtoList = new ArrayList<>();
+
+ for (AddressListDto addressListDto : addressListList) {
+ addressDtoList.addAll(addressListDto.getAddresses());
+ }
+
+ return addressDtoList;
+ }
+
+
+ @Override
+ public List getAddressesWherePlaceIsIn(Long placeId) {
+
+ List addressList = addressService.getAdressesWherePlaceIsIn(placeId);
+
+ List addressDtoList = new ArrayList<>();
+
+ for (Address address : addressList) {
+ addressDtoList.add(new AddressDto(address)); // NOSONAR Instantiating object is necessary
+ }
+
+ return addressDtoList;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/dto/AddressListDto.java b/src/main/java/net/contargo/iris/address/dto/AddressListDto.java
new file mode 100644
index 00000000..00b27446
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/dto/AddressListDto.java
@@ -0,0 +1,56 @@
+package net.contargo.iris.address.dto;
+
+import net.contargo.iris.address.Address;
+import net.contargo.iris.address.AddressList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Represents a list of addresses to the API as List of {@link AddressDto} with parent Address.
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class AddressListDto {
+
+ private final List addresses;
+ private AddressDto parentAddress;
+
+ public AddressListDto(String name, List addresses) {
+
+ Address address = new Address(name);
+ this.parentAddress = new AddressDto(address);
+ this.addresses = addresses;
+ }
+
+
+ public AddressListDto() {
+
+ addresses = new ArrayList<>();
+ }
+
+
+ public AddressListDto(AddressList addressList) {
+
+ this.parentAddress = new AddressDto(addressList.getParentAddress());
+
+ this.addresses = new ArrayList<>();
+
+ for (Address adr : addressList.getAddresses()) {
+ this.addresses.add(new AddressDto(adr));
+ }
+ }
+
+ public AddressDto getParentAddress() {
+
+ return parentAddress;
+ }
+
+
+ public List getAddresses() {
+
+ return addresses;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/dto/GeoLocationDto.java b/src/main/java/net/contargo/iris/address/dto/GeoLocationDto.java
new file mode 100644
index 00000000..29e9813b
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/dto/GeoLocationDto.java
@@ -0,0 +1,80 @@
+package net.contargo.iris.address.dto;
+
+import net.contargo.iris.GeoLocation;
+
+import org.hibernate.validator.constraints.Range;
+
+import java.math.BigDecimal;
+
+import javax.validation.constraints.NotNull;
+
+
+/**
+ * Data Transfer Object for {@link GeoLocation}.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class GeoLocationDto {
+
+ private static final int MAX_VALUE_COORD = 180;
+ private static final int MIN_VALUE_COORD = -180;
+ private static final String TYPE = "GEOLOCATION";
+
+ @NotNull
+ @Range(min = -MAX_VALUE_COORD, max = MAX_VALUE_COORD)
+ private BigDecimal latitude;
+
+ @NotNull
+ @Range(min = MIN_VALUE_COORD, max = MAX_VALUE_COORD)
+ private BigDecimal longitude;
+
+ public GeoLocationDto(GeoLocation geoLocation) {
+
+ if (geoLocation != null) {
+ this.latitude = geoLocation.getLatitude();
+ this.longitude = geoLocation.getLongitude();
+ }
+ }
+
+
+ public GeoLocationDto() {
+
+ // Needed for Jackson Mapping
+ }
+
+ public GeoLocation toEntity() {
+
+ return new GeoLocation(this.latitude, this.longitude);
+ }
+
+
+ // Setters are needed so this DTO can be used as @ModelAttribute in Spring MVC
+ public void setLatitude(BigDecimal latitude) {
+
+ this.latitude = latitude;
+ }
+
+
+ public void setLongitude(BigDecimal longitude) {
+
+ this.longitude = longitude;
+ }
+
+
+ public BigDecimal getLatitude() {
+
+ return latitude;
+ }
+
+
+ public BigDecimal getLongitude() {
+
+ return longitude;
+ }
+
+
+ public String getType() {
+
+ return TYPE;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressDetailKey.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressDetailKey.java
new file mode 100644
index 00000000..24fff3ea
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressDetailKey.java
@@ -0,0 +1,25 @@
+package net.contargo.iris.address.nominatim.service;
+
+/**
+ * @author Arnold Franke - franke@synyx.de *
+ */
+public enum AddressDetailKey {
+
+ STREET("street"),
+ POSTAL_CODE("postalcode"),
+ CITY("city"),
+ COUNTRY("country"),
+ NAME("name");
+
+ private String key;
+
+ private AddressDetailKey(String key) {
+
+ this.key = key;
+ }
+
+ public String getKey() {
+
+ return key;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressResolutionException.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressResolutionException.java
new file mode 100644
index 00000000..4b75c979
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressResolutionException.java
@@ -0,0 +1,12 @@
+package net.contargo.iris.address.nominatim.service;
+
+/**
+ * @author Sandra Thieme - thieme@synyx.de
+ */
+public class AddressResolutionException extends RuntimeException {
+
+ public AddressResolutionException(String msg, Throwable t) {
+
+ super(msg, t);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressService.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressService.java
new file mode 100644
index 00000000..7f829352
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressService.java
@@ -0,0 +1,54 @@
+
+package net.contargo.iris.address.nominatim.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * This service is used for address resolution purposes.
+ *
+ * @author Sven Mueller - mueller@synyx.de
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public interface AddressService {
+
+ /**
+ * Resolves an address (described by the given parameters) to a {@link java.util.List} of
+ * {@link net.contargo.iris.address.Address} objects with the attributes name, latitude and longitude. Uses multiple
+ * fallback strategies to find addresses if not all parameters are provided
+ *
+ * @param addressDetails @return
+ */
+ List getAddressesByDetails(Map addressDetails);
+
+
+ /**
+ * Returns all Addresses where the given place is in.
+ *
+ * @param placeId the OSM Place ID
+ *
+ * @return All addresses belonging to the OSM-Place defined by the OSM Place ID
+ */
+ List getAdressesWherePlaceIsIn(Long placeId);
+
+
+ /**
+ * @param osmId
+ *
+ * @return The address for a certain osmId
+ */
+ Address getAddressByOsmId(long osmId);
+
+
+ /**
+ * @param location
+ *
+ * @return The address for the given geolocation.
+ */
+ Address getAddressByGeolocation(GeoLocation location);
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressServiceHelper.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressServiceHelper.java
new file mode 100644
index 00000000..967e3d42
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressServiceHelper.java
@@ -0,0 +1,26 @@
+package net.contargo.iris.address.nominatim.service;
+
+import net.contargo.iris.address.Address;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * Supporting class for AddressService.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ */
+class AddressServiceHelper {
+
+ List mergeSearchResultsWithoutDuplications(List one, List two) {
+
+ Set resultSet = new HashSet<>();
+ resultSet.addAll(one);
+ resultSet.addAll(two);
+
+ return new ArrayList<>(resultSet);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressSorter.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressSorter.java
new file mode 100644
index 00000000..1c3620e3
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressSorter.java
@@ -0,0 +1,34 @@
+package net.contargo.iris.address.nominatim.service;
+
+import net.contargo.iris.address.Address;
+import net.contargo.iris.gis.service.GisService;
+
+import java.math.BigDecimal;
+
+import java.util.Comparator;
+
+
+/**
+ * Comparator to sort Addresses by distance from the Center of EU to sort the ones outside of the EU to the end.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ */
+class AddressSorter implements Comparator {
+
+ private final GisService gisService;
+
+ public AddressSorter(GisService gisService) {
+
+ this.gisService = gisService;
+ }
+
+ @Override
+ public int compare(Address o1, Address o2) {
+
+ BigDecimal distanceOfO1 = gisService.calcAirLineDistInMeters(o1, GisService.CENTER_OF_THE_EUROPEAN_UNION);
+ BigDecimal distanceOfO2 = gisService.calcAirLineDistInMeters(o2, GisService.CENTER_OF_THE_EUROPEAN_UNION);
+
+ return distanceOfO1.compareTo(distanceOfO2);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/AddressValidator.java b/src/main/java/net/contargo/iris/address/nominatim/service/AddressValidator.java
new file mode 100644
index 00000000..4e068214
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/AddressValidator.java
@@ -0,0 +1,25 @@
+package net.contargo.iris.address.nominatim.service;
+
+import org.slf4j.Logger;
+
+import java.lang.invoke.MethodHandles;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+
+class AddressValidator {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+ private static final int MIN_STREET_LENGTH = 3;
+
+ String validateStreet(String street) {
+
+ if (street != null && street.length() < MIN_STREET_LENGTH) {
+ LOG.info("street='" + street + "' seems to be inaccurate, so it will be ignored during geocoding process.");
+
+ return "";
+ }
+
+ return street;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/NominatimAddressService.java b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimAddressService.java
new file mode 100644
index 00000000..55a13952
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimAddressService.java
@@ -0,0 +1,196 @@
+package net.contargo.iris.address.nominatim.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+import net.contargo.iris.util.HttpUtilException;
+
+import org.slf4j.Logger;
+
+import org.springframework.util.StringUtils;
+
+import java.lang.invoke.MethodHandles;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.CITY;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.COUNTRY;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.NAME;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.POSTAL_CODE;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.STREET;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+
+/**
+ * An implementation of the {@link AddressService} interface using Nominatim to resolveWithNominatim an address to
+ * geocoordinates and jackson to generate Java objects from Json. Nominatim: http://nominatim.openstreetmap.org
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class NominatimAddressService implements AddressService {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final int ARGUMENT_4 = 4;
+ private static final int ARGUMENT_3 = 3;
+ private static final int ARGUMENT_2 = 2;
+ private static final int ARGUMENT_1 = 1;
+ private static final int ARGUMENT_0 = 0;
+
+ private final NominatimUrlBuilder nominatimUrlBuilder;
+ private final NominatimJsonResponseParser nominatimResponder;
+
+ private final AddressSorter addressSorter;
+ private final AddressServiceHelper addressHelper;
+ private final AddressValidator addressValidator;
+
+ public NominatimAddressService(NominatimUrlBuilder nominatimUrlBuilder,
+ NominatimJsonResponseParser nominatimResponder, AddressSorter addressSorter, AddressServiceHelper addressHelper,
+ AddressValidator addressValidator) {
+
+ this.nominatimUrlBuilder = nominatimUrlBuilder;
+ this.nominatimResponder = nominatimResponder;
+ this.addressSorter = addressSorter;
+ this.addressHelper = addressHelper;
+ this.addressValidator = addressValidator;
+ }
+
+ List resolveWithNominatim(String street, String postalCode, String city, String country, String name) {
+
+ /*
+ * Please notice that Nominatim has no search results if query contains name and street.
+ * If name should be a search parameter street and streetNumber have to be ignored.
+ */
+
+ if (StringUtils.hasText(name) && StringUtils.hasText(street)) {
+ return geocodeByName(street, postalCode, city, country, name);
+ } else {
+ String url = nominatimUrlBuilder.buildUrl(street, postalCode, city, country, name);
+
+ List addresses = nominatimResponder.getAddressesForUrl(url);
+
+ Collections.sort(addresses, addressSorter);
+
+ return addresses;
+ }
+ }
+
+
+ @Override
+ public List getAdressesWherePlaceIsIn(Long placeId) {
+
+ return searchSuburbsViaNominatimsDetailPage(placeId, SuburbType.ADDRESSES, new HashSet());
+ }
+
+
+ @Override
+ public Address getAddressByOsmId(long osmId) {
+
+ String suburbUrl = nominatimUrlBuilder.buildOsmUrl(osmId);
+ List foundAddresses = nominatimResponder.getAddressesForUrlForOsmId(suburbUrl);
+
+ return foundAddresses.get(0);
+ }
+
+
+ @Override
+ public Address getAddressByGeolocation(GeoLocation geoLocation) {
+
+ try {
+ String url = nominatimUrlBuilder.buildUrl(geoLocation);
+
+ return nominatimResponder.getAddressForUrl(url);
+ } catch (IllegalArgumentException | HttpUtilException e) {
+ throw new AddressResolutionException("Failed to resolve address for " + geoLocation, e);
+ }
+ }
+
+
+ private List searchSuburbsViaNominatimsDetailPage(Long osmPlaceId, SuburbType suburbType,
+ Set suburbGlobalDisplayNames) {
+
+ List suburbs = new ArrayList<>();
+
+ String suburbUrl = nominatimUrlBuilder.buildSuburbUrl(osmPlaceId, suburbType.getType());
+ List foundSuburbs = nominatimResponder.getAddressesForUrl(suburbUrl);
+
+ if (!foundSuburbs.isEmpty()) {
+ // check for possible redundant address display names
+ for (Address foundSuburb : foundSuburbs) {
+ if (!foundSuburb.getDisplayName().contains("No Name")
+ && !suburbGlobalDisplayNames.contains(foundSuburb.getDisplayName())) {
+ // add to suburbs list
+ suburbs.add(foundSuburb);
+
+ // add to golbal display names, for next iteration
+ suburbGlobalDisplayNames.add(foundSuburb.getDisplayName());
+ }
+ }
+ }
+
+ return suburbs;
+ }
+
+
+ private List geocodeByName(String street, String postalCode, String city, String country, String name) {
+
+ // make 2 querys: 1 query for search by name, 1 query for search by street
+ String url1 = nominatimUrlBuilder.buildUrl(null, postalCode, city, country, name);
+ String url2 = nominatimUrlBuilder.buildUrl(street, postalCode, city, country, null);
+
+ List one = nominatimResponder.getAddressesForUrl(url1);
+ List two = nominatimResponder.getAddressesForUrl(url2);
+
+ // avoid duplication of places by osm_id
+ List mergedList = addressHelper.mergeSearchResultsWithoutDuplications(one, two);
+
+ // sort list so that unplausible results (e.g., not in europe) appear last
+ Collections.sort(mergedList, addressSorter);
+
+ return mergedList;
+ }
+
+
+ @Override
+ public List getAddressesByDetails(Map addressDetails) {
+
+ String[][] resolvingStrategies = createResolvingStrategies(addressDetails.get(POSTAL_CODE.getKey()),
+ addressDetails.get(CITY.getKey()), addressDetails.get(COUNTRY.getKey()),
+ addressDetails.get(NAME.getKey()),
+ addressValidator.validateStreet(addressDetails.get(STREET.getKey())));
+
+ for (String[] args : resolvingStrategies) {
+ List addresses = resolveWithNominatim(args[ARGUMENT_0], args[ARGUMENT_1], args[ARGUMENT_2],
+ args[ARGUMENT_3], args[ARGUMENT_4]);
+
+ if (!addresses.isEmpty()) {
+ return addresses;
+ }
+ }
+
+ LOG.info("No results for city " + addressDetails.get(CITY.getKey()) + " and country "
+ + addressDetails.get(COUNTRY.getKey())
+ + ". Returning empty result.");
+
+ return Collections.emptyList();
+ }
+
+
+ private String[][] createResolvingStrategies(String postalCode, String city, String country, String name,
+ String internStreet) {
+
+ return new String[][] {
+ { internStreet, postalCode, city, country, name },
+ { internStreet, null, city, country, null },
+ { null, postalCode, city, country, null },
+ { null, null, city, country, null }
+ };
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/NominatimJsonResponseParser.java b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimJsonResponseParser.java
new file mode 100644
index 00000000..f269f49b
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimJsonResponseParser.java
@@ -0,0 +1,97 @@
+package net.contargo.iris.address.nominatim.service;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.contargo.iris.address.Address;
+import net.contargo.iris.util.HttpUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import java.lang.invoke.MethodHandles;
+
+import java.util.List;
+
+
+/**
+ * The {@link NominatimJsonResponseParser} sends requests to Nominatim and converts the responses to {@link Address}
+ * objects.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+class NominatimJsonResponseParser {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final HttpUtil httpUtil;
+ private final ObjectMapper objectMapper;
+
+ public NominatimJsonResponseParser(HttpUtil httpUtil, ObjectMapper objectMapper) {
+
+ this.httpUtil = httpUtil;
+ this.objectMapper = objectMapper;
+ }
+
+ /**
+ * Returns {@link java.util.List} of {@link Address}es for the given URL. Returns an empty {@link java.util.List} if
+ * there are no search results.
+ *
+ * @param url String
+ */
+ List getAddressesForUrl(String url) {
+
+ return convertContentToAddresses(url, httpUtil.getResponseContent(url));
+ }
+
+
+ /**
+ * Extracts the addresses from the URL.
+ *
+ * @param url to extract the address
+ *
+ * @return the extracted addresses
+ */
+ List getAddressesForUrlForOsmId(String url) {
+
+ return convertContentToAddresses(url, "[" + httpUtil.getResponseContent(url) + "]");
+ }
+
+
+ Address getAddressForUrl(String reverseGeoCodingLookupURL) {
+
+ String content = httpUtil.getResponseContent(reverseGeoCodingLookupURL);
+ LOG.debug("Got result for reverse Geo coding lookup: {}", content);
+
+ Address address;
+
+ try {
+ address = objectMapper.readValue(content, Address.class);
+ } catch (IOException | NullPointerException e) {
+ address = null;
+ }
+
+ return address;
+ }
+
+
+ private List convertContentToAddresses(String url, String content) {
+
+ List addresses = null;
+
+ if (content != null) {
+ try {
+ addresses = objectMapper.readValue(content, new TypeReference>() {
+ });
+ LOG.debug("{} search result(s) found for URL {}", addresses.size(), url);
+ } catch (IOException e) {
+ addresses = null;
+ }
+ }
+
+ return addresses;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/NominatimUrlBuilder.java b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimUrlBuilder.java
new file mode 100644
index 00000000..85afa773
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/NominatimUrlBuilder.java
@@ -0,0 +1,245 @@
+package net.contargo.iris.address.nominatim.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.countries.service.CountryService;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.io.UnsupportedEncodingException;
+
+import java.lang.invoke.MethodHandles;
+
+import java.net.URLEncoder;
+
+import java.util.Map;
+
+
+/**
+ * To geocode an address {@link NominatimAddressService} needs a URL containing the base URL, different parameters and
+ * the search query. In the application context the variable parameter are defined (like email and base url), the
+ * requested format is always json since the {@link NominatimJsonResponseParser} creates Java objects by json.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Vincent Potucek - potucek@synyx.de
+ */
+class NominatimUrlBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String SEARCH_URL = "search.php?q=";
+ private static final String SUBURB_URL = "suburb.php?place_id=";
+ private static final String SUBURB_TYPE = "&suburb_type=";
+ private static final String ACCEPT_LANGUAGE = "&accept-language=";
+ private static final String FORMAT = "&format=json";
+ private static final String ADDRESS_DETAILS = "&addressdetails=1";
+ private static final String COUNTRY = "&countrycodes=";
+ private static final String WHITESPACE = " ";
+ private static final String OSMTYPE_W = "&osm_type=W";
+ private static final String OSMID = "osm_id=";
+ private static final String REVERSE_URL = "reverse?";
+ private static final String LOG_TWO_PLACEHOLDERS = "{}{}";
+ private static final String LOG_BUILT_URL = "Built URL: ";
+
+ private final String baseUrl;
+ private final String language;
+ private final CountryService countryService;
+
+ NominatimUrlBuilder(String baseUrl, String language, CountryService countryService) {
+
+ this.countryService = countryService;
+
+ Assert.hasText(baseUrl);
+ Assert.hasText(language);
+
+ this.baseUrl = baseUrl;
+ this.language = language;
+ }
+
+ /**
+ * Builds Nominatim's search url appending the query to the base URL.
+ *
+ * @param street + streetNumber
+ * @param postalCode
+ * @param city
+ * @param country
+ * @param name
+ *
+ * @return ready URL to send a GET request to Nominatim
+ */
+ String buildUrl(String street, String postalCode, String city, String country, String name) {
+
+ String url = baseUrl + SEARCH_URL + buildQuery(street, postalCode, city, name) + COUNTRY
+ + encodeUrl(buildCountryCodeList(country)) + FORMAT + ADDRESS_DETAILS + ACCEPT_LANGUAGE + language;
+
+ LOG.debug(LOG_TWO_PLACEHOLDERS, LOG_BUILT_URL, url);
+
+ return url;
+ }
+
+
+ private String buildCountryCodeList(String country) {
+
+ if (StringUtils.hasText(country)) {
+ return country;
+ } else {
+ Map cc = countryService.getCountries();
+
+ return String.join(",", cc.values());
+ }
+ }
+
+
+ /**
+ * Builds Nominatim's suburb url (modified details page) using the given place ID and suburb type.
+ *
+ * @param placeId long
+ * @param suburbType String
+ *
+ * @return ready URL to send a GET request to Nominatim
+ */
+ String buildSuburbUrl(long placeId, String suburbType) {
+
+ String suburbUrl = baseUrl + SUBURB_URL + placeId + FORMAT + ADDRESS_DETAILS + SUBURB_TYPE + suburbType;
+ LOG.debug(LOG_TWO_PLACEHOLDERS, LOG_BUILT_URL, suburbUrl);
+
+ return suburbUrl;
+ }
+
+
+ /**
+ * Appends the query to the base URL using Nominatim's special phrases like 'suburb' for specific searching.
+ *
+ * @param city the city
+ * @param specialPhrase 'suburb' or 'village' or any other specific place type
+ *
+ * @return ready URL to send a GET request to Nominatim
+ */
+ String buildSpecialPhrasesUrl(String city, String specialPhrase) {
+
+ String specialPhrasesUrl = baseUrl + SEARCH_URL + buildSpecialPhraseQuery(city, specialPhrase) + "&limit=100"
+ + FORMAT + ACCEPT_LANGUAGE + language + ADDRESS_DETAILS;
+ LOG.debug(LOG_TWO_PLACEHOLDERS, LOG_BUILT_URL, specialPhrasesUrl);
+
+ return specialPhrasesUrl;
+ }
+
+
+ /**
+ * Build the osm url within the osm id.
+ *
+ * @param osmId to build osm url
+ *
+ * @return The URL for requesting ONE Address by osmId. The osm Type used is "N" for Node by default.
+ */
+ String buildOsmUrl(long osmId) {
+
+ String osmUrl = baseUrl + REVERSE_URL + OSMID + osmId + OSMTYPE_W + FORMAT;
+ LOG.debug(LOG_TWO_PLACEHOLDERS, LOG_BUILT_URL, osmUrl);
+
+ return osmUrl;
+ }
+
+
+ /**
+ * Builds the query and converts it to URL encoded format with {@link java.net.URLEncoder} (e.g. replaces
+ * whitespaces by plus and commas by %2C).
+ *
+ * @param street: street name + house number
+ * @param postalCode
+ * @param city
+ * @param name
+ *
+ * @return search query String
+ */
+ String buildQuery(String street, String postalCode, String city, String name) {
+
+ StringBuilder queryBuilder = new StringBuilder();
+ appendStringWithSeparatorIfHasText(queryBuilder, street, ",");
+ appendStringWithSeparatorIfHasText(queryBuilder, postalCode, WHITESPACE);
+ appendStringWithSeparatorIfHasText(queryBuilder, city, WHITESPACE);
+ appendStringWithSeparatorIfHasText(queryBuilder, name, WHITESPACE);
+
+ String query = queryBuilder.toString();
+ String encodedUrl = encodeUrl(query);
+
+ return encodedUrl == null ? query : encodedUrl;
+ }
+
+
+ /**
+ * Builds the query with special phrase and city and converts it to URL encoded format with
+ * {@link java.net.URLEncoder}.
+ *
+ * @param city
+ * @param specialPhrase
+ *
+ * @return special phrases query
+ */
+ String buildSpecialPhraseQuery(String city, String specialPhrase) {
+
+ StringBuilder queryBuilder = new StringBuilder();
+
+ appendStringWithSeparatorIfHasText(queryBuilder, specialPhrase, WHITESPACE);
+ appendStringWithSeparatorIfHasText(queryBuilder, city, "");
+
+ String query = queryBuilder.toString();
+ String encodedUrl = encodeUrl(query);
+
+ return encodedUrl == null ? query : encodedUrl;
+ }
+
+
+ /**
+ * Encodes an URL with {@link java.net.URLEncoder}.
+ *
+ * @param url String
+ *
+ * @return encoded URL String
+ */
+ private String encodeUrl(String url) {
+
+ String result = null;
+
+ try {
+ result = URLEncoder.encode(url, "UTF-8");
+ } catch (UnsupportedEncodingException ex) {
+ LOG.warn("Could not encode this String to URL: {} ", url);
+ }
+
+ return result;
+ }
+
+
+ /**
+ * If the given String has text, append it with the given separator to the StringBuilder.
+ *
+ * @param builder
+ * @param string
+ * @param separator
+ */
+ void appendStringWithSeparatorIfHasText(StringBuilder builder, String string, String separator) {
+
+ if (StringUtils.hasText(string)) {
+ builder.append(string).append(separator);
+ }
+ }
+
+
+ String buildUrl(GeoLocation geoLocation) {
+
+ if (geoLocation.getLatitude() == null || geoLocation.getLongitude() == null) {
+ throw new IllegalArgumentException("Invalid Geo location: " + geoLocation);
+ }
+
+ String url = baseUrl + "reverse/" + "?format=json" + "&lat=" + geoLocation.getLatitude().toString() + "&lon="
+ + geoLocation.getLongitude().toString();
+
+ LOG.debug("Built request URL: {}", url);
+
+ return url;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/nominatim/service/SuburbType.java b/src/main/java/net/contargo/iris/address/nominatim/service/SuburbType.java
new file mode 100644
index 00000000..d3079000
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/nominatim/service/SuburbType.java
@@ -0,0 +1,26 @@
+package net.contargo.iris.address.nominatim.service;
+
+/**
+ * Enum for suburb types [suburb|administrative|village].
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ */
+enum SuburbType {
+
+ ADDRESSES("addresses"),
+ SUBURB("suburb"),
+ ADMINISTRATIVE("administrative"),
+ VILLAGE("village");
+
+ private String type;
+
+ private SuburbType(String type) {
+
+ this.type = type;
+ }
+
+ public String getType() {
+
+ return this.type;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/service/AddressCache.java b/src/main/java/net/contargo/iris/address/service/AddressCache.java
new file mode 100644
index 00000000..a31f7123
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/service/AddressCache.java
@@ -0,0 +1,78 @@
+package net.contargo.iris.address.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+import net.contargo.iris.address.AddressList;
+
+import org.springframework.cache.Cache;
+
+import java.math.BigDecimal;
+
+import java.util.List;
+
+
+/**
+ * Wraps a Cache to save resolved Address instances.
+ *
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ */
+class AddressCache {
+
+ private final Cache cache;
+
+ public AddressCache(Cache cache) {
+
+ this.cache = cache;
+ }
+
+ /**
+ * Caches all the Addresses.
+ *
+ * @param addressListList
+ */
+ public void cache(List addressListList) {
+
+ for (AddressList list : addressListList) {
+ for (Address address : list.getAddresses()) {
+ cache.put(getAddressLocationBasedHash(address), address);
+ }
+ }
+ }
+
+
+ /**
+ * Returns the Address-Instance for the given GeoLocation IF it exists in the cache - null otherwise.
+ *
+ * @param loc
+ *
+ * @return
+ */
+ public Address getForLocation(GeoLocation loc) {
+
+ Cache.ValueWrapper valWrapper = cache.get(getAddressLocationBasedHash(loc));
+
+ if (valWrapper == null) {
+ return null;
+ } else {
+ return (Address) valWrapper.get();
+ }
+ }
+
+
+ private String getAddressLocationBasedHash(GeoLocation address) {
+
+ BigDecimal latitude = new BigDecimal(-1);
+
+ if (address.getLatitude() != null) {
+ latitude = address.getLatitude();
+ }
+
+ BigDecimal longitude = new BigDecimal(-1);
+
+ if (address.getLongitude() != null) {
+ longitude = address.getLongitude();
+ }
+
+ return latitude + ":" + longitude;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java b/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java
new file mode 100644
index 00000000..b6bbf2a0
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/service/AddressServiceWrapper.java
@@ -0,0 +1,161 @@
+package net.contargo.iris.address.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+import net.contargo.iris.address.AddressList;
+import net.contargo.iris.address.nominatim.service.AddressService;
+import net.contargo.iris.address.staticsearch.StaticAddress;
+import net.contargo.iris.address.staticsearch.service.StaticAddressService;
+import net.contargo.iris.normalizer.NormalizerService;
+
+import org.apache.commons.lang.StringUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.CITY;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.COUNTRY;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.POSTAL_CODE;
+import static net.contargo.iris.address.nominatim.service.AddressDetailKey.STREET;
+
+
+/**
+ * Wrapper class to have better control about the resolving methods of {@link AddressService}.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ * @author Tobias Schneider - schneider@synyx.de
+ */
+public class AddressServiceWrapper {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AddressServiceWrapper.class);
+
+ private final AddressService addressService;
+ private final StaticAddressService staticAddressService;
+ private final AddressCache addressCache;
+ private final NormalizerService normalizerService;
+
+ public AddressServiceWrapper(AddressService addressService, StaticAddressService staticAddressService,
+ AddressCache cache, NormalizerService normalizerService) {
+
+ this.addressService = addressService;
+ this.staticAddressService = staticAddressService;
+ this.addressCache = cache;
+ this.normalizerService = normalizerService;
+ }
+
+ /**
+ * Searches an {@link Address} by the given parameters of the {@link GeoLocation} and returns it.
+ *
+ * @param geoLocation basis for the search of the {@link Address}
+ *
+ * @return The address for the given {@link GeoLocation}.
+ */
+ public Address getAddressForGeoLocation(GeoLocation geoLocation) {
+
+ Address address = addressCache.getForLocation(geoLocation);
+
+ if (address != null) {
+ return address;
+ }
+
+ StaticAddress staticAddress = staticAddressService.getForLocation(geoLocation);
+
+ if (staticAddress != null) {
+ return staticAddress.toAddress();
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Cache miss: {}", geoLocation);
+ }
+
+ address = addressService.getAddressByGeolocation(geoLocation);
+
+ if (address != null) {
+ // the geo coordinates in the returned 'address' object differ from
+ // the geo coordinates in the 'loc' object
+ address.setLatitude(geoLocation.getLatitude());
+ address.setLongitude(geoLocation.getLongitude());
+ addressCache.cache(getSimpleAddressList(Arrays.asList(address)));
+ }
+
+ return address;
+ }
+
+
+ /**
+ * Searches with the given parameters from the addressDetails map like street, postalcode and city the
+ * geocoordinates and returns a list of {@link AddressList} with their geocoordinates. To improve the result quality
+ * of geocoding services, it's sometimes better to make the search not with all the given parameters. (e.g. try to
+ * get address using only street and city)
+ *
+ * @param addressDetails keeps the search information
+ *
+ * @return list of {@link AddressList}
+ */
+ public List getAddressesByDetails(Map addressDetails) {
+
+ List result = new ArrayList<>();
+
+ String cityNormalized = normalizerService.normalize(addressDetails.get(CITY.getKey()));
+ String postalCode = addressDetails.get(POSTAL_CODE.getKey());
+ String country = addressDetails.get(COUNTRY.getKey());
+ String street = addressDetails.get(STREET.getKey());
+
+ if (StringUtils.isNotEmpty(postalCode) || StringUtils.isNotEmpty(cityNormalized)) {
+ result.add(resolveByStaticAddressService(postalCode, cityNormalized, country));
+ }
+
+ if (street != null && !"".equals(street)) {
+ result.addAll(resolveByNominatim(addressDetails));
+ }
+
+ addressCache.cache(result);
+
+ return result;
+ }
+
+
+ private AddressList resolveByStaticAddressService(String postalCode, String city, String country) {
+
+ return staticAddressService.findAddresses(postalCode, city, country);
+ }
+
+
+ private List resolveByNominatim(Map addressDetails) {
+
+ List addresses = addressService.getAddressesByDetails(addressDetails);
+
+ if (addresses == null || addresses.isEmpty()) {
+ LOG.info("No results for city " + addressDetails.get(CITY.getKey()) + " and country "
+ + addressDetails.get(COUNTRY.getKey())
+ + ". Returning empty result.");
+
+ return Collections.emptyList();
+ }
+
+ return getSimpleAddressList(addresses);
+ }
+
+
+ private List getSimpleAddressList(List addresses) {
+
+ List addressListList = new ArrayList<>();
+
+ for (Address address : addresses) {
+ List result = new ArrayList<>();
+ result.add(address);
+
+ AddressList addressesList = new AddressList(address, result);
+ addressListList.add(addressesList);
+ }
+
+ return addressListList;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/StaticAddress.java b/src/main/java/net/contargo/iris/address/staticsearch/StaticAddress.java
new file mode 100644
index 00000000..f3be2a0d
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/StaticAddress.java
@@ -0,0 +1,372 @@
+package net.contargo.iris.address.staticsearch;
+
+import com.google.common.base.Strings;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import org.springframework.util.StringUtils;
+
+import java.math.BigInteger;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import javax.validation.constraints.Size;
+
+
+/**
+ * Entity for static addresses (csv list).
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ * @author Sandra Thieme - thieme@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ */
+@Entity(name = "StaticAddress")
+// NOSONAR Don't want to override equals
+public class StaticAddress extends GeoLocation {
+
+ public static final String STATIC_ID = "static_id";
+
+ private static final int POSTAL_CODE_MAX_SIZE = 10;
+ private static final int COUNTRY_MAX_SIZE = 5;
+ private static final int HASHKEY_MAX_SIZE = 5;
+ private static final int CITY_MAX_SIZE = 255;
+ private static final int SUBURB_MAX_SIZE = 255;
+ private static final int HASH_KEY_SIZE = 5;
+ private static final int BASE_36 = 36;
+ private static final int BASE_10 = 10;
+ private static final int BEGIN_INDEX_0 = 0;
+ private static final int END_INDEX_4 = 4;
+ private static final int SHIFT_20 = 20;
+ private static final int LENGTH_AUTOINCREMENTED_PART = 12;
+ private static final int BINARY_111111 = 63;
+ private static final int SIZE_TO_THE_PAD = 16;
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Size(max = COUNTRY_MAX_SIZE)
+ private String country;
+
+ @NotBlank
+ @Size(max = POSTAL_CODE_MAX_SIZE)
+ private String postalcode;
+
+ @NotBlank
+ @Size(max = CITY_MAX_SIZE)
+ private String city;
+
+ @NotBlank
+ @Size(max = CITY_MAX_SIZE)
+ private String cityNormalized;
+
+ @Size(max = SUBURB_MAX_SIZE)
+ private String suburb;
+
+ @Size(max = SUBURB_MAX_SIZE)
+ private String suburbNormalized;
+
+ @Size(max = HASHKEY_MAX_SIZE)
+ private String hashKey;
+
+ private BigInteger uniqueId;
+
+ public Address toAddress() {
+
+ Address address = new Address();
+
+ injectDisplayName(address);
+ address.getAddress().put(Address.COUNTRY_CODE, getCountry());
+
+ if (postalcode != null) {
+ address.getAddress().put("postcode", postalcode);
+ }
+
+ if (city != null) {
+ address.getAddress().put("city", city);
+ }
+
+ if (suburb != null) {
+ address.getAddress().put("suburb", suburb);
+ }
+
+ address.getAddress().put("hashkey", getHashKey());
+ address.getAddress().put(STATIC_ID, uniqueId == null ? null : uniqueId.toString());
+ address.setLongitude(getLongitude());
+ address.setLatitude(getLatitude());
+
+ return address;
+ }
+
+
+ private void injectDisplayName(Address address) {
+
+ if (StringUtils.hasText(suburb)) {
+ address.setDisplayName(String.format("%s %s (%s)", extractString(postalcode), extractString(city),
+ extractString(suburb)));
+ } else {
+ address.setDisplayName(String.format("%s %s", extractString(postalcode), extractString(city)));
+ }
+ }
+
+
+ private String extractString(String value) {
+
+ return value == null || value.isEmpty() ? "" : value;
+ }
+
+
+ public Long getId() {
+
+ return id;
+ }
+
+
+ public void setId(Long id) {
+
+ this.id = id;
+ }
+
+
+ public String getCountry() {
+
+ return country;
+ }
+
+
+ public void setCountry(String country) {
+
+ this.country = country;
+ }
+
+
+ public String getPostalcode() {
+
+ return postalcode;
+ }
+
+
+ public void setPostalcode(String postalcode) {
+
+ this.postalcode = postalcode;
+ }
+
+
+ public String getCity() {
+
+ return city;
+ }
+
+
+ public void setCity(String city) {
+
+ this.city = city;
+ }
+
+
+ public String getCityNormalized() {
+
+ return cityNormalized;
+ }
+
+
+ public void setCityNormalized(String cityNormalized) {
+
+ this.cityNormalized = cityNormalized;
+ }
+
+
+ public String getSuburb() {
+
+ return suburb;
+ }
+
+
+ public void setSuburb(String suburb) {
+
+ this.suburb = suburb;
+ }
+
+
+ public String getSuburbNormalized() {
+
+ return suburbNormalized;
+ }
+
+
+ public void setSuburbNormalized(String suburbNormalized) {
+
+ this.suburbNormalized = suburbNormalized;
+ }
+
+
+ public String getHashKey() {
+
+ if (hashKey == null || "".equals(hashKey)) {
+ hashKey = generateHashKey();
+ }
+
+ return hashKey;
+ }
+
+
+ /**
+ * Generates a quasi unique "hashKey" from this {@link StaticAddress}'s uniqueId. The Hash Key is no real hash but a
+ * mapping. The Hash Key consists of a 5 digit Base36 number. 5 digit Base36 fits into 26 bit binary. The Hash Key
+ * is assembled in binary by Java bitwise operators and then converted to Base36.
+ *
+ *
Assembly of the 26 bit binary HashKey:
+ *
+ *
The first 6 bits are the first 4 digits of the uniqueId ( representing the systemId) mapped to a 6 digit
+ * binary number. This is only duplicate-free for a small number of possible systemIds.
+ *
+ *
The last 20 bits are the autoincremented last 12 digits of the uniqueId. This works up to 1048575 (2^20)
+ *
+ * @return the uniqueId mapped to a Base36 "hashKey"
+ */
+ private String generateHashKey() {
+
+ if (this.uniqueId == null) {
+ return "";
+ }
+
+ String uniqueIdString = this.uniqueId.toString();
+ uniqueIdString = org.apache.commons.lang.StringUtils.leftPad(uniqueIdString, SIZE_TO_THE_PAD, '0');
+
+ long systemId = Long.valueOf(uniqueIdString.substring(BEGIN_INDEX_0, END_INDEX_4));
+ long systemIdInBinary = mapToSixBits(systemId);
+ long uniqueIdAutoIncrementedPartInBinary = getAutoIncrementedPart(uniqueIdString);
+
+ long hashKeyInBinary = joinBinaryNumbers(systemIdInBinary, uniqueIdAutoIncrementedPartInBinary);
+
+ return convertToBase36(hashKeyInBinary);
+ }
+
+
+ private String convertToBase36(long hashkeyInBinary) {
+
+ String hashKeyInBase36 = conv(String.valueOf(hashkeyInBinary), BASE_10, BASE_36);
+ hashKeyInBase36 = Strings.padStart(hashKeyInBase36, HASH_KEY_SIZE, '0');
+
+ return hashKeyInBase36.toUpperCase();
+ }
+
+
+ private long getAutoIncrementedPart(String uniqueIdString) {
+
+ return Long.valueOf(uniqueIdString.substring(uniqueIdString.length() - LENGTH_AUTOINCREMENTED_PART + 1,
+ uniqueIdString.length()));
+ }
+
+
+ private long joinBinaryNumbers(long systemIdInBit, long uniqueIdAutoIncrementedPart) {
+
+ // Append 20 0-bits to the systemId so there is room for adding a larger number (up to 20 bits)
+ long finalNumberOfBitsWithSystemId = systemIdInBit << SHIFT_20;
+
+ // fill the 0-bits with the AutoIncremented part of the uniqueId. Works for every number not larger than 2^20
+ return finalNumberOfBitsWithSystemId | uniqueIdAutoIncrementedPart;
+ }
+
+
+ private long mapToSixBits(long systemId) {
+
+ // Has to map the systemId to exactly 6 bits without duplication
+ // Get the last 6 bits of the systemId by applying a binary AND with 111111. We need the 6 last bits because
+ // the currently known systemIds have conflicts in the last 5 bits.
+ return systemId & BINARY_111111;
+ }
+
+
+ public BigInteger getUniqueId() {
+
+ return uniqueId;
+ }
+
+
+ public void setUniqueId(BigInteger uniqueId) {
+
+ this.uniqueId = uniqueId;
+ hashKey = generateHashKey();
+ }
+
+
+ String conv(String input, int fromBase, int toBase) {
+
+ try {
+ int inputInFromBase = Integer.parseInt(input, fromBase);
+
+ return Integer.toString(inputInFromBase, toBase);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+
+ return new EqualsBuilder().appendSuper(super.equals(obj)).isEquals();
+ }
+
+
+ @Override
+ public int hashCode() {
+
+ return new HashCodeBuilder().appendSuper(super.hashCode()).toHashCode();
+ }
+
+
+ @Override
+ public String toString() {
+
+ return "StaticAddress [id=" + id + ", country=" + country
+ + ", postalCode=" + postalcode + ", city=" + city
+ + ", cityNormalized=" + cityNormalized + ", suburb=" + suburb
+ + ", suburbNormalized=" + suburbNormalized + ", latitude=" + getLatitude() + ", longitude=" + getLongitude()
+ + "]";
+ }
+
+
+ /**
+ * Check for changed address parameters (suburb, city, postalcode).
+ *
+ * @param staticAddress to check
+ *
+ * @return true, if address parameters are different
+ */
+ public boolean areAddressParametersDifferent(StaticAddress staticAddress) {
+
+ EqualsBuilder builder = new EqualsBuilder();
+ builder.append(this.suburb, staticAddress.getSuburb());
+ builder.append(this.city, staticAddress.getCity());
+ builder.append(this.postalcode, staticAddress.getPostalcode());
+
+ return !builder.isEquals();
+ }
+
+
+ /**
+ * Check for changed parameters longitude and latitude.
+ *
+ * @param staticAddress to check
+ *
+ * @return true, if parameters are different
+ */
+ public boolean areLatitudeAndLongitudeDifferent(StaticAddress staticAddress) {
+
+ boolean latitudeEqual = this.getLatitude().compareTo(staticAddress.getLatitude()) == 0;
+ boolean longitudeEqual = this.getLongitude().compareTo(staticAddress.getLongitude()) == 0;
+
+ return !(latitudeEqual && longitudeEqual);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/api/StaticAddressApiController.java b/src/main/java/net/contargo/iris/address/staticsearch/api/StaticAddressApiController.java
new file mode 100644
index 00000000..179d8335
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/api/StaticAddressApiController.java
@@ -0,0 +1,136 @@
+package net.contargo.iris.address.staticsearch.api;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.api.ListOfAddressListsResponse;
+import net.contargo.iris.address.dto.AddressDto;
+import net.contargo.iris.address.staticsearch.dto.StaticAddressDtoService;
+import net.contargo.iris.address.staticsearch.dto.StaticAddressesResponse;
+import net.contargo.iris.address.staticsearch.dto.StaticAddressesUidResponse;
+import net.contargo.iris.api.AbstractController;
+
+import org.slf4j.Logger;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.stereotype.Controller;
+
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.lang.invoke.MethodHandles;
+
+import java.math.BigDecimal;
+
+import java.util.Collection;
+
+import static net.contargo.iris.api.AbstractController.SLASH;
+import static net.contargo.iris.api.AbstractController.STATIC_ADDRESSES;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
+
+
+/**
+ * Controller for the public API of {@link net.contargo.iris.address.dto.AddressDto}s that belong to static addresses.
+ *
+ * @author Arnold Franke - franke@synyx.de
+ * @author David Schilling - schilling@synyx.de
+ */
+@Api(value = SLASH + STATIC_ADDRESSES, description = "API for interaction with static addresses.")
+@Controller
+@RequestMapping(SLASH + STATIC_ADDRESSES)
+public class StaticAddressApiController extends AbstractController {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+ private static final String LAT = "lat";
+ private static final String LON = "lon";
+
+ private final StaticAddressDtoService staticAddressDtoService;
+
+ @Autowired
+ public StaticAddressApiController(StaticAddressDtoService staticAddressDtoService) {
+
+ this.staticAddressDtoService = staticAddressDtoService;
+ }
+
+ @ApiOperation(value = "Returns all static addresses.", notes = "Returns all static addresses.")
+ @RequestMapping(method = RequestMethod.GET)
+ public StaticAddressesResponse getAll() {
+
+ StaticAddressesResponse response = new StaticAddressesResponse();
+ response.setAddressDtoList(staticAddressDtoService.getAll());
+
+ response.add(linkTo(getClass()).withSelfRel());
+
+ return response;
+ }
+
+
+ @ApiOperation(
+ value = "Returns all static addresses filtered by postalcode, city and country.",
+ notes = "Returns all static addresses filtered by postalcode, city and country.", response = AddressDto.class,
+ responseContainer = "List"
+ )
+ @RequestMapping(method = RequestMethod.GET, params = { "postalCode", "city", "country" })
+ @ResponseBody
+ public Collection getByPostalCodeAndCityAndCountry(
+ @RequestParam(value = "postalCode") String postalCode,
+ @RequestParam(value = "city") String city,
+ @RequestParam(value = "country") String country) {
+
+ return staticAddressDtoService.getAddressesByDetails(postalCode, city, country);
+ }
+
+
+ @ApiOperation(
+ value = "Returns all static addresses filtered by the given geolocation.",
+ notes = "Returns all static addresses filtered by the given geolocation."
+ )
+ @ModelAttribute("geoCodeResponse")
+ @RequestMapping(method = RequestMethod.GET, params = { LAT, LON })
+ public ListOfAddressListsResponse getStaticAddressByGeolocation(@RequestParam(LAT) BigDecimal latitude,
+ @RequestParam(LON) BigDecimal longitude) {
+
+ GeoLocation location = new GeoLocation(latitude, longitude);
+ ListOfAddressListsResponse response = new ListOfAddressListsResponse(
+ staticAddressDtoService.getStaticAddressByGeolocation(location));
+
+ LOG.info("API: Responding to geocode-request for geolocation {} with {} Blocks", location.toString(),
+ response.getAddresses().size());
+
+ return response;
+ }
+
+
+ @ApiOperation(
+ value = "Returns a list of static address uids that are located in a bounding box with a given radius.",
+ notes = "Returns a list of static address uids that are located in a bounding box with a given radius."
+ )
+ @RequestMapping(method = RequestMethod.GET, params = { LAT, LON, "distance" })
+ @ResponseBody
+ public StaticAddressesUidResponse staticAddressesByBoundingBox(@RequestParam(LAT) BigDecimal latitude,
+ @RequestParam(LON) BigDecimal longitude,
+ @RequestParam("distance") Double distance) {
+
+ GeoLocation location = new GeoLocation(latitude, longitude);
+
+ StaticAddressesUidResponse response = new StaticAddressesUidResponse(
+ staticAddressDtoService.getStaticAddressByBoundingBox(location, distance));
+
+ LOG.info("API: Responding with {} items to boundingbox-request for geolocation {} with {} distance",
+ response.getUids().size(), location.toString(), distance);
+
+ response.add(linkTo(methodOn(getClass()).staticAddressesByBoundingBox(latitude, longitude, distance))
+ .withSelfRel());
+
+ return response;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoService.java b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoService.java
new file mode 100644
index 00000000..60c07c90
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoService.java
@@ -0,0 +1,59 @@
+package net.contargo.iris.address.staticsearch.dto;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.dto.AddressDto;
+import net.contargo.iris.address.dto.AddressListDto;
+
+import java.math.BigInteger;
+
+import java.util.List;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+public interface StaticAddressDtoService {
+
+ /**
+ * Finds all {@link AddressDto} of static addresses by the given parameters postalCode, city and country.
+ *
+ * @param postalCode String
+ * @param city String
+ * @param country String
+ *
+ * @return List of {@link AddressDto}s.
+ */
+ List getAddressesByDetails(String postalCode, String city, String country);
+
+
+ /**
+ * Finds all {@link AddressDto} of static addresses.
+ *
+ * @return a list of all {@link AddressDto} of static addresses.
+ */
+ List getAll();
+
+
+ /**
+ * Returns one {@link net.contargo.iris.address.staticsearch.StaticAddress} wrapped in a list of lists for
+ * compatibility with the client and consistency to the other interfaces, that deliver actual lists of address
+ * lists. The static address is the one matching to the given Geolocation.
+ *
+ * @param location
+ *
+ * @return static address wrapped in a list of lists.
+ */
+ List getStaticAddressByGeolocation(GeoLocation location);
+
+
+ /**
+ * Retrieves a list of static address uids that are located in a bounding box with radius {@code km} around
+ * {@code geoLocation}.
+ *
+ * @param location the geolocation at the bounding box's center
+ * @param distance the bounding box's radius
+ *
+ * @return a list of static address uids
+ */
+ List getStaticAddressByBoundingBox(GeoLocation location, Double distance);
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoServiceImpl.java b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoServiceImpl.java
new file mode 100644
index 00000000..e018f85e
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressDtoServiceImpl.java
@@ -0,0 +1,86 @@
+package net.contargo.iris.address.staticsearch.dto;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.AddressList;
+import net.contargo.iris.address.dto.AddressDto;
+import net.contargo.iris.address.dto.AddressListDto;
+import net.contargo.iris.address.staticsearch.StaticAddress;
+import net.contargo.iris.address.staticsearch.service.StaticAddressService;
+
+import java.math.BigInteger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class StaticAddressDtoServiceImpl implements StaticAddressDtoService {
+
+ private final StaticAddressService staticAddressService;
+
+ public StaticAddressDtoServiceImpl(StaticAddressService staticAddressService) {
+
+ this.staticAddressService = staticAddressService;
+ }
+
+ @Override
+ public List getAddressesByDetails(String postalCode, String city, String country) {
+
+ List addressList = staticAddressService.getAddressesByDetails(postalCode, city, country);
+
+ return convertStaticAddressListToDTOList(addressList);
+ }
+
+
+ @Override
+ public List getAll() {
+
+ List staticAddresses = staticAddressService.getAll();
+
+ return convertStaticAddressListToDTOList(staticAddresses);
+ }
+
+
+ private List convertStaticAddressListToDTOList(List addressList) {
+
+ List addressDtoList = new ArrayList<>();
+
+ for (StaticAddress staticAddress : addressList) {
+ addressDtoList.add(new AddressDto(staticAddress.toAddress()));
+ }
+
+ return addressDtoList;
+ }
+
+
+ @Override
+ public List getStaticAddressByGeolocation(GeoLocation location) {
+
+ List addressListList = staticAddressService.getAddressListListForGeolocation(location);
+
+ List addressListDtoList = new ArrayList<>();
+
+ for (AddressList addressList : addressListList) {
+ addressListDtoList.add(new AddressListDto(addressList)); // NOSONAR
+ }
+
+ return addressListDtoList;
+ }
+
+
+ @Override
+ public List getStaticAddressByBoundingBox(GeoLocation location, Double distance) {
+
+ List staticAddresses = staticAddressService.getAddressesInBoundingBox(location, distance);
+
+ List result = new ArrayList<>();
+
+ for (StaticAddress staticAddress : staticAddresses) {
+ result.add(staticAddress.getUniqueId());
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesResponse.java b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesResponse.java
new file mode 100644
index 00000000..7a1b3fc9
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesResponse.java
@@ -0,0 +1,27 @@
+package net.contargo.iris.address.staticsearch.dto;
+
+import net.contargo.iris.address.dto.AddressDto;
+
+import org.springframework.hateoas.ResourceSupport;
+
+import java.util.List;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class StaticAddressesResponse extends ResourceSupport {
+
+ private List addressDtoList;
+
+ public List getAddressDtoList() {
+
+ return addressDtoList;
+ }
+
+
+ public void setAddressDtoList(List addressDtoList) {
+
+ this.addressDtoList = addressDtoList;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesUidResponse.java b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesUidResponse.java
new file mode 100644
index 00000000..f2efe4e5
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/dto/StaticAddressesUidResponse.java
@@ -0,0 +1,32 @@
+package net.contargo.iris.address.staticsearch.dto;
+
+import org.springframework.hateoas.ResourceSupport;
+
+import java.math.BigInteger;
+
+import java.util.List;
+
+
+/**
+ * @author Arnold Franke - franke@synyx.de
+ */
+public class StaticAddressesUidResponse extends ResourceSupport {
+
+ private List uids;
+
+ public StaticAddressesUidResponse(List staticAddressByBoundingBox) {
+
+ uids = staticAddressByBoundingBox;
+ }
+
+ public List getUids() {
+
+ return uids;
+ }
+
+
+ public void setUids(List uids) {
+
+ this.uids = uids;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/persistence/StaticAddressRepository.java b/src/main/java/net/contargo/iris/address/staticsearch/persistence/StaticAddressRepository.java
new file mode 100644
index 00000000..5de00242
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/persistence/StaticAddressRepository.java
@@ -0,0 +1,90 @@
+package net.contargo.iris.address.staticsearch.persistence;
+
+import net.contargo.iris.address.staticsearch.StaticAddress;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import java.util.List;
+
+
+/**
+ * Repository that provides access to data of {@link StaticAddress}.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ */
+public interface StaticAddressRepository extends JpaRepository {
+
+ String SELECT_FROM_STATICADDRESS_A = "SELECT a FROM StaticAddress a ";
+ String ORDER_BY_CITY_SUBURB_POSTALCODE = "ORDER BY a.cityNormalized, a.suburbNormalized, a.postalcode";
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE a.postalcode = ?1 "
+ + "AND (a.cityNormalized LIKE ?2 OR a.suburbNormalized LIKE ?2) " + ORDER_BY_CITY_SUBURB_POSTALCODE
+ )
+ List findByPostalCodeAndCity(String postalCode, String city);
+
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE a.postalcode = ?1 "
+ + "OR a.cityNormalized LIKE ?2 "
+ + "OR a.suburbNormalized LIKE ?2 " + ORDER_BY_CITY_SUBURB_POSTALCODE
+ )
+ List findByPostalCodeOrCity(String postalCode, String city);
+
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE a.country = ?3 "
+ + "AND a.postalcode = ?1 "
+ + "AND (a.cityNormalized LIKE CONCAT('%', ?2, '%') OR a.suburbNormalized LIKE CONCAT('%', ?2, '%')) "
+ + ORDER_BY_CITY_SUBURB_POSTALCODE
+ )
+ List findByCountryAndPostalCodeAndCity(String postalCode, String city, String country);
+
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE a.country = ?3 "
+ + "AND ((a.postalcode = ?1 AND a.postalcode <> '') "
+ + "OR (a.cityNormalized LIKE ?2 AND a.cityNormalized <> '') "
+ + "OR (a.suburbNormalized LIKE ?2 AND a.suburbNormalized <> '')) " + ORDER_BY_CITY_SUBURB_POSTALCODE
+ )
+ List findByCountryAndPostalCodeOrCity(String postalCode, String city, String country);
+
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE (a.latitude between ?1 AND ?2) "
+ + "AND (a.longitude between ?3 AND ?4) " + ORDER_BY_CITY_SUBURB_POSTALCODE
+ )
+ List findByBoundingBox(BigDecimal fromLatitude, BigDecimal toLatitude, BigDecimal fromLongitude,
+ BigDecimal toLongitude);
+
+
+ @Query(
+ SELECT_FROM_STATICADDRESS_A
+ + "WHERE (a.hashKey is null OR a.hashKey = '') "
+ )
+ Page findMissingHashKeys(Pageable pageable);
+
+
+ List findByCityNormalizedAndSuburbNormalizedAndPostalcode(String city, String suburb,
+ String postalcode);
+
+
+ StaticAddress findByLatitudeAndLongitude(BigDecimal latitude, BigDecimal longitude);
+
+
+ StaticAddress findByUniqueId(BigInteger uniqueId);
+
+
+ List findByPostalcode(String postalCode);
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressCoordinatesDuplicationException.java b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressCoordinatesDuplicationException.java
new file mode 100644
index 00000000..8da91efd
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressCoordinatesDuplicationException.java
@@ -0,0 +1,10 @@
+package net.contargo.iris.address.staticsearch.service;
+
+/**
+ * Is thrown when new StaticAddresses are putted in the db which, are duplicates (coordinates) to others.
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ */
+
+public class StaticAddressCoordinatesDuplicationException extends RuntimeException {
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressDuplicationException.java b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressDuplicationException.java
new file mode 100644
index 00000000..7f04019d
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressDuplicationException.java
@@ -0,0 +1,10 @@
+package net.contargo.iris.address.staticsearch.service;
+
+/**
+ * Is thrown when new StaticAddresses are putted in the db which, are duplicates to others.
+ *
+ * @author David Schilling - schilling@synyx.de
+ */
+
+public class StaticAddressDuplicationException extends RuntimeException {
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressService.java b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressService.java
new file mode 100644
index 00000000..ffa09f4e
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressService.java
@@ -0,0 +1,123 @@
+package net.contargo.iris.address.staticsearch.service;
+
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.AddressList;
+import net.contargo.iris.address.staticsearch.StaticAddress;
+
+import java.util.List;
+
+
+/**
+ * Provides service methods for {@link StaticAddress} entities.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ */
+public interface StaticAddressService {
+
+ /**
+ * Finds all {@link net.contargo.iris.address.Address}es by the given parameters postalCode, city and country.
+ *
+ * @param postalCode String
+ * @param city String
+ * @param country String
+ *
+ * @return {@link AddressList}
+ */
+ AddressList findAddresses(String postalCode, String city, String country);
+
+
+ /**
+ * Finds all {@link StaticAddress} by the given parameters postalCode, city and country. The procedure of this
+ * method is following (step to next procedure if result list is empty): 1. Execute an AND search, i.e. find
+ * addresses by postal code AND city 2. Execute an OR search, i.e. find addresses by postal code OR city 3. Execute
+ * a split search, i.e. if the city string is "Neustadt an der Weinstrasse" and nothing is found for that then
+ * execute search for "Neustadt"
+ *
+ * @param postalCode String
+ * @param city String
+ * @param country String
+ *
+ * @return List of {@link StaticAddress}es.
+ */
+ List getAddressesByDetailsWithFallbacks(String postalCode, String city, String country);
+
+
+ List getAddressesByDetails(String postalCode, String city, String country);
+
+
+ /**
+ * Finds an Address at the exact given location or null.
+ *
+ * @param loc
+ *
+ * @return
+ */
+ StaticAddress getForLocation(GeoLocation loc);
+
+
+ /**
+ * @param staticAddressId
+ *
+ * @return
+ */
+ StaticAddress findbyId(Long staticAddressId);
+
+
+ /**
+ * saves the staticAddress in the Database.
+ *
+ * @param staticAddress the staticAddress to save in the database
+ *
+ * @return staticAddress which was saved in the database
+ *
+ * @throws StaticAddressDuplicationException if duplicate was found.
+ */
+ StaticAddress saveStaticAddress(StaticAddress staticAddress);
+
+
+ /**
+ * Finds all {@link StaticAddress}.
+ *
+ * @return a list of all {@link StaticAddress}.
+ */
+ List getAll();
+
+
+ /**
+ * @param staticAddressId
+ *
+ * @return The _one_ Address for the given static address ID. For consistent processing on client side it is
+ * wrapped in a list of AddressLists.
+ */
+ List getAddressListListForStaticAddressId(Long staticAddressId);
+
+
+ /**
+ * @param location
+ *
+ * @return The _one_ Address for the given geoLocation. For consistent processing on client side it is wrapped in a
+ * list of AddressLists.
+ */
+ List getAddressListListForGeolocation(GeoLocation location);
+
+
+ void normalizeFields(StaticAddress staticAddress);
+
+
+ /**
+ * looks for database entries with missing hashkeys. generates and saves the hashkeys.
+ */
+ void fillMissingHashKeys();
+
+
+ /**
+ * Retrieves a list of static addresses that are located in a bounding box with radius {@code km} around
+ * {@code geoLocation}.
+ *
+ * @param geoLocation the geolocation at the bounding box's center
+ * @param km the bounding box's radius
+ *
+ * @return a list of static addresses
+ */
+ List getAddressesInBoundingBox(GeoLocation geoLocation, Double km);
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressServiceImpl.java b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressServiceImpl.java
new file mode 100644
index 00000000..932fcb71
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/service/StaticAddressServiceImpl.java
@@ -0,0 +1,497 @@
+package net.contargo.iris.address.staticsearch.service;
+
+import net.contargo.iris.BoundingBox;
+import net.contargo.iris.GeoLocation;
+import net.contargo.iris.address.Address;
+import net.contargo.iris.address.AddressList;
+import net.contargo.iris.address.staticsearch.StaticAddress;
+import net.contargo.iris.address.staticsearch.persistence.StaticAddressRepository;
+import net.contargo.iris.normalizer.NormalizerServiceImpl;
+import net.contargo.iris.sequence.service.SequenceService;
+
+import org.slf4j.Logger;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+import org.springframework.transaction.annotation.Transactional;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.lang.invoke.MethodHandles;
+
+import java.math.BigInteger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.persistence.Entity;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+
+/**
+ * Implementation of {@link StaticAddressService}.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ */
+@Transactional
+public class StaticAddressServiceImpl implements StaticAddressService {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+ private static final String WILDCARD = "%";
+ private static final int PAGE_SIZE = 50;
+
+ private final StaticAddressRepository repository;
+
+ private final SequenceService uniqueIdSequenceService;
+
+ private final NormalizerServiceImpl normalizerService;
+
+ public StaticAddressServiceImpl(StaticAddressRepository repository, SequenceService uniqueIdSequenceService,
+ NormalizerServiceImpl normalizerService) {
+
+ this.repository = repository;
+ this.uniqueIdSequenceService = uniqueIdSequenceService;
+ this.normalizerService = normalizerService;
+ }
+
+ /**
+ * Finds all {@link Address}es by the given parameters postalCode, city and country. The procedure of this method is
+ * following (step to next procedure if result list is empty): 1. Execute an AND search, i.e. find addresses by
+ * postal code AND city 2. Execute an OR search, i.e. find addresses by postal code OR city 3. Execute a split
+ * search, i.e. if the city string is "Neustadt an der Weinstrasse" and nothing is found for that then execute
+ * search for "Neustadt"
+ *
+ * @param postalCode String
+ * @param city String
+ * @param country String
+ *
+ * @return {@link AddressList}
+ */
+ @Override
+ public AddressList findAddresses(String postalCode, String city, String country) {
+
+ List staticAddresses = getAddressesByDetailsWithFallbacks(postalCode, city, country);
+
+ List addresses = staticAddresses.stream().map(StaticAddress::toAddress).collect(Collectors.toList());
+
+ return new AddressList("City and Suburb Results", addresses);
+ }
+
+
+ @Override
+ @Transactional(readOnly = true)
+ public List getAddressesByDetailsWithFallbacks(String postalCode, String city, String country) {
+
+ String normalizedCity = normalizerService.normalize(city);
+
+ List staticAddresses = executeANDSearch(postalCode, normalizedCity, country);
+
+ if (staticAddresses.isEmpty()) {
+ // fallback to OR search
+ staticAddresses = executeORSearch(postalCode, normalizedCity, country);
+ }
+
+ if (staticAddresses.isEmpty() && city != null) {
+ // fallback to split search
+ staticAddresses = executeSplitSearch(postalCode, city, country);
+ }
+
+ return staticAddresses;
+ }
+
+
+ @Override
+ public List getAddressesByDetails(String postalCode, String city, String country) {
+
+ String normalizedCity = normalizerService.normalize(city);
+
+ return repository.findByCountryAndPostalCodeAndCity(postalCode, normalizedCity, country);
+ }
+
+
+ @Override
+ @Transactional(readOnly = true)
+ public StaticAddress getForLocation(GeoLocation loc) {
+
+ return repository.findByLatitudeAndLongitude(loc.getLatitude(), loc.getLongitude());
+ }
+
+
+ @Override
+ @Transactional(readOnly = true)
+ public StaticAddress findbyId(Long staticAddressId) {
+
+ return repository.findOne(staticAddressId);
+ }
+
+
+ @Override
+ public synchronized StaticAddress saveStaticAddress(StaticAddress staticAddress) {
+
+ setEmptyValues(staticAddress);
+
+ normalizeFields(staticAddress);
+
+ if (staticAddress.getId() == null) {
+ return saveNewStaticAddress(staticAddress);
+ } else {
+ return updateStaticAddress(staticAddress);
+ }
+ }
+
+
+ @Override
+ public List getAll() {
+
+ return repository.findAll();
+ }
+
+
+ /**
+ * @param staticAddressId
+ *
+ * @return The _one_ Address for the given static address ID. For consistent processing on client side it is
+ * wrapped in a list of AddressLists.
+ */
+ @Override
+ public List getAddressListListForStaticAddressId(Long staticAddressId) {
+
+ StaticAddress staticAddress = findbyId(staticAddressId);
+
+ if (staticAddress == null) {
+ return Collections.emptyList();
+ }
+
+ AddressList staticAddressList = new AddressList("Result ", Arrays.asList(staticAddress.toAddress()));
+
+ return Arrays.asList(staticAddressList);
+ }
+
+
+ /**
+ * @param location
+ *
+ * @return The _one_ Address for the given geoLocation. For consistent processing on client side it is wrapped in a
+ * list of AddressLists.
+ */
+ @Override
+ public List getAddressListListForGeolocation(GeoLocation location) {
+
+ StaticAddress staticAddress = getForLocation(location);
+
+ if (staticAddress == null) {
+ return Collections.emptyList();
+ }
+
+ AddressList staticAddressList = new AddressList("Result ", Arrays.asList(staticAddress.toAddress()));
+
+ return Arrays.asList(staticAddressList);
+ }
+
+
+ private void setEmptyValues(StaticAddress staticAddress) {
+
+ if (staticAddress.getCity() == null) {
+ staticAddress.setCity("");
+ }
+
+ if (staticAddress.getPostalcode() == null) {
+ staticAddress.setPostalcode("");
+ }
+
+ if (staticAddress.getSuburb() == null) {
+ staticAddress.setSuburb("");
+ }
+ }
+
+
+ @Override
+ public void normalizeFields(StaticAddress staticAddress) {
+
+ if (staticAddress.getCity() != null) {
+ staticAddress.setCityNormalized(normalizerService.normalize(staticAddress.getCity()));
+ }
+
+ if (staticAddress.getSuburb() != null) {
+ staticAddress.setSuburbNormalized(normalizerService.normalize(staticAddress.getSuburb()));
+ }
+ }
+
+
+ @Override
+ public void fillMissingHashKeys() {
+
+ LOG.info("Starting to fill the static address hashkeys with " + PAGE_SIZE + " items per page");
+
+ int currentPage = 0;
+ int totalPages = getTotalPages(currentPage, PAGE_SIZE);
+
+ boolean hasElements = true;
+
+ while (hasElements) {
+ List addressesOfCurrentPage = getAddresses(0, PAGE_SIZE);
+
+ // change state
+ hasElements = !addressesOfCurrentPage.isEmpty();
+
+ if (hasElements) {
+ LOG.info(String.format("Processing page %d of %d", currentPage + 1, totalPages));
+
+ for (StaticAddress staticAddress : addressesOfCurrentPage) {
+ staticAddress.setUniqueId(staticAddress.getUniqueId());
+ repository.save(staticAddress);
+ }
+
+ currentPage = currentPage + 1;
+ }
+ }
+
+ LOG.info("Finished filling the hashkeys");
+ }
+
+
+ @Override
+ public List getAddressesInBoundingBox(GeoLocation geoLocation, Double km) {
+
+ Assert.notNull(geoLocation);
+ Assert.notNull(km);
+
+ BoundingBox box = geoLocation.getBoundingBox(km);
+ GeoLocation lowerLeft = box.getLowerLeft();
+ GeoLocation upperRight = box.getUpperRight();
+
+ return repository.findByBoundingBox(lowerLeft.getLatitude(), upperRight.getLatitude(), lowerLeft.getLongitude(),
+ upperRight.getLongitude());
+ }
+
+
+ private StaticAddress updateStaticAddress(StaticAddress staticAddress) {
+
+ StaticAddress staticAddressFromDb = repository.findOne(staticAddress.getId());
+
+ boolean addressParametersDifferent = staticAddressFromDb.areAddressParametersDifferent(staticAddress);
+
+ boolean coordinatesDifferent = staticAddressFromDb.areLatitudeAndLongitudeDifferent(staticAddress);
+
+ if (addressParametersDifferent && checkDuplicateAddressParameters(staticAddress)) {
+ throw new StaticAddressDuplicationException();
+ }
+
+ if (coordinatesDifferent && checkOnDuplicateCoordinates(staticAddressFromDb, staticAddress)) {
+ throw new StaticAddressCoordinatesDuplicationException();
+ }
+
+ return repository.save(staticAddress);
+ }
+
+
+ private StaticAddress saveNewStaticAddress(StaticAddress staticAddress) {
+
+ if (staticAddress.getUniqueId() == null) {
+ staticAddress.setUniqueId(determineUniqueId());
+ }
+
+ if (checkDuplicateAddressParameters(staticAddress)) {
+ throw new StaticAddressDuplicationException();
+ }
+
+ if (checkOnDuplicateCoordinates(staticAddress)) {
+ throw new StaticAddressCoordinatesDuplicationException();
+ }
+
+ return repository.save(staticAddress);
+ }
+
+
+ BigInteger determineUniqueId() {
+
+ String entityName = StaticAddress.class.getAnnotation(Entity.class).name();
+ BigInteger nextUniqueId = uniqueIdSequenceService.getNextId(entityName);
+ boolean isUniqueIdAlreadyAssigned = isUniqueIdAlreadyAssigned(nextUniqueId);
+
+ while (isUniqueIdAlreadyAssigned) {
+ // In this loop we increment the ID by ourselves to avoid write-accesses to the DB for performance
+ LOG.warn("StaticAddress uniqueId {} already assigned - trying next uniqueId", nextUniqueId);
+ nextUniqueId = nextUniqueId.add(BigInteger.ONE);
+
+ if (!isUniqueIdAlreadyAssigned(nextUniqueId)) {
+ isUniqueIdAlreadyAssigned = false;
+ uniqueIdSequenceService.setNextId(entityName, nextUniqueId);
+ }
+ }
+
+ return nextUniqueId;
+ }
+
+
+ private boolean isUniqueIdAlreadyAssigned(BigInteger uniqueId) {
+
+ return repository.findByUniqueId(uniqueId) != null;
+ }
+
+
+ private boolean checkOnDuplicateCoordinates(StaticAddress staticAddressFromDb, StaticAddress staticAddress) {
+
+ // compare coordinates
+ if (null != staticAddressFromDb && staticAddressFromDb.equals(staticAddress)) {
+ return false;
+ }
+
+ StaticAddress addressFromDb = repository.findByLatitudeAndLongitude(staticAddress.getLatitude(),
+ staticAddress.getLongitude());
+
+ return null != addressFromDb;
+ }
+
+
+ private boolean checkOnDuplicateCoordinates(StaticAddress staticAddress) {
+
+ return this.checkOnDuplicateCoordinates(null, staticAddress);
+ }
+
+
+ private boolean checkDuplicateAddressParameters(StaticAddress staticAddress) {
+
+ List staticAddresses = repository.findByCityNormalizedAndSuburbNormalizedAndPostalcode(
+ staticAddress.getCityNormalized(), staticAddress.getSuburbNormalized(), staticAddress.getPostalcode());
+
+ return !staticAddresses.isEmpty();
+ }
+
+
+ /**
+ * Similar to method executeORSearch, only using other repository methods.
+ */
+ private List executeANDSearch(String postalCode, String city, String country) {
+
+ List addresses = new ArrayList<>();
+
+ if (StringUtils.hasText(country)) {
+ addresses = repository.findByCountryAndPostalCodeAndCity(postalCode, getParameterWithWildcard(city),
+ country);
+ } else if (StringUtils.hasText(city)) {
+ addresses = repository.findByPostalCodeAndCity(postalCode, getParameterWithWildcard(city));
+ } else {
+ repository.findByPostalcode(postalCode);
+ }
+
+ return addresses;
+ }
+
+
+ /**
+ * Similar to method executeANDSearch, only using other repository methods.
+ */
+ private List executeORSearch(String postalCode, String city, String country) {
+
+ List addresses;
+
+ if (StringUtils.hasText(country)) {
+ addresses = repository.findByCountryAndPostalCodeOrCity(postalCode, getParameterWithWildcard(city),
+ country);
+ } else if (StringUtils.hasText(city)) {
+ addresses = repository.findByPostalCodeOrCity(postalCode, getParameterWithWildcard(city));
+ } else {
+ addresses = repository.findByPostalcode(postalCode);
+ }
+
+ return addresses;
+ }
+
+
+ /**
+ * Adds wildcard to String if it is not empty and returns the new String.
+ *
+ * @param param
+ *
+ * @return new String with wildcard
+ */
+ private String getParameterWithWildcard(String param) {
+
+ // rule for wildcard city: "city%"
+ if (StringUtils.hasText(param)) {
+ return param + WILDCARD;
+ }
+
+ return param;
+ }
+
+
+ /**
+ * This is a fallback method if neither executeANDSearch nor executeORSearch have a result. The city string is split
+ * on whitespaces. The current implementation executes the search only for first element of the split string.
+ *
+ * @param postalCode
+ * @param city
+ * @param country
+ *
+ * @return {@link java.util.List} of {@link StaticAddress} matching the given search parameters
+ */
+ private List executeSplitSearch(String postalCode, String city, String country) {
+
+ Assert.notNull(city);
+
+ String[] singleSearchParameters = city.split(" ");
+
+ String firstString = normalizerService.normalize(singleSearchParameters[0]);
+ List addresses = executeANDSearch(postalCode, firstString, country);
+
+ if (addresses.isEmpty()) {
+ addresses = executeORSearch(postalCode, firstString, country);
+ }
+
+ return addresses;
+ }
+
+
+ /**
+ * Retrieves total amount of pages, according to pagesize.
+ *
+ * @param currentPage
+ * @param pageSize
+ *
+ * @return
+ */
+ private int getTotalPages(int currentPage, int pageSize) {
+
+ Pageable pageable = new PageRequest(currentPage, pageSize);
+
+ return getAllPagesForEmptyItems(pageable).getTotalPages();
+ }
+
+
+ /**
+ * Retreive only empty pages from repository..
+ *
+ * @param pageable
+ *
+ * @return
+ */
+ private Page getAllPagesForEmptyItems(Pageable pageable) {
+
+ return repository.findMissingHashKeys(pageable);
+ }
+
+
+ /**
+ * Get pageable addresses according to the given page size.
+ *
+ * @param startPage
+ * @param pageSize
+ *
+ * @return
+ */
+ private List getAddresses(int startPage, int pageSize) {
+
+ Pageable pageable = new PageRequest(startPage, pageSize);
+
+ return getAllPagesForEmptyItems(pageable).getContent();
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressController.java b/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressController.java
new file mode 100644
index 00000000..26b143aa
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressController.java
@@ -0,0 +1,161 @@
+package net.contargo.iris.address.staticsearch.web;
+
+import net.contargo.iris.Message;
+import net.contargo.iris.address.staticsearch.StaticAddress;
+import net.contargo.iris.address.staticsearch.service.StaticAddressCoordinatesDuplicationException;
+import net.contargo.iris.address.staticsearch.service.StaticAddressDuplicationException;
+import net.contargo.iris.address.staticsearch.service.StaticAddressService;
+import net.contargo.iris.api.AbstractController;
+import net.contargo.iris.sequence.service.UniqueIdSequenceServiceException;
+
+import org.slf4j.Logger;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.springframework.stereotype.Controller;
+
+import org.springframework.ui.Model;
+
+import org.springframework.validation.BindingResult;
+
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.lang.invoke.MethodHandles;
+
+import java.util.List;
+
+import javax.validation.Valid;
+
+import static net.contargo.iris.Message.error;
+import static net.contargo.iris.Message.success;
+import static net.contargo.iris.api.AbstractController.SLASH;
+import static net.contargo.iris.api.AbstractController.STATIC_ADDRESSES;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+
+/**
+ * Controller for the web API of {@link net.contargo.iris.address.staticsearch.StaticAddress}s.
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ * @author Arnold Franke - franke@synyx.de
+ * @author Jörg Alberto Hoffmann - hoffmann@synyx.de
+ */
+@Controller
+@RequestMapping(SLASH + STATIC_ADDRESSES)
+public class StaticAddressController extends AbstractController {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String CONTROLLER_CONTEXT = "staticAddressManagement" + SLASH;
+
+ private static final String STATIC_ADDRESS_REQUEST_BEAN = "request";
+ private static final String ENTITY_ATTRIBUTE = "staticAddress";
+ private static final String ENTITYS_ATTRIBUTE = "staticAddresses";
+
+ private static final String ENTITY_VIEW = "staticaddresses";
+ private static final String ENTITY_FORM_VIEW = "staticaddressForm";
+
+ private static final Message SAVE_SUCCESS_MESSAGE = success("staticaddress.save.success");
+ private static final Message UPDATE_SUCCESS_MESSAGE = success("staticaddress.update.success");
+ private static final Message DUPLICATION_ERROR_MESSAGE = error("staticaddress.error.duplicate");
+ private static final Message DUPLICATION_GEOCOORDINATES_ERROR_MESSAGE = error(
+ "staticaddress.error.coordinates.duplicate");
+
+ private final StaticAddressService staticAddressService;
+
+ @Autowired
+ public StaticAddressController(StaticAddressService staticAddressService) {
+
+ this.staticAddressService = staticAddressService;
+ }
+
+ @RequestMapping(method = RequestMethod.GET)
+ public String getByDetails(Model model, @ModelAttribute StaticAddressRequest staticAddressRequest) {
+
+ if (!staticAddressRequest.isEmpty()) {
+ List staticAddressList = staticAddressService.getAddressesByDetailsWithFallbacks(
+ staticAddressRequest.getPostalcode(), staticAddressRequest.getCity(), null);
+
+ model.addAttribute(ENTITYS_ATTRIBUTE, staticAddressList);
+ }
+
+ model.addAttribute(STATIC_ADDRESS_REQUEST_BEAN, staticAddressRequest);
+
+ return CONTROLLER_CONTEXT + ENTITY_VIEW;
+ }
+
+
+ @RequestMapping(value = SLASH + ID_PARAM, method = RequestMethod.GET)
+ public String getStaticAddress(Model model, @PathVariable Long id) {
+
+ model.addAttribute(ENTITY_ATTRIBUTE, staticAddressService.findbyId(id));
+
+ return CONTROLLER_CONTEXT + ENTITY_FORM_VIEW;
+ }
+
+
+ @RequestMapping(value = SLASH + "new", method = RequestMethod.GET)
+ public String prepareForCreate(Model model) {
+
+ model.addAttribute(ENTITY_ATTRIBUTE, new StaticAddress());
+
+ return CONTROLLER_CONTEXT + ENTITY_FORM_VIEW;
+ }
+
+
+ @ModelAttribute("staticAddress")
+ public void prepareSaveStaticAddress(StaticAddress staticAddress) {
+
+ staticAddressService.normalizeFields(staticAddress);
+ }
+
+
+ @RequestMapping(value = SLASH, method = RequestMethod.POST)
+ public String saveStaticAddress(@Valid @ModelAttribute StaticAddress staticAddress, BindingResult result,
+ Model model) {
+
+ return saveOrUpdateStaticAddress(staticAddress, result, model, SAVE_SUCCESS_MESSAGE);
+ }
+
+
+ @RequestMapping(value = SLASH + ID_PARAM, method = RequestMethod.PUT)
+ public String updateStaticAddress(@Valid @ModelAttribute StaticAddress staticAddress, BindingResult result,
+ Model model) {
+
+ return saveOrUpdateStaticAddress(staticAddress, result, model, UPDATE_SUCCESS_MESSAGE);
+ }
+
+
+ private String saveOrUpdateStaticAddress(StaticAddress staticAddress, BindingResult result, Model model,
+ Message message) {
+
+ if (result.hasErrors()) {
+ model.addAttribute(ENTITY_ATTRIBUTE, staticAddress);
+
+ return CONTROLLER_CONTEXT + ENTITY_FORM_VIEW;
+ }
+
+ try {
+ StaticAddress savedStaticAddress = staticAddressService.saveStaticAddress(staticAddress);
+
+ model.addAttribute(ENTITY_ATTRIBUTE, savedStaticAddress);
+ model.addAttribute(AbstractController.MESSAGE, message);
+ } catch (StaticAddressDuplicationException e) {
+ model.addAttribute(ENTITY_ATTRIBUTE, staticAddress);
+ model.addAttribute(AbstractController.MESSAGE, DUPLICATION_ERROR_MESSAGE);
+ } catch (StaticAddressCoordinatesDuplicationException e) {
+ model.addAttribute(ENTITY_ATTRIBUTE, staticAddress);
+ model.addAttribute(AbstractController.MESSAGE, DUPLICATION_GEOCOORDINATES_ERROR_MESSAGE);
+ } catch (UniqueIdSequenceServiceException e) {
+ model.addAttribute(ENTITY_ATTRIBUTE, staticAddress);
+ model.addAttribute(AbstractController.MESSAGE, UNIQUEID_ERROR_MESSAGE);
+ LOG.error(e.getMessage());
+ }
+
+ return CONTROLLER_CONTEXT + ENTITY_FORM_VIEW;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressRequest.java b/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressRequest.java
new file mode 100644
index 00000000..ddefb072
--- /dev/null
+++ b/src/main/java/net/contargo/iris/address/staticsearch/web/StaticAddressRequest.java
@@ -0,0 +1,52 @@
+package net.contargo.iris.address.staticsearch.web;
+
+import org.springframework.util.StringUtils;
+
+
+/**
+ * View bean to encapsulate search parameter for a static address.
+ *
+ * @author Michael Herbold - herbold@synyx.de
+ */
+class StaticAddressRequest {
+
+ private String postalcode = null;
+ private String city = null;
+
+ public String getPostalcode() {
+
+ if (null != postalcode && postalcode.isEmpty()) {
+ postalcode = null;
+ }
+
+ return postalcode;
+ }
+
+
+ public void setPostalcode(String postalcode) {
+
+ this.postalcode = postalcode;
+ }
+
+
+ public String getCity() {
+
+ if (null != city && city.isEmpty()) {
+ city = null;
+ }
+
+ return city;
+ }
+
+
+ public void setCity(String city) {
+
+ this.city = city;
+ }
+
+
+ public boolean isEmpty() {
+
+ return StringUtils.isEmpty(postalcode) && StringUtils.isEmpty(city);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/AbstractController.java b/src/main/java/net/contargo/iris/api/AbstractController.java
new file mode 100644
index 00000000..589fa6ca
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/AbstractController.java
@@ -0,0 +1,54 @@
+package net.contargo.iris.api;
+
+import net.contargo.iris.Message;
+
+import static net.contargo.iris.Message.error;
+
+
+/**
+ * @author Vincent Potucek - potucek@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ */
+public abstract class AbstractController {
+
+ public static final Message UNIQUEID_ERROR_MESSAGE = error("uniqueid.error.message");
+
+ // Domain specific constants
+ public static final String TERMINALS = "terminals";
+ public static final String SEAPORTS = "seaports";
+ public static final String ROUTE_TYPES = "routetypes";
+ public static final String STATIC_ADDRESSES = "staticaddresses";
+ public static final String CONNECTIONS = "connections";
+ public static final String LOGIN = "login";
+ public static final String ROUTE_DETAILS = "routedetails";
+ public static final String OSM_ADDRESSES = "osmaddresses";
+ public static final String COUNTRIES = "countries";
+ public static final String REVERSE_GEOCODE = "reversegeocode";
+ public static final String ADDRESSES = "addresses";
+ public static final String PLACES = "places";
+ public static final String GEOCODES = "geocodes";
+ public static final String SIMPLE_GEOCODES = "simplegeocodes";
+ public static final String TRIANGLE = "triangle";
+ public static final String TRIANGLE_VIEW = "routing/triangle";
+
+ // parameter constants
+ public static final String ID_PARAM = "{id}";
+ public static final String ID = "id";
+ public static final String PARAM_LATITUDE = "{lat}";
+ public static final String PARAM_LONGITUDE = "{lon}";
+
+ // character constants
+ public static final String SLASH = "/";
+ public static final String COLON = ":";
+ public static final String STAR = "*";
+
+ // navigation constants
+ public static final String FORM = "form";
+ public static final String MESSAGE = "message";
+ public static final String REDIRECT = "redirect:";
+ public static final String RESPONSE = "response";
+ public static final String WEB = "web";
+ public static final String LINK_REF_ROOT = "root";
+ public static final String INDEX = "index";
+ public static final String WEBAPI_ROOT_URL = SLASH + WEB + SLASH;
+}
diff --git a/src/main/java/net/contargo/iris/api/NotFoundException.java b/src/main/java/net/contargo/iris/api/NotFoundException.java
new file mode 100644
index 00000000..429d96a1
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/NotFoundException.java
@@ -0,0 +1,14 @@
+package net.contargo.iris.api;
+
+/**
+ * Should be thrown when an entity cannot be found. Will be mapped to an HTTP 404 error.
+ *
+ * @author Sandra Thieme - thieme@synyx.de
+ */
+public class NotFoundException extends RuntimeException {
+
+ public NotFoundException(String message) {
+
+ super(message);
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/PublicAPIExceptionHandler.java b/src/main/java/net/contargo/iris/api/PublicAPIExceptionHandler.java
new file mode 100644
index 00000000..6ab89315
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/PublicAPIExceptionHandler.java
@@ -0,0 +1,98 @@
+package net.contargo.iris.api;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * This ExceptionHandler handles thrown Exceptions of our public API and responds with Exception specific HTTP status
+ * codes.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Tobias Schneider - schneider@synyx.de
+ */
+public class PublicAPIExceptionHandler implements HandlerExceptionResolver {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PublicAPIExceptionHandler.class);
+
+ @Override
+ public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
+ Exception ex) {
+
+ ModelAndView modelAndView = new ModelAndView();
+
+ LOG.error("REST-API Exception: " + ex.getMessage(), ex);
+
+ try {
+ if (ex instanceof IllegalArgumentException) {
+ modelAndView = handleIllegalArgumentException(response, (IllegalArgumentException) ex);
+ } else if (ex instanceof IllegalStateException) {
+ modelAndView = handleIllegalStateException(response, (IllegalStateException) ex);
+ } else if (ex instanceof HttpClientErrorException) {
+ modelAndView = handleHttpClientErrorException(response, (HttpClientErrorException) ex);
+ } else if (ex instanceof NotFoundException) {
+ modelAndView = handleNotFoundException(response, (NotFoundException) ex);
+ } else {
+ modelAndView = handleGenericException(response, ex);
+ }
+ } catch (IOException handlerException) {
+ LOG.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
+ }
+
+ return modelAndView;
+ }
+
+
+ private ModelAndView handleIllegalArgumentException(HttpServletResponse response, IllegalArgumentException ex)
+ throws IOException {
+
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request. " + ex.getMessage());
+
+ return new ModelAndView();
+ }
+
+
+ private ModelAndView handleIllegalStateException(HttpServletResponse response, IllegalStateException ex)
+ throws IOException {
+
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad request. " + ex.getMessage());
+
+ return new ModelAndView();
+ }
+
+
+ private ModelAndView handleHttpClientErrorException(HttpServletResponse response, HttpClientErrorException ex)
+ throws IOException {
+
+ response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+ "Service is temporary not available, please try again later. " + ex.getMessage());
+
+ return new ModelAndView();
+ }
+
+
+ private ModelAndView handleGenericException(HttpServletResponse response, Exception ex) throws IOException {
+
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error. " + ex.getMessage());
+
+ return new ModelAndView();
+ }
+
+
+ private ModelAndView handleNotFoundException(HttpServletResponse response, NotFoundException ex)
+ throws IOException {
+
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found. " + ex.getMessage());
+
+ return new ModelAndView();
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/WebExceptionHandler.java b/src/main/java/net/contargo/iris/api/WebExceptionHandler.java
new file mode 100644
index 00000000..bc5656c3
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/WebExceptionHandler.java
@@ -0,0 +1,120 @@
+package net.contargo.iris.api;
+
+import net.contargo.iris.security.UserAuthenticationService;
+
+import org.joda.time.DateTime;
+
+import org.slf4j.Logger;
+
+import org.springframework.security.core.Authentication;
+
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+
+import java.lang.invoke.MethodHandles;
+
+import java.util.Random;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+
+/**
+ * This ExceptionHandler generates detailed error reports if an unexpected error resp. Exception occurs.
+ *
+ * @author Aljona Murygina - murygina@synyx.de
+ * @author Oliver Messner - messner@synyx.de
+ */
+public class WebExceptionHandler extends SimpleMappingExceptionResolver implements HandlerExceptionResolver {
+
+ private static final Logger LOG = getLogger(MethodHandles.lookup().lookupClass());
+ private static final int MAX_RANDOM_NUMBER = 9999;
+
+ private final File basePath;
+ private final Random random = new Random();
+ private final UserAuthenticationService userAuthenticationService;
+
+ public WebExceptionHandler(File basePath, UserAuthenticationService userAuthenticationService) {
+
+ this.basePath = basePath;
+ this.userAuthenticationService = userAuthenticationService;
+
+ createDirectoryIfNotExists(basePath);
+ }
+
+ String textualRepresentationOfCurrentUser() {
+
+ Authentication currentUser = userAuthenticationService.getCurrentUser();
+
+ return currentUser == null ? "?" : String.format("user: %s%n", currentUser.getName());
+ }
+
+
+ @Override
+ public ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
+ Exception e) {
+
+ // generate unique id for exception
+ DateTime now = new DateTime();
+ String id = String.format("%s-%4d", now.toString("yyyy-MM-dd_HH-mm-ss"), random.nextInt(MAX_RANDOM_NUMBER));
+
+ request.setAttribute("exception_id", id);
+
+ String user = textualRepresentationOfCurrentUser();
+
+ addExceptionStackTraceToRequest(request, e);
+
+ File file = new File(basePath, id + "-report.txt");
+
+ try {
+ PrintWriter writer = new PrintWriter(file, "UTF-8");
+ writer.append(String.format(
+ "id: %s%ndate: %s%n%s%n" + "requesturl: %s%ncontroller: %s%n%n%n"
+ + "stacktrace:%n", id, now.toString("dd.mm.yyyy HH:mm:ss"), user,
+ request.getRequestURL().toString(), handler.getClass().getName()));
+
+ e.printStackTrace(writer);
+ writer.append("\n\n");
+
+ writer.close();
+ } catch (FileNotFoundException fileNotFoundException) {
+ LOG.warn("File not found.", fileNotFoundException);
+ } catch (UnsupportedEncodingException unsupportedEncodingException) {
+ LOG.warn("Encoding not supported.", unsupportedEncodingException);
+ }
+
+ LOG.error("A new error report was generated due to an exception. See " + file.getPath()
+ + " for further information.", e);
+
+ return super.doResolveException(request, response, handler, e);
+ }
+
+
+ private void addExceptionStackTraceToRequest(HttpServletRequest request, Exception exception) {
+
+ StringWriter writer = new StringWriter();
+ exception.printStackTrace(new PrintWriter(writer));
+ request.setAttribute("exception_trace", writer.toString());
+ }
+
+
+ private void createDirectoryIfNotExists(File dir) {
+
+ if (dir.exists() && !dir.isDirectory()) {
+ throw new IllegalStateException("Directory " + dir.getAbsolutePath()
+ + " exists and is a file (needed a directory)");
+ } else if (!dir.exists() && !dir.mkdirs()) {
+ throw new IllegalStateException("Directory " + dir.getAbsolutePath()
+ + " can not be created");
+ }
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/discover/DiscoverPublicApiController.java b/src/main/java/net/contargo/iris/api/discover/DiscoverPublicApiController.java
new file mode 100644
index 00000000..33fefc88
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/discover/DiscoverPublicApiController.java
@@ -0,0 +1,135 @@
+package net.contargo.iris.api.discover;
+
+import com.mangofactory.swagger.annotations.ApiIgnore;
+
+import net.contargo.iris.address.api.AddressApiController;
+import net.contargo.iris.api.AbstractController;
+import net.contargo.iris.connection.api.MainRunConnectionApiController;
+import net.contargo.iris.container.ContainerType;
+import net.contargo.iris.countries.api.CountriesApiController;
+import net.contargo.iris.enricher.api.RouteEnricherApiController;
+import net.contargo.iris.route.RouteCombo;
+import net.contargo.iris.seaport.api.SeaportApiController;
+import net.contargo.iris.terminal.api.TerminalApiController;
+
+import org.springframework.beans.factory.annotation.Value;
+
+import org.springframework.stereotype.Controller;
+
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import java.lang.reflect.Method;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
+
+
+/**
+ * Controller which presents all public API's as link so they can be used to move through the API.
+ *
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author David Schilling - schilling@synyx.de
+ */
+@Controller
+@RequestMapping
+@ApiIgnore
+public class DiscoverPublicApiController extends AbstractController {
+
+ static final String REL_COUNTRIES = COUNTRIES;
+ static final String REL_REVERSE_GEOCODE = REVERSE_GEOCODE;
+ static final String REL_GEOCODE = "geocode";
+ static final String REL_TERMINALS = TERMINALS;
+ static final String REL_TERMINAL_EXAMPLE = "terminal (by uid)";
+ static final String REL_SEAPORTS_OF_CONNECTIONS = "seaports (as part of connections)";
+ static final String REL_SEAPORTS_OF_CONNECTIONS_FILTERED = "seaports (as part of connections, filtered)";
+ static final String REL_SEAPORT_EXAMPLE = "seaport (by uid)";
+ static final String REL_CONNECTIONS = "connections_url";
+ static final String REL_SIMPLE_GEOCODES_EXAMPLE = "simplegeocodes_example";
+ static final String REL_ROUTE_DETAILS_EXAMPLE = "route_details_example";
+ static final String REL_OSM_ADDRESSES = OSM_ADDRESSES;
+
+ private static final String ROOT_URL = SLASH;
+ private static final Double SEAPORTS_LAT = 49.0;
+ private static final Double SEAPORTS_LON = 8.41;
+
+ private static final BigInteger SEAPORT_UID = new BigInteger("1301000000000001");
+ private static final BigInteger TERMINAL_UID = new BigInteger("1301000000000001");
+
+ @Value(value = "${application.version}")
+ private String applicationVersion;
+
+ @RequestMapping(value = ROOT_URL, method = RequestMethod.GET)
+ @ModelAttribute(RESPONSE)
+ public DiscoverResponse discover() throws NoSuchMethodException {
+
+ DiscoverResponse discoverResponse = new DiscoverResponse(applicationVersion);
+
+ // connections_url
+ discoverResponse.add(linkTo(
+ methodOn(MainRunConnectionApiController.class).getSeaportRoutes(SEAPORT_UID, SEAPORTS_LAT, SEAPORTS_LON,
+ true, ContainerType.TWENTY_LIGHT, false, RouteCombo.WATERWAY)).withRel(REL_CONNECTIONS));
+
+ // countries
+ discoverResponse.add(linkTo(CountriesApiController.class).withRel(REL_COUNTRIES));
+
+ // osmaddresses
+ discoverResponse.add(linkTo(AddressApiController.class).slash(OSM_ADDRESSES).slash(
+ "134631686?_=1381911583029").withRel(REL_OSM_ADDRESSES));
+
+ // reverse_geocode
+ Method method = AddressApiController.class.getMethod(AddressApiController.METHOD_ADDRESS_BY_GEOLOCATION,
+ BigDecimal.class, BigDecimal.class);
+ discoverResponse.add(linkTo(method, new BigDecimal("49.123"), new BigDecimal("8.12")).slash(".").withRel(
+ REL_REVERSE_GEOCODE));
+
+ // geocode
+ discoverResponse.add(linkTo(AddressApiController.class).slash(GEOCODES + "?city=Karlsruhe&postalcode=76137")
+ .withRel(REL_GEOCODE));
+
+ // seaport
+ discoverResponse.add(linkTo(methodOn(SeaportApiController.class).getSeaportById(SEAPORT_UID)).withRel(
+ REL_SEAPORT_EXAMPLE));
+ discoverResponse.add(linkTo(
+ methodOn(MainRunConnectionApiController.class).getSeaportsInConnections(RouteCombo.ALL)).withRel(
+ REL_SEAPORTS_OF_CONNECTIONS));
+ discoverResponse.add(linkTo(
+ methodOn(MainRunConnectionApiController.class).getSeaportsInConnections(RouteCombo.RAILWAY)).withRel(
+ REL_SEAPORTS_OF_CONNECTIONS_FILTERED));
+
+ // terminal (by uid)
+ discoverResponse.add(linkTo(methodOn(TerminalApiController.class).getTerminalByUid(TERMINAL_UID)).withRel(
+ REL_TERMINAL_EXAMPLE));
+
+ // terminals
+ discoverResponse.add(linkTo(methodOn(TerminalApiController.class).getTerminals()).withRel(REL_TERMINALS));
+
+ discoverResponse.add(linkTo(AddressApiController.class).slash(
+ "simplegeocodes?city=Karlsruhe&postalcode=76137").withRel(REL_SIMPLE_GEOCODES_EXAMPLE));
+
+ discoverResponse.add(linkTo(RouteEnricherApiController.class).slash(
+ "?data.parts[0].origin.longitude=4.3&data.parts[0].origin.latitude=51.36833"
+ + "&data.parts[0].destination.longitude=8.2852700000&data.parts[0].destination.latitude=49.0690300000"
+ + "&data.parts[0].routeType=BARGE&data.parts[0].containerType=TWENTY_LIGHT"
+ + "&data.parts[0].containerState=FULL"
+ + "&data.parts[1].origin.longitude=8.2852700000&data.parts[1].origin.latitude=49.0690300000"
+ + "&data.parts[1].destination.longitude=8.41&data.parts[1].destination.latitude=49.0"
+ + "&data.parts[1].routeType=TRUCK&data.parts[1].containerType=TWENTY_LIGHT"
+ + "&data.parts[1].containerState=FULL"
+ + "&data.parts[2].origin.longitude=8.41&data.parts[2].origin.latitude=49.0"
+ + "&data.parts[2].destination.longitude=8.2852700000&data.parts[2].destination.latitude=49.0690300000"
+ + "&data.parts[2].routeType=TRUCK&data.parts[2].containerType=TWENTY_LIGHT"
+ + "&data.parts[2].containerState=EMPTY"
+ + "&data.parts[3].origin.longitude=8.2852700000&data.parts[3].origin.latitude=49.0690300000"
+ + "&data.parts[3].destination.longitude=4.3&data.parts[3].destination.latitude=51.36833"
+ + "&data.parts[3].routeType=BARGE"
+ + "&data.parts[3].containerType=TWENTY_LIGHT&data.parts[3].containerState=EMPTY").withRel(
+ REL_ROUTE_DETAILS_EXAMPLE));
+
+ return discoverResponse;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/discover/DiscoverResponse.java b/src/main/java/net/contargo/iris/api/discover/DiscoverResponse.java
new file mode 100644
index 00000000..b4c942d5
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/discover/DiscoverResponse.java
@@ -0,0 +1,38 @@
+package net.contargo.iris.api.discover;
+
+import org.springframework.hateoas.ResourceSupport;
+
+
+/**
+ * Response Object which is used in the {@link DiscoverPublicApiController} to return the current version of the
+ * application.
+ *
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author David Schilling - schilling@synyx.de
+ */
+class DiscoverResponse extends ResourceSupport {
+
+ private String version;
+
+ public DiscoverResponse(String applicationVersion) {
+
+ this.version = applicationVersion;
+ }
+
+
+ public DiscoverResponse() {
+
+ // Needed for Jackson Mapping
+ }
+
+ public String getVersion() {
+
+ return version;
+ }
+
+
+ public void setVersion(String version) {
+
+ this.version = version;
+ }
+}
diff --git a/src/main/java/net/contargo/iris/api/discover/TestExplainView.java b/src/main/java/net/contargo/iris/api/discover/TestExplainView.java
new file mode 100644
index 00000000..92255197
--- /dev/null
+++ b/src/main/java/net/contargo/iris/api/discover/TestExplainView.java
@@ -0,0 +1,173 @@
+package net.contargo.iris.api.discover;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import net.contargo.iris.security.UserAuthenticationService;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+
+import org.springframework.context.ApplicationContextAware;
+
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.ResourceSupport;
+
+import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import java.net.URLDecoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.apache.commons.lang.CharEncoding.UTF_8;
+
+
+/**
+ * @author Marc Kannegiesser - kannegiesser@synyx.de
+ * @author David Schilling - schilling@synyx.de
+ */
+public class TestExplainView extends MappingJackson2JsonView implements ApplicationContextAware {
+
+ private final ObjectMapper objectMapper;
+
+ @Value(value = "${application.version}")
+ private String applicationVersion;
+
+ private final UserAuthenticationService userAuthenticationService;
+
+ @Autowired
+ public TestExplainView(UserAuthenticationService userAuthenticationService, ObjectMapper objectMapper) {
+
+ setContentType("text/html");
+ this.userAuthenticationService = userAuthenticationService;
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ protected void renderMergedOutputModel(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+
+ @SuppressWarnings("unchecked")
+ Map value = (Map) filterModel(model);
+
+ StringBuilder builder = new StringBuilder();
+
+ addGreetingSection(builder, request);
+
+ addFormatsSection(request, builder);
+
+ addLinkSection(builder, value);
+
+ addJSONPrettyPrintSection(response, value, builder);
+
+ response.getOutputStream().write(("" + applicationVersion).getBytes(UTF_8));
+ }
+
+
+ void addGreetingSection(StringBuilder builder, HttpServletRequest request) {
+
+ String username = userAuthenticationService.getCurrentUser().getName();
+ builder.append("
Hello ").append(username).append(" from ").append(request.getRemoteHost()).append("
");
+
+ String url = urlB.toString();
+
+ builder.append("You requested this page using url ").append(url).append(" and Accept-Header ").append(
+ accept).append("
");
+
+ builder.append("You requested this page as HTML (Probably because your client has text/html "
+ + "prior to application/json or application/xml in the Accept-Header.");
+ builder.append("If you are intersted in another format please adjust your Accept-Header "
+ + "or append .json or .xml at the url: