Giter Club home page Giter Club logo

force-rest-api's Introduction

Salesforce REST API Connector

Lightweight library for building Salesforce apps with OAuth authentication and data access through the Salesforce REST API.

Usage

Releases are published on Maven Central. Include in your project with:

<dependency>
    <groupId>com.frejo</groupId>
    <artifactId>force-rest-api</artifactId>
    <version>0.0.45</version>
</dependency>

Build and link locally

$ git clone https://github.com/jesperfj/force-rest-api.git
$ cd force-rest-api
$ mvn install -DskipTests

The version number is never updated in SCM. So builds will always produce a module with version 0-SNAPSHOT. Add it as a dependency to your local builds with:

<dependency>
    <groupId>com.frejo</groupId>
    <artifactId>force-rest-api</artifactId>
    <version>0-SNAPSHOT</version>
</dependency>

To check out the source code for a particular version found in Maven Central, use the corresponding git tag, e.g:

 $ git clone https://github.com/jesperfj/force-rest-api.git
 $ cd force-rest-api
 $ git checkout force-rest-api-0.0.41

Authentication and Instantiation

API versions

Salesforce updates its API version with every Salesforce release (3 times per year). The new version is supposed to always be backwards compatible, so in theory it is safe to always use the latest API version. However force-rest-api is designed to be conservative. The API version used may change with new versions of the library, but for a given version of the library, the version will always be ApiVersion.DEFAULT_VERSION unless you explicitly set it to something different. You set the API version when you instantiate an ApiConfig:

ApiConfig mycfg = new ApiConfig().setApiVersionString("v99.0");

You can also use the ApiVersion enum to set the version:

ApiConfig mycfg = new ApiConfig().setApiVersion(ApiVersion.V38);

But the enum may not always have the version you need and there is no particular benefit to using it compared to using a simple String.

Username / Password Authentication

Authenticate using just login and password:

ForceApi api = new ForceApi(new ApiConfig()
    .setUsername("[email protected]")
    .setPassword("password"));

OAuth Username/Password Authentication Flow

As documented here

ForceApi api = new ForceApi(new ApiConfig()
    .setUsername("[email protected]")
    .setPassword("password")
    .setClientId("longclientidalphanumstring")
    .setClientSecret("notsolongnumeric"));

OAuth Web Server Flow

As documented here

String url = Auth.startOAuthWebServerFlow(new AuthorizationRequest()
	.apiConfig(new ApiConfig()
		.setClientId("longclientidalphanumstring")
		.setRedirectURI("https://myapp.mydomain.com/oauth"))
	.state("mystate"));

// redirect browser to url
// Browser will get redirected back to your app after user authentication at
// https://myapp.mydomain.com/oauth with a code parameter. Now do:

ApiSession s = Auth.completeOAuthWebServerFlow(new AuthorizationResponse()
	.apiConfig(new ApiConfig()
		.setClientId("longclientidalphanumstring")
		.setClientSecret("notsolongnumeric")
		.setRedirectURI("https://myapp.mydomain.com/oauth"))
	.code("alphanumericstringpassedbackinbrowserrequest"));

ForceApi api = new ForceApi(s.getApiConfig(),s);

Instantiate with existing accessToken and endpoint

If you already have an access token and endpoint (e.g. from a cookie), you can pass an ApiSession instance to ForceApi:

ApiConfig c = new ApiConfig()
    .setRefreshToken("refreshtoken")
    .setClientId("longclientidalphanumstring")
    .setClientSecret("notsolongnumeric"),

ApiSession s = new ApiSession()
    .setAccessToken("accessToken")
    .setApiEndpoint("apiEndpoint");

ForceApi api = new ForceApi(c,s);

CRUD and Query Operations

Get an SObject

Account res = api.getSObject("Account", "001D000000INjVe").as(Account.class);

This assumes you have an Account class defined with proper Jackson deserialization annotations. For example:

import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown=true)
public class Account {

	@JsonProperty(value="Id")
	String id;
	@JsonProperty(value="Name")
	String name;
	@JsonProperty(value="AnnualRevenue")
	private Double annualRevenue;
	@JsonProperty(value="externalId__c")
	String externalId;

	public String getId() { return id; }
	public void setId(String id) { this.id = id; }
	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
	public Double getAnnualRevenue() { return annualRevenue; }
	public void setAnnualRevenue(Double value) { annualRevenue = value; }
	public String getExternalId() { return externalId; }
	public void setExternalId(String externalId) { this.externalId = externalId; }
}

Create SObject

Account a = new Account();
a.setName("Test account");
String id = api.createSObject("account", a);

Update SObject

a.setName("Updated Test Account");
api.updateSObject("account", id, a);

Create or Update SObject

a = new Account();
a.setName("Perhaps existing account");
a.setAnnualRevenue(3141592.65);
api.createOrUpdateSObject("account", "externalId__c", "1234", a);

Delete an SObject

api.deleteSObject("account", id);

Query SObjects

QueryResult<Account> res = api.query("SELECT id FROM Account WHERE name LIKE 'Test account%'", Account.class);

