A simple solution for JSON-HAL object retrieval and caching.
- What is it?
- Features
- Example
- Usage
- What's that name about... π€?
- TODOs and future of the project
- Third-Party software
- Disclaimer
- License
It is a helper software (library) to make retrieving "JSON+HAL" objects (possibly from REST web services) easy. Additionally, it will optionally cache the objects retrieved, so there won't be subsequent HTTP calls for fetching one and the some object.
Another speciality of storestahl is, that is will automatically follow HAL-relations and retrieve not only singular objects, but complete object structures, if they are linked. The caching described above will also take place for these linked objects.
- Lightweight architecture, only one entry point
- Fully automatic cache handling - one URL, one object
- Extensive test set
- Customizable relation and caching settings
- Comprehensive documentation
Here is an example of what Storesthal can do for you, regarding an exemplary object structure.
Consider the following object structure:
Testparent
/ \
Testchild 1 Testchild 2
|
Test-Subchild 1
This structure matches with the following JSON+HAL data retrieved from a web service, e. g. at https://mygreatwebservice.com/api/
:
(Accessible at https://mygreatwebservice.com/api/parents/3
)
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/parents/3"},
"children": [{
"href": "https://mygreatwebservice.com/api/children/5"
},{
"href": "https://mygreatwebservice.com/api/children/14"
}]
},
"number": 3,
"comment": "Test",
"name": "Testparent"
}
(Accessible at https://mygreatwebservice.com/api/children/5
)
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/children/5"},
"parent": {"href": "https://mygreatwebservice.com/api/parents/3" },
"children": [{
"href": "https://mygreatwebservice.com/api/subchildren/99"
}]
},
"number": 5,
"comment": "Test",
"name": "Testchild 1"
}
(Accessible at https://mygreatwebservice.com/api/children/14
)
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/children/14"},
"parent": {"href": "https://mygreatwebservice.com/api/parents/3" },
"children": []
},
"number": 5,
"comment": "Test",
"name": "Testchild 2"
}
(Accessible at https://mygreatwebservice.com/api/subchildren/99
)
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/subchildren/99"},
"parent": {"href": "https://mygreatwebservice.com/api/children/5" },
"children": []
},
"number": 9,
"comment": "Test",
"name": "Test-Subchild 1"
}
(All of the objects, of course, having their individual attributes as well.)
If you use Storesthal to retrieve just the parent URL (https://mygreatwebservice.com/api/parents/3
), this is what will happen:
- The parent object is loaded from the parent URL and populated with its attributes
- Child object relations are automatically followed, retrieved and "attached" to the parent object
- Back-References (child β parent) are also handled correctly. This means, there will be only one instance of the particular parent object, linked to every child of it.
- Only one HTTP call will be made for the parent object (not one for every reference to it), it will from then be used from cache.
- The structure as depicted above will be returned by Storesthal (the parent object with its children and subschildren linked)
OK, it's not pure magic π. The Java classes for the objects must be existing, so there must be a class for the parent object and (at least) one for the child object. As the sub-child (in out example) has a structure equal to the one of the child, a specific sub-child object is not necessary at all.
The classes should have matching @HALRelation
annotations to let Storesthal know, which relation refers to which attribute. If these annotations are missing (working in "annotationless mode"), Storesthal will try its best to find these things out on its own.
Also, the classes should have senseful @Cacheable
annotations, so Storesthal will put the objects retrieved "into the right bucket". (Otherwise, a "general cache" for all objects is used or you can work without any caching).
You will find some examples below.
When designing Storesthal, special emphasis was placed on ease of usage and convention over configuration. Especially when using "annotationless mode" (see below) and the default configuration, it will "just work" for a lot of common use cases. Nevertheless, it's possible (and advised) to customize and fine-tune Storesthal's settings.
Explaining the usage of Storesthal, we will mainly stick to the example object structure given above.
In order to retrieve an object via Storesthal, you basically just need one call:
ParentObject Testparent = Storesthal.getObject("https://mygreatwebservice.com/api/parents/3", ParentObject.class);
Storesthal will then retrieve and examine the object found at the given URL and traverse the object structure as it discovers it and add matching sub-objects to every level of object relations. Especially, Storesthal is able to handle back-references and references to (yet) unknown or "incomplete" objects correctly! Additionally, Storesthal will use an object cache by default, making subsequent calls to the same URL performant.
There's one caveat using Storesthal: When intending to retrieve a collection of objects from the first level URL, getObject
isn't sufficient, you'll have to use getCollection
.
Example:
Let's say you want to retrieve the content from a JSON body like this:
[
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/characters/1"},
},
"name": "Mr. Spock"
},
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/characters/2"},
},
"name": "Commander Data"
},
{
"_links": {
"self": {"href": "https://mygreatwebservice.com/api/characters/3"},
},
"name": "Seven of Nine"
}
]
You would write something like ArrayList<Character> characters = getCollection("https://mygreatwebservice.com/api/characters", Character.class)
and get an ArrayList containing the three entries of the JSON array in return.
This is due to technical limitations of Java and perhaps also my own knowledge or creativity. π
Please note that this doesn't apply to any collections on any other level of the object hierarchy. They will be retrieved correctly in any way.
The separate objects within the collection will be treated like single objects retrieved by getObject
, what refers to caching, relation handling etc.
Please note, that in case of querying top-level collections, every member of the collection should have a valid self
-relation in order to make caching and relationship mechanisms work properly.
(When retrieving a top-level collection, Storesthal is just given one single URL for multiple object, so it can't determine the specific URL for every single object automatically.)
As stated above, Storesthal will automatically find and "attach" related objects to the one retrieved. For this to work, every object class needs to have
either a matching setter method (e. g., if the relation is called customer
in JSON, there must be a setCustomer
) or an arbitrary method (or attribute)
annotated with @HALRelation(name=...)
. So, if your setter is called setCustomer
, but the JSON relation is named cstmr
, you would need to use
@HALRelation(name="cstmr")
and could annotate any method with it, as long as it takes only one argument of the correct type.
The same scheme applies to any type of relation: 1 to 1, 1 to many, backreferences, ...
For convenience, it is also possible to annotate a class field with @HALRelation
. If doing so, Storesthal will again expect a setter method with the correspondent name to be
present in your class. So, if the annotated field is named studends
, there must be a method called setStudents
, accepting any java Collection
type as first and only parameter.
If that collection type is an abstract one, Storesthal will try to use an appropriate implementation, otherwise it will try and instantiate a new collection of the given type.
For details, see method handleCollection
in Storesthal.java
.
Please note, that - at least for the moment - Storesthal is not able to handle arrays instead of Collection
s.
If an object has a self
-relation, Storesthal will also take this into account concerning caching. Please see the note above concerning self
-relations when retrieving collections on first level.
One speciality about Storesthal is, that it brings along a simple, yet powerful, caching facility that comes out of the box. This does especially make sense, as caching is often very helpful (/ performance increasing / resource saving) when it comes to JSON+HAL object retrieval. Imagine you have 10.000 items each of which is linked to one of 20 categories. When loading the 10.000 items, it's not necessary to query one and the same category more than one single time and then "link" it to every item associated. This will not only speed up your application and save network and hardware resources, it will also lead to a more convenient and comprehensible in-memory object structure.
By default, nevertheless caching is not applied to any object, as Storesthal can not know beforehand, if there are fast-changing objects in your object structure, that might change very quickly and therefore shouldn't be cached. The only caching that takes place by default (and cannot be disabled) is the "intermediate caching" described below.
To add "rudimentary" caching to an object class, you would simply annotate it with @Cacheable
. Instances of classes with this annotation will then be cached automatically.
Anyway, it is suggested to use the cacheName
and cacheSize
attributes of the annotation as well: If you omit the cacheName
, the object will be stored in the "common" cache,
where any object of any kind will be stored, if no other cache name is given. Using a cache name, you have a better control about cache size and clearing of the cache. So, in the simple
example above, it should be best to annotate your item class with @Cacheable(cacheName="ItemCache")
and your category class with @Cacheable(cacheName="CategoryCache")
.
The cacheSize
attribute allows you to specify, how many object instances the cache will hold. We're using a LRU (last recently used) cache here, so if the cache is full, the object
last recently accessed will be evicted from it. This is not necessarily the object last recently added to the cache! If you use a cache name, you will also be able to clear the
whole Cache at once using the Storesthal.clearCache
function and supplying that cache name. All the other caches won't be touched.
Caching happens based on the URL of the object retrieved. This means, if caching is enabled, only the first request for http://my.web.service/api/cagetory/42 will really cause an HTTP call to that URL. Subsequent requests for the same URL will just retrieve the cached object from the memory as long it is not evicted from the cache or the cache is cleared.
There is one special cache, that can't be disabled: It's the intermediate cache. When traversing an object structure, it might occur that Storesthal finds one and the same related URI (and therefore object) multiple times. In these cases, all references but the first one are fetched from the intermediate cache - everything else would result in an inconsistent object structure as two references to the same URI would result in two different objects.
Please note, that the intermediate cache is only used within one single getObject
call. It will be cleared before the next one starts. So, it will probably be in use for a few seconds
(or probably less) only.
- Please make sure, your HTTP answer has the correct
Content-Type
set in its header:application/hal+json
(and possibly acharset=...
appended) . Otherwise, relations might not be found even though they are delivered correctly via_links
! (See (Non-HAL-answer retrieval)[#non-hal-answer-retrieval] below for alternatives.) - The standard demands a valid URL for each relation link. So,
"_links": {"child": {"href": null}}
is not allowed to indicate that there is no child object! In this case, there must not be any"child"
link, otherwise an error will occur.
Sometimes, you might need to retrieve non-HAL-answer from a remote web service. Storesthal supports this as well, even in combination with the caching mechanisms described.
I'll refer these non-HAL answers as "primitives" here, though String
is not a primitive in Java language sense.
There are four ways of retrieving primitives using Storesthal:
Method | Description | Example web service answer | Result |
---|---|---|---|
Storesthal.getInteger() |
Retrieves integer values | -56438 |
-56448 as an Integer |
Storesthal.getDouble() |
Retrieves double values | -53995.232 |
-53995.232 as Double |
Storesthal.getBoolean() |
Retrieves boolean values | true |
Boolean with true value |
Storesthal.getString() |
Retrieves string values | Hello, this is a string |
String containing Hello, this is a string value |
Please note, that the final conversion of the web service answer is done by Spring's RestTemplate.getForObject()
method,
so you might find further information there.
Each of the methods above has two overloaded methods for convenience, allowing you finer-grained caching control:
get[Integer|Doouble|Boolean|String](String url)
: The method call with just the web service URL as parameter will return the desired primitive without any caching.get[Integer|Doouble|Boolean|String](String url, boolean doCache)
: IfdoCache
istrue
, the caching mechanisms will be applied and the answer will be cached in the cache denoted byStoresthal.COMMON_CACHE_NAME
.get[Integer|Doouble|Boolean|String](String url, String cacheName)
: IfcacheName
is notnull
, the caching mechanisms will be applied and the answer will be cached in the cache denoted bycacheName
, otherwise the cache denoted byStoresthal.COMMON_CACHE_NAME
will be used.
I'm not a very creative person when it comes to such things... π It's just a word composed of "Store", "REST" and "HAL". :simple_smile:
For the moment, Storesthal is quite sufficient for my personal needs, so major changes or improvements are not planned with high priority. Anyway, I'll try and keep the project alive and well-maintained. Please feel free to use GitHubs possibilities to submit issues, feature requests or pull requests! :simple_smile:
If the project will have some kind of a bigger impact (I'm aware it will never be a real big thing ^^), it is more likely for me to continue working on it, as otherwise this would just be "for my private amusement".
- I love Spring Boot and would like to integrate Storesthal better with it, perhaps even as kind of a plug-in. But I have not had the time to investigate on how to do this up to now.
- See the "Issues" tab :simple_smile:
Storesthal makes heavy use of third-party software and libraries during the build process as well as during runtime. A detailed list of the third-party dependencies can be found
in build.gradle
.
Different license terms may apply to this software packages and must be considered before usage. There is no relation between the author(s) of Storesthal and the people or companies supplying third-party software. These packages are - gratefully! - used within Storesthal, but not maintained, merchandised, licensed or anything else by Storesthals author(s).
This program is free software. It comes without any warranty, not even for merchantability or fitness for a particular purpose.
Another disclaimer may apply to third-party software included in Storesthal, please see the respective license models.
Please see the License section for more details.
Storesthal is licensed und the terms of the GNU Lesser General Public License (LPGL). Please see LICENSE.md for details.
Please consider the information in the "Third-Party software" and "Disclaimer" sections also.