GithubHelp home page GithubHelp logo

spring-projects / spring-hateoas Goto Github PK

View Code? Open in Web Editor NEW
1.0K 102.0 474.0 7.11 MB

Spring HATEOAS - Library to support implementing representations for hyper-text driven REST web services.

Home Page: https://spring.io/projects/spring-hateoas

License: Apache License 2.0

Java 98.45% Shell 0.31% Kotlin 1.24%

spring-hateoas's Introduction

ga snapshot spring hateoas

icon?job=spring hateoas%2Fmain&subject=main icon?job=spring hateoas%2F1.2.x&subject=1.2 icon?job=spring hateoas%2F1.1.x&subject=1.1 icon?job=spring hateoas%2F1.0.x&subject=1.0 icon?job=spring hateoas%2F0.25.x&subject=0.25

Spring HATEOAS

This project provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly.

Working with Spring HATEOAS

Since all commits are headlined with its github issue, git will treat it as a comment. To get around this, apply the following configuration to your clone:

git config core.commentchar "/"

Making a release

  1. Create a new release (on the main branch).

    % ci/create-release.sh <release ticket without hash> <release version> <next snapshot version>
  2. With the release tagged, push the tagged version to the release branch.

    % git checkout -b release
    % git reset --hard <tag>
    % git push -f origin release
Note
You can chain the previous set of commands together using &&.

The pipeline will build and release the "release" branch. It will also build a new snapshot and stage it on artifactory. And if it’s a .RELEASE, the pipeline will push it out to Maven Central. To complete a release on Maven Central, you must login to the server shown in pom.xml, close, and release. This is a stop gap to guard against bad releases accidentally getting pushed out to Maven Central.

Running CI tasks locally

Since the pipeline uses Docker, it’s easy to:

  • Debug what went wrong on your local machine.

  • Test out a a tweak to your test.sh script before sending it out.

  • Experiment against a new image before submitting your pull request.

All of these use cases are great reasons to essentially run what Jenkins does on your local machine.

Important
To do this you must have Docker installed on your machine.
  1. docker run -it --mount type=bind,source="$(pwd)",target=/spring-hateoas-github adoptopenjdk/openjdk8:latest /bin/bash

    This will launch the Docker image and mount your source code at spring-hateoas-github.

  2. cd spring-hateoas-github

    Next, run the test.sh script from inside the container:

  3. PROFILE=none ci/test.sh

Since the container is binding to your source, you can make edits from your IDE and continue to run build jobs.

If you need to test the build.sh script, then do this:

  1. docker run -it --mount type=bind,source="$(pwd)",target=/spring-hateoas-github --mount type=bind,source="/tmp/spring-hateoas-artifactory",target=/spring-hateoas-artifactory adoptopenjdk/openjdk8:latest /bin/bash

    This will launch the Docker image and mount your source code at spring-hateoas-github and the temporary artifactory output directory at spring-hateoas-artifactory.

    Next, run the build.sh script from inside the container:

  2. ci/build.sh

Important
build.sh will attempt to push to Artifactory. If you don’t supply credentials, it will fail.
Note
Docker containers can eat up disk space fast! From time to time, run docker system prune to clean out old images.

spring-hateoas's People

Contributors

alza-bitz avatar darklynx avatar dschulten avatar gregturn avatar hibaymj avatar izeye avatar laures avatar luvarqpp avatar lyca avatar martinotten avatar michael-wirth avatar mikerocke avatar mp911de avatar mschout avatar odrotbohm avatar patouche avatar pnowy avatar reda-alaoui avatar remonsinnema avatar rgladwell avatar rolandkrueger avatar rolandkulcsar avatar ronnie76er avatar rweisleder avatar schauder avatar spring-builds avatar spring-operator avatar sylvain-maillard avatar tgeens avatar vpavic avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spring-hateoas's Issues

StackOverFlow exception in ControllerEntityLinks.linkFor

I guess the code should be

@Override
public LinkBuilder linkFor(Class<?> entity, Object... parameters) {
  Class<?> controllerClass = entityToController.get(entity);
  return linkBuilderFactory.linkTo(controllerClass, parameters);
}

Add @EnableHypermediaSupport to default configuration

The currently available hypermedia support (e.g. LinkDiscoverer, Jackson modules to render HAL etc.) has to be configured manually. So if you'd like to work with HAL as hypermedia format you have to configure the according LinkDiscoverer, customize the ObjectMapper setup etc.

