├── .gitignore
├── LICENSE
├── README.md
├── RELEASE_NOTES.md
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── washingtonpost
│ └── wordpress
│ └── rest
│ └── api
│ ├── JAXRSWordPressClient.java
│ ├── MockWordPressClient.java
│ ├── StringUtils.java
│ ├── WordPressClient.java
│ ├── WordPressClientFactory.java
│ ├── model
│ ├── Post.java
│ └── WordPressPost.java
│ └── transformers
│ ├── AbstractTransformer.java
│ ├── Transformer.java
│ └── WordPressTransformer.java
└── test
├── java
└── com
│ └── washingtonpost
│ └── wordpress
│ └── rest
│ └── api
│ ├── IntTestJAXRSWordPressClient.java
│ ├── TestJAXRSWordPressClient.java
│ └── TestWordPressClientFactory.java
└── resources
├── off-the-shelf-response.json
└── simplelogger.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 |
3 | # Mobile Tools for Java (J2ME)
4 | .mtj.tmp/
5 |
6 | # Package Files #
7 | *.jar
8 | *.war
9 | *.ear
10 |
11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
12 | hs_err_pid*
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wordpress-rest-api-java-client
2 | Java Client for connecting to and parsing JSON coming our of a WordPress REST API plugin
3 |
4 | This Client uses JAX-RS to connect to a remote WordPress REST API and retrieve the lists of Posts from the /posts endpoint. The client passes through whatever query parameters sent to the `getPosts( )` method directly to the WordPress REST API. This client comes with a default implementation of a Transformer class that knows how to transform WordPress JSON into a "WordPressPost.java" POJO.
5 |
6 | If your WordPress JSON feed is customized to include fields not contained in the off-the-shelf WordPress JSON API installation, you can construct an instance of a WordPressClient with your own Transformer/POJO pair that's suitable for your organization.
7 |
8 | ## Including this JAR
9 |
10 | Add this dependency to your Maven POM:
11 | ```
12 |
Blah
23 | */ 24 | public class JAXRSWordPressClient implements WordPressClient { 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(JAXRSWordPressClient.class); 27 | 28 | private final URI targetURI; 29 | private final Transformer extends Post> transformer; 30 | private Client jaxrsClient; 31 | 32 | /** 33 | *Package private constructor to encourage the caller to use this Client's associated Factory
34 | * @param targetURI The base URI of the Wordpress REST API. The {@code targetUri} should include the FQDN up to, but 35 | * not including the REST endpoints (e.g. "https://wp.foo.com/wp-json/" not 36 | * "https://wp.foo.com/wp-json/posts") 37 | * @param requestFilter A possibly null filter to pass every request this CLient makes to WordPress through. Useful for 38 | * adding BasicAuth header information to every request, if appropriate. 39 | * @param transformer A concrete class to provide transformation services from the JSON returned by the WP-API and a POJO 40 | * that implements the Post marker interface 41 | */ 42 | JAXRSWordPressClient(URI targetURI, 43 | ClientRequestFilter requestFilter, 44 | Transformer extends Post> transformer) { 45 | this.targetURI = targetURI; 46 | this.transformer = transformer; 47 | this.jaxrsClient = ClientBuilder.newClient(); 48 | 49 | // If our authenticator is provided, register it as a default ClientRequestFilter so that every request this Client 50 | // makes has BasicAuth information added to it 51 | if (requestFilter != null) { 52 | jaxrsClient = jaxrsClient.register(requestFilter); 53 | } 54 | logger.debug("Constructing new JAXRSWordPressClient with targetURI '{}' and transformer '{}' " 55 | + "and requestFilter '{}'", targetURI, transformer, requestFilter); 56 | } 57 | 58 | @Override 59 | public List extends Post> getPosts(String queryParams) { 60 | StringBuilder endpoint = new StringBuilder(this.targetURI.toString()).append("/posts"); 61 | if (queryParams != null && !queryParams.isEmpty()) { 62 | endpoint.append("?").append(queryParams); 63 | } 64 | 65 | logger.debug("JAXRSWordPressClient making request to '{}'", endpoint); 66 | 67 | Response response = jaxrsClient 68 | .target(endpoint.toString()) 69 | .request(MediaType.APPLICATION_JSON) 70 | .get(); 71 | 72 | if (Status.OK.getStatusCode() == response.getStatusInfo().getStatusCode()) { 73 | String json = readInputStreamToString((InputStream)response.getEntity()); 74 | try { 75 | return this.transformer.transform(json); 76 | } 77 | catch (IOException ex) { 78 | logger.error("Unable to transform JSON '{}' into a Post", json); 79 | throw new RuntimeException(ex); 80 | } 81 | } 82 | else { 83 | String errMsg = String.format("Did not get an OK status code from WordPress API at %s, rather: %s", 84 | endpoint, response.getStatusInfo()); 85 | logger.error(errMsg); 86 | throw new RuntimeException(errMsg); 87 | } 88 | } 89 | 90 | /** 91 | * Visible for testing. 92 | * @param inputStream An input stream 93 | * @return The String content of that inputStream 94 | */ 95 | String readInputStreamToString(InputStream inputStream) { 96 | StringWriter writer = new StringWriter(); 97 | try { 98 | IOUtils.copy(inputStream, writer, Charset.forName("UTF-8")); 99 | } 100 | catch (IOException ex) { 101 | throw new RuntimeException(ex); 102 | } 103 | return writer.toString(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/MockWordPressClient.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api; 2 | 3 | import com.washingtonpost.wordpress.rest.api.model.Post; 4 | import com.washingtonpost.wordpress.rest.api.transformers.Transformer; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.List; 8 | 9 | /** 10 | *A Mock Client, helping for testing and localhost development
11 | */ 12 | public class MockWordPressClient implements WordPressClient { 13 | private List extends Post> posts; 14 | 15 | public MockWordPressClient(String classpathFixture, Transformer extends Post> transformer) { 16 | InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(classpathFixture); 17 | String jsonFixture = StringUtils.inputStreamToString(inputStream); 18 | try { 19 | this.posts = transformer.transform(jsonFixture); 20 | } 21 | catch (IOException ex) { 22 | throw new RuntimeException(ex); 23 | } 24 | } 25 | 26 | @Override 27 | public List extends Post> getPosts(String queryParams) { 28 | return posts; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | 8 | /** 9 | *10 | * Hand-jammed utility to avoid bringing in unnecessary dependencies to our wrapping classes
11 | */ 12 | final class StringUtils { 13 | private static final int BUFFER_SIZE = 8192; 14 | 15 | private StringUtils() { 16 | // shutup, checkstyle 17 | } 18 | 19 | static String inputStreamToString(InputStream inputStream) { 20 | StringBuilder sb = new StringBuilder(BUFFER_SIZE); 21 | try { 22 | BufferedReader r = new BufferedReader(new InputStreamReader(inputStream)); 23 | String str = null; 24 | while ((str = r.readLine()) != null) { 25 | sb.append(str); 26 | } 27 | } 28 | catch (IOException ioe) { 29 | throw new RuntimeException(ioe); 30 | } 31 | return sb.toString(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/WordPressClient.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api; 2 | 3 | import com.washingtonpost.wordpress.rest.api.model.Post; 4 | import java.util.List; 5 | 6 | /** 7 | *The interface describing how to connect to a WordPress REST API and get a List of Posts out of it
8 | * @paramThe type of the Post 9 | */ 10 | public interface WordPressClient
{ 11 | 12 | /** 13 | * @param queryParams How to query the WordPress API 14 | * @return A list of {@code P} object matching the queryParams. 15 | */ 16 | List
getPosts(String queryParams); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/WordPressClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api; 2 | 3 | import com.washingtonpost.wordpress.rest.api.model.Post; 4 | import com.washingtonpost.wordpress.rest.api.transformers.Transformer; 5 | import com.washingtonpost.wordpress.rest.api.transformers.WordPressTransformer; 6 | import java.io.IOException; 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.nio.charset.StandardCharsets; 11 | import javax.ws.rs.client.ClientRequestContext; 12 | import javax.ws.rs.client.ClientRequestFilter; 13 | import javax.ws.rs.core.MultivaluedMap; 14 | import javax.xml.bind.DatatypeConverter; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | *
A Factory/Builder for creating a concrete JAXRSWordPressClient
20 | *Usage is fluent, like:
21 | *22 | * JAXRSWordPressClient myClient = (new JAXRSWordPressFactory()) 23 | * .withBaseUrl("http://wordpress.foo.com/wp-json") 24 | * .withCredentials("joe", "secret") 25 | * .withTransformer(new MySpecialTransformer()) 26 | * .build(); 27 | *28 | *
You can also create a Mock Client by specifying a classpath resource containing an array of Post JSON, like:
29 | *30 | * MockWordPressClient myClient = (new JAXRSWordPressFactory()) 31 | * .withMockResource("/some/fixture/on/my/classpath.blah") 32 | * .build(); 33 | *34 | */ 35 | public class WordPressClientFactory { 36 | private static final Logger logger = LoggerFactory.getLogger(WordPressClientFactory.class); 37 | private ClientRequestFilter clientRequestFilter; 38 | private Transformer extends Post> transformer = new WordPressTransformer(); 39 | private URI uri; 40 | private String mockResource = null; 41 | 42 | public WordPressClientFactory() { 43 | } 44 | 45 | 46 | public WordPressClientFactory withCredentials(String username, String password) { 47 | this.clientRequestFilter = new ClientRequestFilter() { 48 | private final String charset = StandardCharsets.UTF_8.name(); 49 | 50 | @Override 51 | public void filter(ClientRequestContext requestContext) throws IOException { 52 | 53 | String token = String.format("%s:%s", username, password); 54 | 55 | String basicAuthHeader = null; 56 | 57 | try { 58 | basicAuthHeader = "BASIC " + DatatypeConverter.printBase64Binary(token.getBytes(charset)); 59 | } 60 | catch (UnsupportedEncodingException ex) { 61 | throw new IllegalStateException("Cannot encode with " + charset, ex); 62 | } 63 | 64 | MultivaluedMap
Marker Interface for an Object that is Transformed from WordPress JSON to a Java POJO
5 | */ 6 | public interface Post { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/model/WordPressPost.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | /** 6 | *This concrete class models the out-of-the-box fields that the WordPress REST API returns. Extend it if your organization 7 | * includes additional attributes in your plugin's response
8 | *From the wp-api.org documentation, the following fields are the out-of-the-box values understood and modeled by this 9 | * POJO:
10 | *11 | * title - Title of the post. (string) required 12 | * content_raw - Full text of the post. (string) required 13 | * excerpt_raw - Text for excerpt of the post. (string) optional 14 | * name - Slug of the post. (string) optional 15 | * status - Post status of the post: draft, publish, pending, future, private, or any custom registered status. 16 | * If providing a status of future, you must specify a date in order for the post to be published as expected. 17 | * Default is draft. (string) optional 18 | * type - Post type of the post: post, page, link, nav_menu_item, or a any custom registered type. 19 | * Default is post. (string) optional 20 | * date - Date and time the post was, or should be, published in local time. Date should be an RFC3339 timestamp] 21 | * (http://tools.ietf.org/html/rfc3339). Example: 2014-01-01T12:20:52Z. Default is the local date and time. 22 | * (string) optional 23 | * date_gmt - Date and time the post was, or should be, published in UTC time. Date should be an RFC3339 timestamp. 24 | * Example: 201401-01T12:20:52Z. Default is the current GMT date and time. (string) optional 25 | * author - Author of the post. Author can be provided as a string of the author’s ID or as the User object of the author. 26 | * Default is current user. (object | string) optional 27 | * password - Password for protecting the post. Default is empty string. (string) optional 28 | * post_parent - Post ID of the post parent. Default is 0. (integer) optional 29 | * post_format - Format of the post. Default is standard. (string) optional 30 | * menu_order - The order in which posts specified as the page type should appear in supported menus. 31 | * Default 0. (integer) optional 32 | * comment_status - Comment status for the post: open or closed. Indicates whether users can submit comments to the post. 33 | * Default is the option ‘default_comment_status’, or ‘closed’. (string) optional 34 | * ping_status - Ping status for the post: open or closed. Indicates whether users can submit pingbacks or trackbacks to 35 | * the post. Default is the option ‘default_ping_status’. (string) optional 36 | * sticky - Sticky status for the post: true or false. Default is false. (boolean) optional 37 | * post_meta - Post meta entries of the post. Post meta should be an array of one or more Meta objects for each post meta 38 | * entry. See the Create Meta for a Post endpoint for the key value pairs. (array) optional 39 | *40 | */ 41 | public class WordPressPost implements Post { 42 | 43 | @JsonProperty(value="title", required=true) 44 | private String title; 45 | @JsonProperty(value="ID", required=true) 46 | private long id; 47 | @JsonProperty(value="content_raw", required=true) 48 | private String contentRaw; 49 | @JsonProperty(value="excerpt_raw") 50 | private String excerptRaw; 51 | @JsonProperty(value="name") 52 | private String name; 53 | @JsonProperty("status") 54 | private String status; 55 | @JsonProperty("type") 56 | private String type; 57 | @JsonProperty("date") 58 | private String date; 59 | @JsonProperty("date_gmt") 60 | private String dateGMT; 61 | @JsonProperty("author") 62 | private String author; 63 | @JsonProperty("password") 64 | private String password; 65 | @JsonProperty("post_parent") 66 | private long postParent; 67 | @JsonProperty("post_format") 68 | private String postFormat; 69 | @JsonProperty("menu_order") 70 | private int menuOrder; 71 | @JsonProperty("comment_status") 72 | private String commentStatus; 73 | @JsonProperty("ping_status") 74 | private String pingStatus; 75 | @JsonProperty("sticky") 76 | private boolean sticky; 77 | @JsonProperty("post_meta") 78 | private String postMeta; 79 | 80 | public long getId() { 81 | return id; 82 | } 83 | 84 | public void setId(long id) { 85 | this.id = id; 86 | } 87 | 88 | public String getTitle() { 89 | return title; 90 | } 91 | 92 | public void setTitle(String title) { 93 | this.title = title; 94 | } 95 | 96 | public String getContentRaw() { 97 | return contentRaw; 98 | } 99 | 100 | public void setContentRaw(String contentRaw) { 101 | this.contentRaw = contentRaw; 102 | } 103 | 104 | public String getExcerptRaw() { 105 | return excerptRaw; 106 | } 107 | 108 | public void setExcerptRaw(String excerptRaw) { 109 | this.excerptRaw = excerptRaw; 110 | } 111 | 112 | public String getName() { 113 | return name; 114 | } 115 | 116 | public void setName(String name) { 117 | this.name = name; 118 | } 119 | 120 | public String getStatus() { 121 | return status; 122 | } 123 | 124 | public void setStatus(String status) { 125 | this.status = status; 126 | } 127 | 128 | public String getType() { 129 | return type; 130 | } 131 | 132 | public void setType(String type) { 133 | this.type = type; 134 | } 135 | 136 | public String getDate() { 137 | return date; 138 | } 139 | 140 | public void setDate(String date) { 141 | this.date = date; 142 | } 143 | 144 | public String getDateGMT() { 145 | return dateGMT; 146 | } 147 | 148 | public void setDateGMT(String dateGMT) { 149 | this.dateGMT = dateGMT; 150 | } 151 | 152 | public String getAuthor() { 153 | return author; 154 | } 155 | 156 | public void setAuthor(String author) { 157 | this.author = author; 158 | } 159 | 160 | public String getPassword() { 161 | return password; 162 | } 163 | 164 | public void setPassword(String password) { 165 | this.password = password; 166 | } 167 | 168 | public long getPostParent() { 169 | return postParent; 170 | } 171 | 172 | public void setPostParent(long postParent) { 173 | this.postParent = postParent; 174 | } 175 | 176 | public String getPostFormat() { 177 | return postFormat; 178 | } 179 | 180 | public void setPostFormat(String postFormat) { 181 | this.postFormat = postFormat; 182 | } 183 | 184 | public int getMenuOrder() { 185 | return menuOrder; 186 | } 187 | 188 | public void setMenuOrder(int menuOrder) { 189 | this.menuOrder = menuOrder; 190 | } 191 | 192 | public String getCommentStatus() { 193 | return commentStatus; 194 | } 195 | 196 | public void setCommentStatus(String commentStatus) { 197 | this.commentStatus = commentStatus; 198 | } 199 | 200 | public String getPingStatus() { 201 | return pingStatus; 202 | } 203 | 204 | public void setPingStatus(String pingStatus) { 205 | this.pingStatus = pingStatus; 206 | } 207 | 208 | public boolean isSticky() { 209 | return sticky; 210 | } 211 | 212 | public void setSticky(boolean sticky) { 213 | this.sticky = sticky; 214 | } 215 | 216 | public String getPostMeta() { 217 | return postMeta; 218 | } 219 | 220 | public void setPostMeta(String postMeta) { 221 | this.postMeta = postMeta; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/transformers/AbstractTransformer.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api.transformers; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.washingtonpost.wordpress.rest.api.model.Post; 5 | 6 | /** 7 | *
An Abstract Transformer
8 | * @paramThe transformer type 9 | */ 10 | public abstract class AbstractTransformer
implements Transformer
{ 11 | 12 | protected ObjectMapper objectMapper; 13 | 14 | public AbstractTransformer() { 15 | objectMapper = new ObjectMapper(); 16 | // FIXME remove this once the Objects actually map the JSON spec 17 | // objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 18 | } 19 | 20 | public ObjectMapper getObjectMapper() { 21 | return objectMapper; 22 | } 23 | 24 | public void setObjectMapper(ObjectMapper objectMapper) { 25 | this.objectMapper = objectMapper; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/transformers/Transformer.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api.transformers; 2 | 3 | import com.washingtonpost.wordpress.rest.api.model.Post; 4 | import java.io.IOException; 5 | import java.util.List; 6 | 7 | /** 8 | *
An interface describing how to transform JSON into a Model object
9 | * @paramThe type of post this Transformer produces. 10 | */ 11 | public interface Transformer
{ 12 | 13 | List
transform(String json) throws IOException; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/washingtonpost/wordpress/rest/api/transformers/WordPressTransformer.java: -------------------------------------------------------------------------------- 1 | package com.washingtonpost.wordpress.rest.api.transformers; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.washingtonpost.wordpress.rest.api.model.WordPressPost; 5 | import java.io.IOException; 6 | import java.util.List; 7 | 8 | /** 9 | *
Transformer for the standard WordPress response
10 | */ 11 | public class WordPressTransformer extends AbstractTransformerUses WireMock to assert what the client is actually requesting on the wire of our "server"
16 | */ 17 | @SuppressWarnings("unchecked") 18 | public class IntTestJAXRSWordPressClient { 19 | 20 | private final int port = 8089; 21 | private final String url = String.format("http://localhost:%d/wp-json", port); 22 | @Rule 23 | public WireMockRule wireMockRule = new WireMockRule(port); 24 | 25 | 26 | @Test 27 | public void testUnauthenticatedGetPosts() { 28 | stubFor(get(urlMatching("/wp-json/posts\\?foo=bar")) 29 | .willReturn(aResponse() 30 | .withStatus(200) 31 | .withHeader("Content-Type", "application/json") 32 | .withBody("[{\"title\":\"Some title\"}]"))); 33 | 34 | WordPressClientTests the JAXRSWordPressClient
15 | */ 16 | @SuppressWarnings("unchecked") 17 | public class TestJAXRSWordPressClient { 18 | 19 | @Test 20 | public void testHappyPath() throws IOException { 21 | 22 | WordPressClientTests the WordPressClientFactory
13 | */ 14 | @SuppressWarnings("unchecked") 15 | public class TestWordPressClientFactory { 16 | 17 | @Test(expected=IllegalArgumentException.class) 18 | public void testFactoryChokesOnBadBaseUrl() { 19 | new WordPressClientFactory().withBaseUrl("ggg:\\bad.format").build(); 20 | } 21 | 22 | @Test 23 | public void testFactoryWithFakeBuilder() { 24 | WordPressClient