CRUD operations on root path

Sometimes you want to do CRUD operations without the standard /services/data/<version> path prefix. To do this you can get a ForceApi instance that uses root path:

ForceApi api = new ForceApi(myConfig,mySession);
api.rootPath().get("/services/apexrest/myApexClass");

rootPath() returns a new ForceApi instance that uses root path for the get(), delete(), put(), post(), patch() and request() methods.

Working with API versions

You can inspect supported API versions and get more detailed info for each version using SupportedVersions:

SupportedVersions versions = api.getSupportedVersions();
System.out.println(versions.oldest());          // prints v20.0
System.out.println(versions.contains("v25.0")); // prints true

The set of supported versions may vary based on where your organization is located. New versions are introduced 3 times a year and are rolled out gradually. During the rollout period, some organizations will have the latest version while others will not. The oldest supported version for REST API is v20.0. Salesforce API versions go further back than v20.0, but REST API does not support those older versions.

There is a direct mapping between season/year and version numbers. You can translate between season/year and version number in this way:

ExtendedApiVersion v = new ExtendedApiversion(ExtendedApiVersion.Season.SPRING, 2012);
System.out.println(v.getVersionString());       // prints v21.0

ExtendedApiVersion is called "Extended" because it goes beyond what ApiVersion offers and can represent more details about an API version, e.g. its season, year and URL base.

Run Tests

This project has a mix of unit tests and integration tests that hit the actual API. To make the integration tests work you must set up a proper test fixture and reference it from environment variables. .testenv.sample contains a sample shell script indicating what variables must be set. Copy it to .testenv and once you have all the correct values, set it in the environment by running the shell command:

source .testenv

Login and password

You need credentials to a Force.com developer org to run tests against. These go in the username and password vaiables. Needless to say, don't use credentials for a production org containing sensitive data. If you don't have a developer org, sign up for one. It's free. Remember to append the security token to your chosen password in the password variable.

Client ID and Secret