To ease that we should expose an @EnableHypermediaSupport annotation exposing an attribute to configure the supported hypermedia type triggering the registration of the necessary components accordingly.

Links in HTTP header

It would be nice to have an option to add all/partial links to HTTP link header instead of embedded in resource body. Or does spring-hateoas take approach of preferring embedded links over HTTP headers?

Allow ResourceAssemberSupport to be used without requiring domain entities to implement Identifiable

I think it would be beneficial if ResourceAssemberSupport could be used without the dependency on Identifiable for entities, personally I am using Spring Data and @id annotations to avoid implementing this kind of interface; I'm sure there will be other potential users of this library who might wish to avoid a dependency on a framework class such as Identifiable, in their domain models.

I will provide a pull request with a suggested implementation/solution for the above, most likely just extracting a parent class of ResourceAssemberSupport and renaming ResourceAssemblerSupport to something like, ResourceAssemberIdentifiableSupport.

Thanks

Support for automatic creation of forms

As a service implementer, I want to use existing information about a controller method to create a forms-like response which points to the controller method, so that my clients need no out-of-band information in order to build a suitable request.

As long as a server can give expanded links to the client like example.org/persons/12345, it is easy to avoid out-of-band information.

However, if the server needs input from the client, expanded links are not enough. The server can respond with non-expanded URI templates [0] where the client must fill out the placeholders, as in example.org/persons/{personId}. But there is no standard media type to return such templates yet, and the client must be programmed to expand them. There are implementations for this [1], but when it comes to required request bodies for a POST or a PUT, a URI template is not enough anyway.

If the server expects GET request parameters with defaults for some parameters, or restrictions for valid parameters, or if it needs to tell the client that a POST to /persons is needed and how the POST body should look like, a media type is needed which allows to give this information to the client.

Currently only few media types are capable of this. Examples:

  • text/html (Jon Moore's Oredev talk)
  • application/vnd.siren+json [2]

The Spring MVC controller methods contain all necessary information how a form should look like. Leveraging the new methodOn() idiom for controller methods, it is possible to create a form like this [3]:

@RequestMapping(value = "/customer", method = RequestMethod.GET)
public HttpEntity<FormDescriptor> searchPersonForm() {
    FormDescriptor form = ControllerFormBuilder.createForm("searchPerson",
        methodOn(PersonController.class).showPerson(1L));
    return new HttpEntity<FormDescriptor>(form);
}

The server can be configured to render the Response as an html form using an HtmlFormMessageConverter[4] if the client accepts text/html, and we could create other converters for siren etc. I think for now it would be perfectly OK to respond with html forms for complicated requests and with json/hal/collection+json if we only need expanded links.

The advantages are:

  • The client does not need to learn out-of-band how to construct a request for a POST or parameterized GET, there is a standard for this. We only need a client framework which is able to handle a standard media type such as an html form, or other emerging standards like siren which define a possible action for the client. Spring-hateoas could easily offer such a client framework [5]
  • The server can evolve quite independently from the client, e.g. by adding default values for new request parameters which old clients can submit as-is
  • Developers can "browse the API" to find out which requests the client must use and program the client against rels, form names and field names which come with the response.

Prepared pull request #38 with tests, strictly focused on html form support ;-)

Feedback welcome.

[0] http://tools.ietf.org/html/rfc6570
[1] https://github.com/damnhandy/Handy-URI-Templates
[2] https://github.com/kevinswiber/siren
[3] https://github.com/dschulten/spring-hateoas/blob/master/src/main/java/org/springframework/hateoas/HtmlFormMessageConverter.java
[4] https://github.com/dschulten/spring-hateoas-rest-service-sample/blob/master/rest-service/src/main/java/de/escalon/rest/PersonController.java
[5] https://github.com/dschulten/spring-hateoas-rest-service-sample/tree/master/spring-hateoas-client

Add basic support for RFC5988 style link headers

RFC5988 defines a Link header and the expected format of link representations in headers. We should make sure our Link abstraction can be easily created from a link header of that format and written back into a header string.

The support for this might need the addition of a Links container as the RFC allows multiple values being present in the Link header. Also we'll currently support the rel attribute only as our Link abstraction is limited to that one.

We should make sure the design allows the usage of our Link and Links types in Spring MVC controllers such as:

@Controller
class SampleController {

  @RequestMapping("/path")
  HttpEntity<?> someMethod(@RequestHeader("Link") Links links) {
    …
  }
}

Hal support is incomplete

My experiments with the Hal support gave me this for a Resource having an attached Resources object. The Resource is Thorin, his belongings are the embedded Resources. I added a describedBy rel to each Resource and to Resources just for demonstration purposes.

{
    "firstname": "Thorin",
    "lastname": "Oakenshield1",
    "products": {
        "content": [
            {
                "productName": "Orcrist",
                "_links": {
                    "self": {
                        "rel": "self",
                        "href": "http://localhost:8080/products/1"
                    },
                    "describedBy": {
                        "rel": "describedBy",
                        "href": "http://example.com/doc#product"
                    }
                }
            }
        ],
        "_links": {
            "describedBy": {
                "rel": "describedBy",
                "href": "http://example.com/doc#products"
            }
        }
    },
    "_links": {
        "self": {
            "rel": "self",
            "href": "http://localhost:8080/people/1"
        },
        "describedBy": {
            "rel": "describedBy",
            "href": "http://example.com/doc#customer"
        }
    }
}

This is not the correct HAL representation of a resource with embedded resources. The correct representation should be:

{
    "firstname": "Thorin",
    "lastname": "Oakenshield1",
    "_embedded": {
        "products": [
            {
                "productName": "Orcrist",
                "_links": {
                    "self": { "href": "http://localhost:8080/products/1"},
                    "describedBy": {"href": "http://example.com/doc#product"}
                }
            }
        ],
        "_links": {
            "describedBy": {"href": "http://example.com/doc#products"}
        }
    },
    "_links": {
        "self": {"href": "http://localhost:8080/people/1"},
        "describedBy": {"href": "http://example.com/doc#customer"}
    }
}

For decent HAL support we would also need templated URIs, for something like:

"find": { "href": "/orders{?id}", "templated": true }

Resource<T> in RestTemplate's getForObject

I have a hard time figuring out how to use the Resource wrapper in Spring's RestTemplate's get/put/postForObject methods, where the second param is Class<T> responseType.

Of course this may be a missing feature in RestTemplate, but I just wonder if you have any workarounds.

Special characters in path variables are escaped multiple times when added with slash()

I'm trying to use a URI-encoded value as a path segment value when constructing a link with LinkBuilder's slash() method, but I find that any 'special' characters (such as %) will be repeatedly double-encoded by each subsequent slash() call.

For example:

linkToCurrentMapping().slash("first")
    .slash("sec%ond").slash("thi%rd")
    .slash("fourth").withSelfRel();

Should return http://localhost/first/sec%25ond/thi%25rd/fourth but gets http://localhost/first/sec%252525ond/thi%2525rd/fourth

I think that this is being caused by LinkBuilderSupport's interaction with HierarchicalUriComponentBuilder.toUri and URI (which always encodes the path).

Note that I'm actually trying to percent-encode a path as a path segment value, but if I don't explicitly percent-encode slashes in the path, then they are not encoded at all:

linkToCurrentMapping().slash("first")
    .slash("sec/ond").slash("third").withSelfRel();

I would like http://localhost/first/sec%2Fond/third/fourth but get http://localhost/first/sec/ond/third (due to the StringUtils.tokenizeToStringArray(object.toString(), "/") call in LinkBuilderSupport.slash).

Here's the full unit test: https://gist.github.com/4436408

Add SPI to let HandlerMethodArgumentResolver implementations contribute to URI building

HandlerMethodArgumentResolver implementations are usually used to automatically populate special types being used in Spring MVC controller methods. For example, imagine there is an implementation that allows pulling request parameters into a Pageable implementation (taken from Spring Data).

@Controller
class PeopleController {

  @RequestMapping("/people")
  HttpEntity<?> showPeople(Pageable pageable) { … }
}

This implementation might pull request attributes page and size from the request. Now when using the MethodLinkBuilderFactory API, you might want to create a link pointing to exactly this method here:

Pageable pageable = new PageRequest(0, 10);
Link link = linkBuilderFactory.linkTo(
  methodOn(PeopleController.class).showPeople(pageable)).
  withRel("people");

As the request attributes required to be present when extracting the Pageable from the request, we can also leverage the knowledge to build the appropriate URI, e.g. /people?page=0&size=10.

We should create an SPI that can be used to hook HandlerMethodArgumentResolvers into the link creation.

Add support for request parameters to Link builder

i want to add "next" and "previous" links to collection resources. i can create a link to the controller that returns the next page, but i can not configure the required parameters (in my case: limit and offset)

currently i use the builder for the base uri and add the parameters as string. not the best solution.

Support arbitrary path appends with LinkBuilder

If i'm violating a best practice or design principle, by all means, let me know. On occasion, I find a need to have an arbitrary path that doesn't necessarily represent an entity. For example, let's say I have an API for files.

Call GET on /api/files/{id} and a JSON representation of the file's metadata is returned.

Call GET on /api/files/{id}/data and the file is actually sent to the client as application/octet-stream or whatever content type is appropriate.

I'd like to include a link to /api/files/{id}/data in the /api/files/{id} result.

Getting java.net.ConnectException: Connection timed out and java.lang.ClassNotFoundException: org.springframework.web.util.UriComponentsBuilder exception

Hi,
whenever I am using ControllerLinkBuilder to create the link I am getting Getting java.net.ConnectException: Connection timed out and java.lang.ClassNotFoundException: org.springframework.web.util.UriComponentsBuilder exception. I am gving the code snippet below, any help will be appreciable.

public @ResponseBody
UserDelegationBean getLoggedinUserRoles(HttpServletRequest request,
        HttpServletResponse response) throws IOException,
        SessionExpiredException {

        Link link1 = linkTo(UserDelegationController.class).withRel("people");
        userBean.setLink(link1);
    return userDelegationBean;
}

ResourceSupport.getId() too restrictive

Having ResourceSupport.getId() method return Link is too restrictive IMO. It makes my representation unable to have id field and corresponding getId() method with different type. It seems a very common use case to assume in UI that the id field of resource would return actual id of the resource and not a link.
Probably ResourceSupport could also be made generic where the id return type is passed by the class extending ResourceSupport.

Add HAL support for PagedResources

Originally by @ericbottard:

OTOH, as stated above, this is in the context of PagedResources <-- Resources which have a "content" property.
Maybe the serialization of that should not change name according to the target type (as this would prevent client systems from knowing how to extract the content from a list in a systematic way, for example).

HAL : Wrong rel name for embedded collections

Context:

  • I have a resource with an embedded collection of "templates" (actually this is for pagination support, so resource contains pagination metadata + actual items).
  • I have customized relation names with @relation(value="template", collectionRelation="templates")

This is what I get :

{
  "_links": {
    "self": {
      "href": "...."
    }
  },
  "_embedded": {
    "template": [
      {
        "name": "...",
        "_links": {
          "self": {
            "..."
          }
        }
      },
      {
        "name": "...",
        "_links": {
          "self": {
            "..."
          }
        }
      }
    ]
  },
  "page": {
    "size": 10,
    "totalElements": 2,
    "totalPages": 1,
    "number": 0
  }
}

Now, have a look at the _embedded entry, it should read "templates", and not "template".
I believe the issue lies here although it may be elsewhere as well (Jackson1, etc)

Support formatting of @PathVariables

Currently the ControllerLinkBuilder.linkTo(Object invocationValue) method uses the AnnotatedParametersParameterAccessor.getBoundParameters(…)' method to find all thePathVariableson a method invocation and put them in a map for substitution into theUriTemplate`.

It doesn't seem to take account of any converters or formatters which are configured and would be applied by the request handler.

For example if you annotate a parameter

@PathVariable @DateTimeFormat(pattern = "yyyyMMdd") DateTime from

then the link builder just outputs the plain `toString()' version of the date rather than the formatted version for that part of the URL.

Controller Level Request Mapping with Path Variable

Hi,
I am having difficulties to use a controller level request mapping with path variable. It seems to be supported by linkTo(Controller.class, Object... args) which is part of 0.3.0, but the resource is not found.

If I have a controller

@Controller
@RequestMapping("/people/{personId}/products")
public class PersonProductController {

@RequestMapping(value = "/", method = RequestMethod.GET)
    public HttpEntity<List<ProductResource>> getProductsOfPerson(@PathVariable Long personId) {
    ...
}
}

and I link to the controller using linkTo with args like this:

Link personProducts = linkTo(PersonProductController.class, 1L).withRel("personProducts");

then the response creates this link which seems to be correct, but it gives me a 404.

{
"rel":"personProducts", 
"href":"http://localhost:8080/people/1/products"
},

Is it possible that in Spring you cannot use path variables on the controller level? Or did I get the usage wrong?

Best regards,
Dietrich

HalJacksonHttpMessageConverter and HalJackson2HttpMessageConverter

How is the HAL support meant to be used? My expectation would be that the client should tell the server that it Accepts the HAL mediatype (application/hal+json or application/hal+xml), and that the server should answer accordingly.
The current implementation allows us to say

mapper.registerModule(new Jackson1HalModule());

in order to render the json response with HAL style _links etc. instead of the standard Resource and Resources objects. I do not see a way to let the client control this, is it missing? If this is missing, how should we go about it? A simple way could be subtypes of MappingJacksonHttpMessageConverter and MappingJackson2HttpMessageConverter which contain this configuration for the object mapper. Then the implementers could choose to add the media type they want to use in the spring configuration.

ControllerLinkBuilder should be extensible

Current implementation of ControllerLinkBuilder relies on Spring's ServletUriComponentsBuilder to construct links. The problem with ServletUriComponentsBuilder is that it is poorly designed (uses only static method calls), eliminating the possibility to extend and enhance the behaviour.

One typical scenario that I discovered yesterday:

Our tomcat server is hosted behind Apache. The client (browser) accesses a URL using www.example.com/path/to/resource. However, because of the way Apache has been set up, it doesn't send along the Host header value of www.example.com to Tomcat. Instead, it sends the internal IP address. The original hostname of www.example.com is set in an X-Forwarded-Host header.

Now, because Spring's ServletUriComponentsBuilder uses request.getServerName() to construct the hostname part of the URL, any generated URLs will have the internal IP address instead of www.example.com.

The only way I can fix this currently is by either:

  • Creating a HttpServletRequestWrapper that takes the value of X-Forwarded-Host header and sets it to Host header.

OR

  • Rewriting ControllerLinkBuilder logic to not use Spring's ServletUriComponentsBuilder.

Add to Maven Central

I would be nice if this API were available from the Maven Central repo, it doesn't appear to have been uploaded there.

Adding support for a hateoas enabled client

The promise of hateoas is to decouple clients from rest api changes. Standards for this seem to be evolving right now:

http://tools.ietf.org/html/rfc6570 defines how to create URI templates which contain variables or get parameters (March 2012)

In http://tools.ietf.org/html/draft-nottingham-json-home-00 Mark Nottingham comes up with a home document which tells clients how to use the api on a home document, it contains URI templates for the resources.

What is your take on this? Do you plan to add support for hateoas clients which need not be changed if the hateoas-enabled api changes? What should such a support look like?

In general, what would you recommend if someone wants to build a hateoas client based on spring-hateoas resources?

Best regards,
Dietrich

Make source encoding explicit in pom.xml

Current pom.xml does not specify source code encoding. This results in platform-specific builds:

[WARNING] Source files encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!

Adding property <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> to pom will make source encoding explicit and allows repeatable builds across platforms with different default encodings.

Will provide patch shortly.

Make Resource and Resources a general abstraction for hypermedia resources

While experimenting with the Hal support it occured to me, that maybe we should re-focus the current approach to providing both our own and third-party hypermedia content types. The current Resource classes with their json links and content attributes are in fact yet another way of adding hypermedia controls to json, IOW we are establishing yet another hypermedia content-type, without defining a proper content-type for it (at least not yet).
Maybe we should use Resource and Resources as an abstraction which is meant to be represented in more widely-used hypermedia formats (atom, hal, siren, collection+json, json-ld, xhtml) - as requested by the client via Accept. I think this is what has just started by adding Hal. Enabling people to build abstract hypermedia resources and making the representation a matter of choice or taste would be the correct thing to do IMO. We could provide MessageConverters for the media types.

To support these media types better, Link seems to need these extensions:

Resource and Resources could be extended by the following properties:

  • actions (used by xhtml forms, Siren)
  • context (e.g. used by json-ld)

or maybe simply a facility to add arbitrary Object properties to it, so that people can add data to the Resource as needed.

I want to find out in which direction you are going here. We can then create single User stories and add these extensions one at a time.

Best regards
Dietrich

ResourceSupport.getId() shadows properties named "id" on content objects

because of the naming. because of a known bug (that concernes jsonUnwrap) in jackson it is not possible to create an "id" property on model objects that are wrapped into resources. the resourceSupport.getId will shadow the model property getId.

you can successfuly serialize such resources but you can not deserialize them. jackson will simply ignore the "id" property because it is annotated with jsonignore on the resourcesupport class.

the easy workaround is to rename the model property into something other than "id". but thats a bit annoying.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.