Once you have signed up for an org, create a Connected App:

  • In Setup, type "App" into Quick Find and select "App Manager"
  • Click "New Connected App" in the upper right corner.
  • Choose any name for your application
  • Choose any callback URL (you'll need to set this properly when web server flow is supported)
  • Choose some contact email
  • Click "Save"
  • Copy "Consumer Key" and set as the clientId environment variable
  • Click on "Click to reveal" and copy "Consumer Secret" and set as the clientSecret environment variable

Add externalId__c to Account SObject

Use the Force.com Web UI to add a custom field called externalId__c and mark it as an external ID field:

  • (sorry, you have to figure out how to do this yourself. Will add instructions or automate it later)

Create a second user for IP restrictions test

To test IP restrictions failure handling you need additional test setup:

  • Go to Manage Users --> Profiles and create a new profile based on "Standard Platform User". Call it "IP Restricted User"
  • Set Login IP Ranges for the new profile to something obscure like 1.1.1.1-1.1.1.1. Hit save and confirm that it's ok even though your user is not logged in from this range.
  • Create a new user and reset password
  • Log in as the new user and generate a security token
  • Set username in the iprestrictedUsername env var and password (with token appended) in the iprestrictedPassword env var.
  • Log back in with the admin user and go to Manage Users --> Profiles
  • TODO: complete these instructions

Run Tests

Before running the whole test suite, it is a good idea to run a single login test to check if the configuration is correct. If the username/password is not configured correctly, the test suite will trigger an account lock-out due to the many failed attempts. Run a single test such as testSoapLogin with:

mvn -Dtest=com.force.api.AuthTest#testSoapLogin test

Now run tests with

$ mvn test

You will see some log messages that look like errors or warnings. That's expected and does not indicate test failures. You can add debug logging with:

$ mvn test -Dorg.slf4j.simpleLogger.defaultLogLevel=debug

Interactive end-to-end OAuth handshake Test

This test is not run as part of the test suite because it requires manual intervention. Run it like this:

mvn -Dtest=com.force.api.EndToEndOAuthFlowExample test

Cutting a Release

This project now uses Alex Fontaine's release process because the release plugin is a pretty insane piece of software that should never exist. The pom.xml version number checked into SCM is always 0-SNAPSHOT. Mapping releases back to source code now relies on git tags only.

The project is set up to release to Maven Central. If you have forked it and want to deploy your own version, you will need to update groupId and set up your own Sonatype credentials and GPG. Assuming this is all correctly set up. Here's how you cut a new release:

First ensure all your code is checked in (with git status or the like). Then run tests one extra time and also test javadoc generation since it's easy to introduce errors in javadoc comments that will break the deploy:

$ mvn test javadoc:javadoc

Note. You must have JAVA_HOME set for this to succeed. On Mac, set it with

$ export JAVA_HOME=$(/usr/libexec/java_home)

Now find the latest version number with git tag (or in Maven central depending on what you trust most). Bump the version number to that plus one:

$ mvn versions:set -DnewVersion=<new-version>

For example:

$ mvn versions:set -DnewVersion=0.0.50

This will update pom.xml locally to the new version and leaving it uncommitted (which is what you want). Now run

$ mvn scm:tag

This tags the local and remote repository with the full module name, e.g. force-rest-api-0.0.50. Now deploy:

$ mvn clean deploy -DperformRelease

That command will fail if you don't have gpg installed. Install on MacOS with

$ brew install gpg

When you're done, reset the local version change to pom.xml with:

$ mvn versions:revert

Just as a validation, try to push local changes including tags:

$ git push origin master --tags

There should be nothing to push. If something is messed up, delete the tags in Github and in your local repo and start over.

Release History

0.0.46

  • Added this fix: #105. Thanks @marcantoine-bibeau.

0.0.45

  • No major changes
  • Default API version bumped to v55.
  • Update Jetty version used for OAuth test (only a test change)

0.0.44

  • Default API version bumped to v51. Added v50 and v51 to supported versions.
  • Add ability to access root path with API calls get put patch post delete request using ForceApi.rootPath() (briefly explained in README). Thanks @ModeratelyComfortableChair for suggestion.
  • login and password strings are now XML escaped when doing soaploginPasswordFlow. The characters < > & ' " are replaced with their &...; XML escape codes. Thanks @fredboutin for suggestion.
  • JUnit test dependency bumped from 4.10 to 4.13.1
  • Added test dependencies for jetty-util and jetty-http because apparently they are now needed.

0.0.43

  • The full request is no longer logged on a bad request to prevent sensitive data from ending up in logs. Contributed by faf0-addepar
  • Default API version bumped to v49 (Summer 2020). Code added to handle v46+ new behavior on upsert. 2021-03-19 NOTE: There was a bug here. Default version in 0.0.43 was actually v45.

0.0.42

  • Add JsonAlias to support Platform Events. Contributed by rgoers.
  • Note that default API version for this release is old: v45. Tests are failing on v46 and up due to some undocumented changes in response codes.

0.0.41

  • Introduces ForceApi.describeSObjectIfModified to make it more efficient to poll Salesforce for metadata changes to an SObject.
  • Change jackson-databind version range to [2.9.10.3,) to address security alert
  • This release is happening a long time after 0.0.40. It is possible a few other things might have changed.

0.0.40

  • You can now specify a request timeout in ApiConfig.

0.0.39

  • Change jackson-databind version range to [2.5,2.9] to address a problem where maven pulls in 3.0.0-SNAPSHOT and the compilation breaks.

0.0.38

  • Introduces ability to use a custom Jackson ObjectMapper. This can be used to support JodaTime for example. It also allows developers to choose how null values should be treated during serialization and deserialization. Before, null values were always ignored which is not always what you want. The custom ObjectMapper is set on ApiConfig. It will be used everywhere in ForceApi and ResourceRepresentation, but not in the Auth class.
  • Allows any 2.x jackson-databind version 2.5 or newer. Tests have only been run with 2.5.0 and 2.9.1.

0.0.37

  • Remove specific response code checks from generic REST api calls. Different resources may return different response codes on the same verb, e.g. POST to chatter resources returns 201, but POST to /process/approvals/ return 200. The library already checks the bounds of the response code and throws an exception if it is not between 200 and 299. The strict check on response codes is considered a bug introduced in 0.0.35 and fixed with this release.

0.0.36

  • Introduced SessionRefreshListener so you can register a listener and be notified when ForceApi refreshes the access token. See the test for sample code.

0.0.35

  • Introduced generic REST api calls get, delete, post, put and patch on ForceApi for any arbitrary path. This allows force-rest-api to be used for the many non-sObject resources exposed in Force.com REST API. See ChatterTest for an example.
  • Added getSession() convenience method on ForceApi as requested by several people. It took me a little while to become comfortable with it.
  • Added curlHelper() convenience method on ForceApi to easily print a curl command string with valid access token for debugging purposes.

0.0.34

  • Introduced ForceApi.getSupportedVersions and friends to enabled more advanced version handling. Thanks to @cswendrowski for the contributions. See "Working with API versions" in this README.

0.0.33

  • Update to Salesforce API v39

0.0.32

  • Add explicit authentication error handling. Addresses issue #32

0.0.31

  • Update to v37
  • Add queryAll

0.0.30

  • Fix NullPointerException in ApiConfig.setForceURL. Thanks steventamm.

0.0.29

  • Update to Force.com API version 36

0.0.28

  • No feature changes
  • Project now configured to release to Maven Central
  • No longer uses maven-release-plugin
  • Version number in source code is always 0-SNAPSHOT
  • Use git tags to map from Maven Central version to corresponding source code

0.0.23

  • Upgrade to Jackson 2. Thanks to emckissick for the pull request.

0.0.22

  • Include Javadoc in release jars

0.0.21

  • Made various fixes to get tests passing again after a long period of inactivity
  • end-to-end oauth test has been renamed to exlude it from test suite. Run it manually instead. It no longer uses HtmlUnit but instead requires manual intervention
  • ApiVersion is now up to date up to v33.0.
  • API version can now be set as a string. Setting it as an ApiVersion enum has been deprecated. There doesn't seem to be much value in strongly typing the api version.

0.0.20

  • thysmichels noticed that Spring 14 broke this library because Identity.java was set to strictly map to the underlying JSON resource. This class now uses ignoreUnknown=true so it should be more robust to changes.

0.0.19

0.0.18

  • Some relationship queries work now. See QueryTest for an example.
  • Tested with Jackson 1.9.7

0.0.17

  • Modified deserialization of query results to better supper queries that return graphs of records.

0.0.16

  • Added more testing, including an end-to-end oauth flow test using HtmlUnit
  • Scope is now an enum

0.0.15

  • ApiSession now serializable, so it can be cached in Memcached and similar

0.0.14

  • Fixed bug in DescribeSObject. Had inlineHelpText as boolean instead of String

0.0.13

  • More complete DescribeSObject. Can now be used to generate Java classes. An example can be found in the tests based on PojoCodeGenerator

0.0.12

  • 0.0.11 broke describeSObject. Fixed now and added test

0.0.11

0.0.10 was botched. Missed a checkin

0.0.10

  • Basic exceptions
  • Some internal refactorings
  • First attempt at session renewal

0.0.9

  • Minimalistic Describe

0.0.8

  • Added revoke support (read more)
  • Refactored refreshToken out of ApiConfig

0.0.7

  • Added support for OAuth refresh_token flow
  • Added a bit more debug info to createSObject
  • Should work with Jackson 1.9.1 and 1.9.2. Both are accepted in the version range

0.0.6

  • Tested with Winter '12, API version 23
  • Requires (and explicitly declares dependency on) Jackson 1.9.1. Not tested with other Jackson versions.
  • Basic CRUD and query functionality for SObjects
  • OAuth functionality that covers all Force.com options
  • Only happy path tested, almost no error scenarios or edge cases covered except for some sporadic debug output
  • Focused on typed access. But you must build SObject classes manually for now (or use builders available elsewhere)

Project Goals:

  • Make it as thin as possible
    • Status: Both ForceApi and Auth classes are very thin wrappers on top of the APIs.
  • Few or no dependencies
    • Status: Currently only depends on Jackson. Could consider supporting gson as well for added flexibility
  • Other projects will handle generation of typed SObject classes and it should work here
  • Automatic session renewal
    • Status: Added in 0.0.10 and testable in 0.0.12. Waiting for feedback to see if it works.
  • Pluggable JSON kit
    • Status: Not yet. This is currently low priority
  • Make sure it's Spring friendly. This solution may be necessary.
    • Status: No Spring work has been done yet
  • Consider adding newrelic hooks.

License

BSD 2-clause license

Author

Jesper Joergensen

force-rest-api's People

Contributors

dependabot[bot] avatar faf0-addepar avatar hf-kklein avatar ian-sfdc avatar jesperfj avatar marcantoine-bibeau avatar meccup avatar rgoers avatar ryanbrainard avatar steventamm 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

force-rest-api's Issues

JDK 8, mvn install -DskipTests is failing.

Environment

JDK: 1.8

Issue

After cloning the project and running mvn install -DskipTests, it is failing with the error class file for org.eclipse.jetty.util.component.aggregatelifecycle not found while compiling the file EndToEndOAuthFlowExample.java at line 131.

Resolution

After adding the dependency https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-util/8.1.14.v20131031 to pom.xml, the issue was resolved.

Is there anything I am missing here?

Jodatime Serialization

Would it be possible to allow ApiConfig to add serialization mappers to the static instance of Jackson's ObjectMapper use in ForceApi? Or even better, could we pass in our own ObjectMapper so we can customize serialization? My use case is that I use JodaTime DateTime objects, which won't serialize properly to the Salesforce format unless I can configure Jackson to use the JodaModule.

If this is something you're interested in doing, I can whip up a PR

Ability to convert lead into account?

Hi! I wasn't sure what was the best way to contact you.

Is there a way to convert a lead into an account through the client? If so, would you be able to add it to the readme as an example?

Thanks!

asRawJson() option in ResourceRepresentation

In addition to asList() and asMap() and the custom as(Class<T>) it would be nice to expose asRawJson() method or similar. This way one could receive the raw JSON from the endpoint, and deserialize in other/custom ways. Perhaps there's a clean way to do this which I am not seeing, if so any insight in that direction is appreciated. Thank you for the work on this library.

ApiVersion enum does not contain new Versions

Salesforce added v38.0 in the Winter '17 release and v39.0 in the Spring '17.

I have tested locally that adding these options to the ApiVersion enum seems to be the only action required to enable interaction with these releases and have had no issues taking advantage of new API features.

Due to the fairly high rate of version updates (3 times a year) as well as the consistency of versioning, it might be worth adding logic that calculates out the version number based on an input of [WINTER, SPRING, SUMMER], YEAR to reduce maintenance, with the calculations as follows:

Winter: 3(YEAR - 2004) - 1
Spring: 3(YEAR - 2004)
Summer: 3(YEAR - 2004) + 1

An input of WINTER, 2017 would, therefore, return version 38.

why don't you add the query all?

QueryAll
At ForceApi.java:
public QueryResult<Map> queryAll(String query) { return queryAll(query, Map.class); }

`public <T> QueryResult<T> queryAll(String query, Class<T> clazz) {
    try {
        return queryAny(uriBase() + "/queryAll/?q=" + URLEncoder.encode(query, "UTF-8"), clazz);
    } catch (UnsupportedEncodingException e) {
        throw new ResourceException(e);
    }
}`

Auth with user/password/loginendpoint credentials not possible

soaploginPasswordFlow(config) does not support an already set loginendpoint because it is adding hardwired "/services/Soap/u/33.0"
to an already complete endpoint: .setLoginEndpoint("https://login.salesforce.com/services/Soap/c/28.0/0Dxxxxxxxxxp4")

ForceApi api = new ForceApi(new ApiConfig()
.setApiVersion(ApiVersion.V28)
.setUsername("[email protected]")
.setPassword("gzui678(&GHHJGF")
.setLoginEndpoint("https://login.salesforce.com/services/Soap/c/28.0/0Dxxxxxxxxxp4")
);

updateSObject: "The Id field should not be specified in the sobject data"

Hi,
I'm using the example code to update an object. The object has an id and some properties, just like this:

@JsonIgnoreProperties(ignoreUnknown=true)
public static class MyObject {
	@JsonProperty(value="Id")
	private String id;
	@JsonProperty(value="some_property__c")
	private String some_property__c;

        public String getId() {return id;}
        public void setId(String Id) {this.id = Id;}
        public String getSome_property__c() {return some_property_c;}
        public void setSome_property__c(String some_propery__c) {this.some_property__c = some_property__c;}
	}

When I try to change the property, my program fails:

final String query = "SELECT id, some_property__c FROM sometable";
QueryResult<MyObject> result = api.query(query, MyObject.class);
for (MyObject mo : result.getRecords()) {
	mo.setSome_property__c("some value");
	api.updateSObject("sometable", mo.getId(), mo);
}

The error message of the API with HTTP status code 400 (bad request) ist:

[
	{
		"message": "The Id field should not be specified in the sobject data.",
		"errorCode": "INVALID_FIELD"
	}
]

The Java error is:

INFO com.force.api.http.Http - Bad response code: 400 on request: POST https://myinstance.my.salesforce.com/services/data/v39.0/sobjects/sometable/thealphanumerickey?_HttpMethod=PATCH
Accept: application/json
Content-Type: application/json
Authorization: Bearer ...
{"Id":"thealphanumerickey","some_property__c":"some value"}

And in fact, as soon as I modify the request body, so that it does not contain the id, it works fine.

API version is 39.0

Redirect not working

In the http client, follow redirects is turned on:

conn.setInstanceFollowRedirects(true);

This, however has no effect, since later in the file, every response code not in code < 300 && code >= 200) ; (302 = redirect) causes an error.

BUG with get knowledgeArticles

Hello,

I tried with /services/data/v40.0/support/knowledgeArticles via force-rest-api v40.0 and I get this issue:
[{"errorCode":"MISSING_ARGUMENT","message":"Language has to be specified in HTTP header."}]

I search in Google and I know how to fix it: we need add header:
Accept-language: en-US,en;q=0.9,vi;q=0.8

Document: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_knowledge_support_artlist.htm

I need its go to maven, b/c i include this client to my project via maven. Could u pls review and fix it and release to maven 0.0.41 ? tks!

Unrecognized field "email_verified"

java.lang.RuntimeException: org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field "email_verified" (Class com.force.api.Identity), not marked as ignorable

Think this is a new field that was added for Spring 14 release.

compilation failure

Pulled down the project to start working on adding a timeout to HTTP requests, got this failure when trying to compile master by running mvn install -X -DskipTests:

[ERROR] /dev_exclusions/force-rest-api/src/main/java/com/force/api/ForceApi.java:[330,81] error: method traverse in interface TreeNode cannot be applied to given types;
[ERROR]  actual and formal argument lists differ in length
[ERROR] /dev_exclusions/force-rest-api/src/main/java/com/force/api/ForceApi.java:[387,109] error: method traverse in interface TreeNode cannot be applied to given types;
[ERROR]  actual and formal argument lists differ in length
[ERROR] /dev_exclusions/force-rest-api/src/main/java/com/force/api/ForceApi.java:[391,57] error: method traverse in interface TreeNode cannot be applied to given types;
[ERROR] -> [Help 1]
org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project force-rest-api: Compilation failure
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:213)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:154)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:146)
	at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:117)
	at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:81)
	at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build(SingleThreadedBuilder.java:51)
	at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:128)
	at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:309)
	at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:194)
	at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:107)
	at org.apache.maven.cli.MavenCli.execute(MavenCli.java:993)
	at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:345)
	at org.apache.maven.cli.MavenCli.main(MavenCli.java:191)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:289)
	at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:229)
	at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:415)
	at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:356)
Caused by: org.apache.maven.plugin.CompilationFailureException: Compilation failure
	at org.apache.maven.plugin.AbstractCompilerMojo.execute(AbstractCompilerMojo.java:656)
	at org.apache.maven.plugin.CompilerMojo.execute(CompilerMojo.java:128)
	at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:134)
	at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:208)
	... 20 more```

UnrecognizedPropertyException

Hello,

I followed the instructions in the README file in order to make a call to get an Account object and I received the following error. Is this a bug or am I doing something wrong?

2017-09-20 20:31:35,559 ERROR [http-nio-8080-exec-10] GrailsExceptionResolver - UnrecognizedPropertyException occurred when processing request: [GET] /api/test/testSalesforce
Unrecognized field "attributes" (class com.test.Account), not marked as ignorable (6 known properties: "tffAccountNumber", "metaClass", "externalId", "id", "name", "annualRevenue"])
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@68b2688d; line: 1, column: 16] (through reference chain: com.test.Account["attributes"]). Stacktrace follows:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "attributes" (class com.test.Account), not marked as ignorable (6 known properties: "tffAccountNumber", "metaClass", "externalId", "id", "name", "annualRevenue"])
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@68b2688d; line: 1, column: 16] (through reference chain: com.test.Account["attributes"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:51)
at com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:817)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:954)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1324)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1302)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:249)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:136)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3562)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2648)
at com.force.api.ResourceRepresentation.as(ResourceRepresentation.java:31)
at com.test.TestController$$EQVncaRc.testSalesforce(TestController.groovy:116)
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:198)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at net.bull.javamelody.JspWrapper.invoke(JspWrapper.java:150)
at net.bull.javamelody.JdbcWrapper$DelegatingInvocationHandler.invoke(JdbcWrapper.java:286)
at net.bull.javamelody.MonitoringFilter.doFilter(MonitoringFilter.java:201)
at net.bull.javamelody.MonitoringFilter.doFilter(MonitoringFilter.java:178)
at grails.plugin.springsecurity.web.filter.GrailsAnonymousAuthenticationFilter.doFilter(GrailsAnonymousAuthenticationFilter.java:53)
at grails.plugin.springsecurity.rest.RestAuthenticationFilter.doFilter(RestAuthenticationFilter.groovy:139)
at grails.plugin.springsecurity.web.authentication.RequestHolderAuthenticationFilter.doFilter(RequestHolderAuthenticationFilter.java:53)
at grails.plugin.springsecurity.web.authentication.logout.MutableLogoutFilter.doFilter(MutableLogoutFilter.java:62)
at grails.plugin.springsecurity.rest.RestLogoutFilter.doFilter(RestLogoutFilter.groovy:80)
at com.AssetFilter.doFilter(AssetFilter.groovy:55)
at com.security.XssFilter.doFilterInternal(XssFilter.groovy:17)
at grails.plugin.databasesession.SessionProxyFilter.doFilterInternal(SessionProxyFilter.java:60)

Add support for JWT Bearer Flow

This library does not support the required and best practice authentication mechanism of the "JWT Bearer Flow" https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&language=en_US&type=5 .

This is the standard way of doing machine to machine OAuth. You have support for setRefreshToken() but this isn't helpful as the standard JWT bearer flow Oauth is what you need to to in order to get a token in the first place. Semi-related, the Ruby restforce gem has similar confusion and missing features around JWT bearer flow.

FWIW here's what it looks like in a Postman pre-request script to get a proper access token with Salesorce, where you build a JWT with claims, sign it with the private key (part of the JWT bearer flow), and gets back an access token that's valid for a few hours:

var navigator = {};
var window = {};

const jwt_header = {
  "alg": "RS256",
  "typ": "JWT"
};
const jwt_claims = {
  "iss": pm.collectionVariables.get("SALESFORCE_CONSUMER_KEY"),
  "sub": pm.collectionVariables.get("SALESFORCE_USERNAME"),
  "aud": "https://test.salesforce.com",
  "exp": Date.now() + 10000
};
const private_key = atob(pm.collectionVariables.get("SALESFORCE_PRIVATE_KEY"));

pm.sendRequest({
  url: 'http://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js',
  method: 'GET'
}, function (err, res) {
    eval(res.text());

    const signedJwt = KJUR.jws.JWS.sign('RS256', JSON.stringify(jwt_header), JSON.stringify(jwt_claims), private_key);
    // console.log('Assertion to send to Salesforce:', signedJwt);

    pm.sendRequest({
    url: pm.collectionVariables.get("SALESFORCE_TOKEN_URL"),
    method: 'POST',
    header: {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: {
        mode: 'urlencoded',
            urlencoded: [
            { key: "grant_type", value: "urn:ietf:params:oauth:grant-type:jwt-bearer", disabled: false },
            { key: "assertion", value: signedJwt, disabled: false}
        ]
    }
    }, function (err, res) {
        console.log('Auth err:', err, ' Auth res', res.json());
        pm.environment.set("instance_url", res.json().instance_url);
        pm.environment.set("authorization", res.json().access_token);
    });

});

It looks like this library may be abandonware, are there any forks which add this required auth mechanism?

Timeouts are not handled

In our application, we started seeing requests to Salesforce time out, which led to problems with blocked threads and our application basically "hanging" after a while.

Digging into the code, it looks as if there is no explicit timeout set with the HTTP clients used. For now, we fixed out problem by wrapping the call into a Hystrix command (which then indeed showed that our problem was timeouts). We just wanted to raise this problem here, as others might run into it as well. Maybe it would be a good extension to allow users to configure a timeout for the calls?

Add support for Headers

There are some HTTP headers that can be passed when creating objects in salesforce.
Ref Assignment Rule Header for one such header.

Currently ForceAPI#createSObject does not accept any of these custom headers.

Proposed fix would be to have another create method which would take a collection of headers to be applied on the request. Let me know if this sounds good, and I would be more than happy to submit a PR with the change.

Force REST API - giving below exception

java.lang.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
at [Source: UNKNOWN; line: 1, column: 1]
java.lang.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
at [Source: UNKNOWN; line: 1, column: 1]
at com.force.api.Auth.oauthLoginPasswordFlow(Auth.java:47)
at com.force.api.Auth.authenticate(Auth.java:208)
at com.force.api.ForceApi.(ForceApi.java:68)
at com.aws.datasync.SalesforceConnector.main(SalesforceConnector.java:23)
Caused by: com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
at [Source: UNKNOWN; line: 1, column: 1]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:3607)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3547)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2648)
at com.force.api.Auth.oauthLoginPasswordFlow(Auth.java:32)
... 3 more

No easy way to get id after "createOrUpdate"

The CreateOrUpdateResult exposes just a Status which does'nt help. Will fork and try a few ways to make "upsert" work. Probably include the ResourceRepresentation as part of the Result so that you can fetch and do a As() from the Result but that feels inconsistent with create and update where you give a straight up ResourceRepresentation.

Possibility to reconfigure the json mapper?

Hi,

The following line in the ForceApi class is causing us some trouble:
jsonMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

jsonMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

The problem we are having is that when an attribute currently has a value, but then that value was cleared, we receive that new value as "null" from our source system, but it never reaches Salesforce because this null value is ignored by the JSON serialization in ForceApi.

Was there a specific reason why this is the non-changeable default? Would it maybe be a good extension to make this configurable?

We'd be happy to create a pull request for this, were just wondering what the best way might be. Would it be too granular to introduce a boolean option in the configuration that says includeNonNull, with default value true? (In that case the jsonMapper couldn't be a static field anymore, would have to be an instance field that gets initialised in the constructor)

Thanks!

"No content to map due to end-of-input"

Hi, thanks for this library, i am trying to connect force API, my code is here

 ForceApi api = new ForceApi(new ApiConfig()
                .setUsername("username")
                .setPassword("password")
                .setClientId("client_id")
                .setClientSecret("client_secret"));

result is

Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: No content to map due to end-of-input
 at [Source: UNKNOWN; line: 1, column: 1]

Support for Salesforce Platform Events

The ForceAPI createSObject method is able to successfully create a custom Salesforce event but it fails processing the response with:

Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "statusCode" (class com.force.api.ApiError), not marked as ignorable (3 known properties: "fields", "apiErrorCode", "message"])
at [Source: (sun.net.www.protocol.http.HttpURLConnection$HttpInputStream); line: 1, column: 159] (through reference chain: com.force.api.CreateResponse["errors"]->java.lang.Object[][0]->com.force.api.ApiError["statusCode"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:840) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1192) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1592) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1570) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:294) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:195) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:21) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3242) ~[jackson-databind-2.10.1.jar!/:2.10.1]
at com.force.api.ForceApi.createSObject(ForceApi.java:207) ~[force-rest-api-0.0.41.jar!/:?]

This is because Salesforce returns a statusCode attribute in the response instead of the apiErrorCode attribute that is returned when creating "normal" objects.

ForceApi with autorenew enabled can/will create new ApiSession but does not provide access to it

I had a situation where I was persisting an accessToken and refreshToken to database after doing an oauth login. The initial accessToken provided quickly expires (hour or so) and so internally the autorenew was fetching a new accessToken but I had no way of accessing it. Every time thereafter I would still instantiate ForceApi using the old, invalid access token and good refresh token and every single time ForceApi would then need to use the refreshToken to create a new accessToken.

This actually caused a further problem in my case because when forceApi was able to communicate I assumed my access token was good and was then using it in my own custom query request (i.e. a url endpoint not supported by this great api - btw, it is great; thanks for the good work).

Something like this should do it:

/** * Need access to ApiSession in case autorenew has been used and a new accessToken issued. * That accessToken may be used externally or used for future creation of ForceApi objects. * @return current ApiSession in use */ public ApiSession getApiSession(){ return this.session; }

Add formula to DescribeSObject.Field

Hi,

We are using the ForceAPI library and would like to to retrieve the calculated formula of a field.

As described here: https://developer.salesforce.com/docs/atlas.en-us.246.0.api.meta/api/sforce_api_calls_describesobjects_describesobjectresult.htm
Salesforce supplies formula which is:

The formula specified for this field. If no formula is specified for this field, it is not returned.

Can you add it the the DescribeSObject.Field as you already have calculated fields which (If I understand correctly) indicated whether this field have a formula or not.

Thank you

Provide support for Rest Resource calls.

Love the library. Would just be great if you could allow for Rest Resource calls. The ForceApi.uriBase() gets in the way of targeting /services/apexrest. If we could either get a request function which doesn't add this to the .url, or make the fields protected so we could extend the class and overwrite our function, it would be awesome! I'm currently just copying over the entire class, but that doesn't look as neat.

Request to make ForceApi extendable

I would like to extend ForceApi to call custom salesforce apex rest api's.
Could you please change session and apiRequest access modifiers to protected?

How to generate Jackson object from wsdl?

You have mentioned that, we need to have Objects with proper Jackson deserialization annotations, How can we create jaxb/jackson object from the enterprise wsdl.

When i use wsc jar [https://github.com/forcedotcom/wsc] to generate the objects, the objects are totally different (soap related). Writing all these objects will be a huge task - is there any best solution to generate these like the way you have mentioned.

Here is the code you have mentioned.
This assumes you have an Account class defined with proper Jackson deserialization annotations. For example:
@JsonIgnoreProperties(ignoreUnknown=true)
public class Account {
}

DescribeSObject.Field.referenceTo not being populated

Didn't get a chance to dig into this, but com.force.apiDescribeSObject.Field.referenceTo is not getting populated when it should by Jackson. Assuming this has something to do with with being a collection field. Here's a test showing the behavior:

@Test
public void testDescribeSObject() {
    DescribeSObject ds = api.describeSObject("Contact");
    assertEquals("Contact", ds.getName());
    assertNotNull(ds.getAllFields());
    assertNotNull(ds.getAllFields().iterator().next().getSoapType());
    assertDescribedRelationship(ds, "AccountId", "Account");
}

private void assertDescribedRelationship(DescribeSObject ds, String fieldName, String relationshipName) {
    for (DescribeSObject.Field f : ds.getFields()) {
        if (f.getName().equals(fieldName)) {
            assertEquals(relationshipName, f.getReferenceToEntity().get(0));
            return;
        }
    }

    fail(fieldName + " not found");
}

Stacktrace:

ava.lang.NullPointerException
at com.force.api.DescribeTest.assertDescribedRelationship(DescribeTest.java:40)
at com.force.api.DescribeTest.testDescribeSObject(DescribeTest.java:34)

Return http responses instead of void calls

Hello,

I was wondering why there is no return value for http responses on some methods:
public void updateSObject(...
whereas this similar method has one (even though its a simple Enum value):
public CreateOrUpdateResult createOrUpdateSObject(...

I would want to monitor what happens to my http call, but as it is, its lost in the void.

Thanks.

Missing refresh token on response

I keep getting this error no matter what I do.

java.io.IOException: Missing refresh token on response
at com.force.sdk.oauth.connector.ForceOAuthConnector.createTokenInternal(ForceOAuthConnector.java:196)
at com.force.sdk.oauth.connector.ForceOAuthConnector.getAccessToken(ForceOAuthConnector.java:145)
at com.force.sdk.springsecurity.OAuthAuthenticationProvider.authenticate(OAuthAuthenticationProvider.java:78)
at org.springframework.security.authentication.ProviderManager.doAuthentication(ProviderManager.java:130)

Support for jdk 10

Hi,
we're using ForceAPI to make calls to an environment that uses JDK9 at the moment, but now we're thinking of moving to JDK10. I couldn't find anything in the docs, is there anything we need to be aware of before we do that?

Add proxy settings in ApiConfig

I use this library in an application with a proxy and it is not working. Even using the following property in my Java code.

         System.setProperty ("http.proxyHost", "...");
         System.setProperty ("http.proxyPort", "...");

Is it possible to add the possibility to set up a proxy in the class "ApiConfig" so that it is used in methods "openConnection" Class "HttpURLConnection"?

The code is present in this fork: https://github.com/alexsapps/force-rest-api/tree/improvementsMarch2015

Thank you

Springboot 3.3 Stream closed

I don't know the cause but as soon as I update our project to use Springboot 3.3.0, we start getting this exception.

Any clue on your side?

java.io.IOException: Stream closed
at java.base/java.io.BufferedInputStream.getInIfOpen(BufferedInputStream.java:169)
at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:345)
at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420)
at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:399)
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95)
at com.force.api.http.Http.readResponse(Http.java:57)
at com.force.api.http.Http.send(Http.java:113)
... 17 common frames omitted
Wrapped by: java.lang.RuntimeException: java.io.IOException: Stream closed
at com.force.api.http.Http.send(Http.java:118)
at com.force.api.ForceApi.apiRequest(ForceApi.java:499)
at com.force.api.ForceApi.queryAny(ForceApi.java:343)
at com.force.api.ForceApi.query(ForceApi.java:315)

Test failure on v46

When going from v45 to v46, com.force.api.BasicCRUDTest.basicCRUDTest fails. v46 returns code 200 where v45 returns expected code (201 or 204). What changed in Salesforce REST API between 45 and 46?

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.