diff --git a/build.gradle b/build.gradle
index 936e275..833fef7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,14 @@ repositories {
apply plugin: "java"
dependencies {
- compile "com.github.jreddit:jreddit:1.0.3"
+ //compile "com.github.jreddit:jreddit:1.0.3" //not supported by reddit api anymore
+ //jreddit 1.0.4 dependencies
+ compile group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'
+ compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3.3'
+ compile group: 'org.apache.oltu.oauth2', name: 'org.apache.oltu.oauth2.client', version: '1.0.0'
+ testCompile group: 'commons-validator', name: 'commons-validator', version: '1.4.1'
+ testCompile group: 'junit', name: 'junit', version: '4.8.1'
+ testCompile group: 'org.mockito', name: 'mockito-core', version: '1.9.5'
}
sourceSets {
diff --git a/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java b/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java
new file mode 100644
index 0000000..2730230
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/RedditOAuthAgent.java
@@ -0,0 +1,370 @@
+package com.github.jreddit.oauth;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.xml.bind.DatatypeConverter;
+
+import org.apache.oltu.oauth2.client.OAuthClient;
+import org.apache.oltu.oauth2.client.URLConnectionClient;
+import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
+import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder;
+import org.apache.oltu.oauth2.common.OAuthProviderType;
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
+import org.apache.oltu.oauth2.common.message.types.GrantType;
+
+import com.github.jreddit.oauth.app.RedditApp;
+import com.github.jreddit.oauth.exception.RedditOAuthException;
+import com.github.jreddit.oauth.param.RedditDuration;
+import com.github.jreddit.oauth.param.RedditScopeBuilder;
+import com.github.jreddit.request.util.KeyValueFormatter;
+
+/**
+ * Thread-safe reddit OAuth agent.
+ *
+ * Communicates with reddit to retrieve tokens, and converts them
+ * into {@link RedditToken}s, which are used internally by jReddit. This class
+ * supports both the code grant flow and implicit grant flow.
+ *
+ * @author Simon Kassing
+ */
+public class RedditOAuthAgent {
+
+ /** Reddit authorization endpoint. */
+ private static final String REDDIT_AUTHORIZE = "https://www.reddit.com/api/v1/authorize?";
+
+ /** Grant type for an installed client (weirdly enough a URI). */
+ private static final String GRANT_TYPE_INSTALLED_CLIENT = "https://oauth.reddit.com/grants/installed_client";
+
+ /** Grant type for client credentials (described in OAuth2 standard). */
+ private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
+
+ /* Parameter keys */
+ private static final String PARAM_CLIENT_ID = "client_id";
+ private static final String PARAM_RESPONSE_TYPE = "response_type";
+ private static final String PARAM_STATE = "state";
+ private static final String PARAM_REDIRECT_URI = "redirect_uri";
+ private static final String PARAM_DURATION = "duration";
+ private static final String PARAM_SCOPE = "scope";
+ private static final String PARAM_GRANT_TYPE = "grant_type";
+ private static final String PARAM_CODE = "code";
+ private static final String PARAM_DEVICE_ID = "device_id";
+
+ /* Header keys */
+ private static final String HEADER_USER_AGENT = "User-Agent";
+ private static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /** User agent. */
+ private final String userAgent;
+
+ /** OAuth2 client for OAuth related requests. */
+ private OAuthClient oAuthClient;
+
+ /** Reddit application. */
+ private RedditApp redditApp;
+
+ /**
+ * Constructor for a Reddit OAuth agent.
+ *
+ * A default Apache OAuthClient will be made to perform the OAuth communication.
+ *
+ * @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java")
+ * @param redditApp Reddit application
+ */
+ public RedditOAuthAgent(String userAgent, RedditApp redditApp) {
+ this(userAgent, redditApp, new OAuthClient(new URLConnectionClient()));
+ }
+
+ /**
+ * Constructor for a Reddit OAuth agent.
+ *
+ * @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java")
+ * @param redditApp Reddit application
+ * @param oAuthClient Apache OAuth2 client
+ */
+ public RedditOAuthAgent(String userAgent, RedditApp redditApp, OAuthClient oAuthClient) {
+ this.userAgent = userAgent;
+ this.redditApp = redditApp;
+ this.oAuthClient = oAuthClient;
+ }
+
+ /**
+ * Generate the code flow Uniform Resource Locator (URI) for a
+ * reddit user to authorize your application.
+ *
+ * The user will, after authorization, receive a code. This can be turned into
+ * a RedditToken using {@link #token(String)}.
+ *
+ * @param scopeBuilder Authorization scope builder (must not be null)
+ * @param duration Duration that the token can last
+ *
+ * @return The URI users need to visit and retrieve the code from
+ *
+ * @see {@link #token(String)} for converting the code into a usable RedditToken
+ */
+ public synchronized String generateCodeFlowURI(RedditScopeBuilder scopeBuilder, RedditDuration duration) {
+
+ // Set parameters
+ Map params = new HashMap();
+ params.put(PARAM_CLIENT_ID, redditApp.getClientID());
+ params.put(PARAM_RESPONSE_TYPE, "code");
+ params.put(PARAM_STATE, UUID.randomUUID().toString());
+ params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI());
+ params.put(PARAM_DURATION, duration.value());
+ params.put(PARAM_SCOPE, scopeBuilder.build());
+
+ // Create URI
+ return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true);
+
+ }
+
+ /**
+ * Generate the implicit flow Uniform Resource Locator (URI) for a
+ * reddit user to authorize your application.
+ *
+ * The user will, after authorization, receive token information. This can be turned into
+ * a RedditToken using {@link #tokenFromInfo(String, String, long, String)}.
+ *
+ * @param scopeBuilder Authorization scope builder (must not be null)
+ *
+ * @return The URI users need to visit and retrieve the token information from
+ *
+ * @see {@link #tokenFromInfo(String, String, long, String)} for converting the
+ * token information into RedditToken
+ */
+ public synchronized String generateImplicitFlowURI(RedditScopeBuilder scopeBuilder) {
+
+ // Set parameters
+ Map params = new HashMap();
+ params.put(PARAM_CLIENT_ID, redditApp.getClientID());
+ params.put(PARAM_RESPONSE_TYPE, "token");
+ params.put(PARAM_STATE, UUID.randomUUID().toString());
+ params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI());
+ params.put(PARAM_SCOPE, scopeBuilder.build());
+
+ // Create URI
+ return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true);
+
+ }
+
+ /**
+ * Add a user agent to the OAuth request.
+ *
+ * @param request OAuth request
+ */
+ private void addUserAgent(OAuthClientRequest request) {
+ request.addHeader(HEADER_USER_AGENT, userAgent);
+ }
+
+ /**
+ * Add the basic authentication protocol to the OAuth request using
+ * the credentials of the Reddit application provided.
+ *
+ * @param request OAuth request
+ * @param app Reddit application
+ */
+ private void addBasicAuthentication(OAuthClientRequest request, RedditApp app) {
+ String authString = app.getClientID() + ":" + app.getClientSecret();
+ String authStringEnc = DatatypeConverter.printBase64Binary(authString.getBytes());
+ request.addHeader(HEADER_AUTHORIZATION, "Basic " + authStringEnc);
+ }
+
+ /**
+ * Token retrieval (code flow).
+ *
+ * Retrieve a token for a specific user, meaning that the token is coupled to a user.
+ * After it has expired, the token will no longer work. You must either request a new
+ * token, or refresh it using {@link #refreshToken(RedditToken)}.
+ *
+ * @param code One-time code received from the user, after manual authorization by that user
+ *
+ * @return Token (associated with a user)
+ *
+ * @throws RedditOAuthException
+ */
+ public synchronized RedditToken token(String code) throws RedditOAuthException {
+
+ try {
+
+ // Set general values of the request
+ OAuthClientRequest request = OAuthClientRequest
+ .tokenProvider(OAuthProviderType.REDDIT)
+ .setGrantType(GrantType.AUTHORIZATION_CODE)
+ .setClientId(redditApp.getClientID())
+ .setClientSecret(redditApp.getClientSecret())
+ .setRedirectURI(redditApp.getRedirectURI())
+ .setParameter(PARAM_CODE, code)
+ .buildBodyMessage();
+
+ // Add the user agent
+ addUserAgent(request);
+
+ // Add basic authentication
+ addBasicAuthentication(request, redditApp);
+
+ // Return a wrapper controlled by jReddit
+ return new RedditToken(oAuthClient.accessToken(request));
+
+ } catch (OAuthSystemException oase) {
+ throw new RedditOAuthException(oase);
+ } catch (OAuthProblemException oape) {
+ throw new RedditOAuthException(oape);
+ }
+
+ }
+
+ /**
+ * Refresh token.
+ *
+ * This is only possible for tokens retrieved through the code flow
+ * authorization and had their duration set to permanent. Tokens that do not have
+ * a refresh_token with them or are expired, will not be able to be refreshed.
+ * In that case, a new one must be acquired.
+ *
+ * @param rToken Reddit token (which needs to be refreshed)
+ *
+ * @return Whether the token was successfully refreshed
+ *
+ * @throws RedditOAuthException
+ *
+ * @see RedditToken#isRefreshable()
+ */
+ public synchronized boolean refreshToken(RedditToken rToken) throws RedditOAuthException {
+
+ try {
+
+ // Check whether the token can be refreshed
+ if (rToken.isRefreshable()) {
+
+ // Set general values of the request
+ OAuthClientRequest request = OAuthClientRequest
+ .tokenProvider(OAuthProviderType.REDDIT)
+ .setGrantType(GrantType.REFRESH_TOKEN)
+ .setRefreshToken(rToken.getRefreshToken())
+ .buildBodyMessage();
+
+ // Add the user agent
+ addUserAgent(request);
+
+ // Add basic authentication
+ addBasicAuthentication(request, redditApp);
+
+ // Return a wrapper controlled by jReddit
+ rToken.refresh(oAuthClient.accessToken(request));
+
+ return true;
+
+ } else {
+
+ // The token cannot be refreshed
+ return false;
+
+ }
+
+
+ } catch (OAuthSystemException oase) {
+ throw new RedditOAuthException(oase);
+ } catch (OAuthProblemException oape) {
+ throw new RedditOAuthException(oape);
+ }
+
+ }
+
+ /**
+ * Token retrieval (app-only flow).
+ *
+ * Retrieve a token for the application-only, meaning that the
+ * token is not coupled to any user. The token is typically only
+ * valid for a short period of time (at the moment of writing: 1 hour).
+ * After it has expired, the token will no longer work. You must request a new
+ * token in that case. Refreshing an application-only token is not possible.
+ *
+ * @param confidential True: confidential clients (web apps / scripts) not acting on
+ * behalf of one or more logged out users. False: installed app types,
+ * and other apps acting on behalf of one or more logged out users.
+ *
+ * @return Token (not associated with a user)
+ *
+ * @throws RedditOAuthException
+ */
+ public synchronized RedditToken tokenAppOnly(boolean confidential) throws RedditOAuthException {
+
+ try {
+
+ // Set general values of the request
+ TokenRequestBuilder builder = OAuthClientRequest
+ .tokenProvider(OAuthProviderType.REDDIT)
+ .setParameter(PARAM_GRANT_TYPE, confidential ? GRANT_TYPE_CLIENT_CREDENTIALS : GRANT_TYPE_INSTALLED_CLIENT)
+ .setClientId(redditApp.getClientID())
+ .setClientSecret(redditApp.getClientSecret());
+
+ // If it is not acting on behalf of a unique client, a unique device identifier must be generated:
+ if (!confidential) {
+ builder = builder.setParameter(PARAM_DEVICE_ID, UUID.randomUUID().toString());
+ }
+
+ // Build the request
+ OAuthClientRequest request = builder.buildBodyMessage();
+
+ // Add the user agent
+ addUserAgent(request);
+
+ // Add basic authentication
+ addBasicAuthentication(request, redditApp);
+
+ // Return a wrapper controlled by jReddit
+ return new RedditToken(oAuthClient.accessToken(request));
+
+ } catch (OAuthSystemException oase) {
+ throw new RedditOAuthException(oase);
+ } catch (OAuthProblemException oape) {
+ throw new RedditOAuthException(oape);
+ }
+
+ }
+
+ /**
+ * Generate a token from information received using the implicit grant flow.
+ *
+ * WARNING: The expiration of the token is no longer very accurate. There is a delay
+ * between the user receiving the token, and inputting it into this function. Beware that the
+ * token might expire earlier than that the token reports it to.
+ *
+ * @param accessToken Access token
+ * @param tokenType Token type (commonly "bearer")
+ * @param expiresIn Expires in (seconds)
+ * @param scope Scope
+ *
+ * @return RedditToken generated using the given information.
+ */
+ public synchronized RedditToken tokenFromInfo(String accessToken, String tokenType, long expiresIn, String scope) {
+ return new RedditToken(accessToken, tokenType, expiresIn, scope);
+ }
+
+ /**
+ * Revocation of a RedditToken.
+ *
+ * Be sure to not use the token after
+ * calling this function, as its state pertaining its validity (e.g. scope,
+ * expiration, refreshability) is no longer valid when it is revoked.
+ *
+ * Note: Per RFC 7009, this request will return a success (204) response
+ * even if the passed in token was never valid.
+ *
+ * @param token RedditToken to revoke
+ * @param revokeAccessTokenOnly Whether to only revoke the access token, or both
+ *
+ * @return Whether the token is no longer valid
+ */
+ public boolean revoke(RedditToken token, boolean revokeAccessTokenOnly) {
+ // TODO: Implement
+ // https://www.reddit.com/api/v1/revoke_token
+ // In POST data: token=TOKEN&token_type_hint=TOKEN_TYPE
+ // TOKEN_TYPE: refresh_token or access_token
+ //
+ return true;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/RedditToken.java b/src/main/com/github/jreddit/oauth/RedditToken.java
new file mode 100644
index 0000000..8f41e55
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/RedditToken.java
@@ -0,0 +1,187 @@
+package com.github.jreddit.oauth;
+
+import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
+
+import com.github.jreddit.oauth.param.RedditScope;
+import com.github.jreddit.oauth.param.RedditTokenCompleteScope;
+
+/**
+ * OAuth2 token wrapper for reddit.
+ *
+ * This class wraps the information received from reddit following
+ * the request for a token.
+ * A token has three dimensions:
+ *
+ * - scope: what can it be used for?
+ *
- expiration: how long can it be used?
+ *
- refreshable: can its duration be prolonged?
+ *
+ *
+ * @author Simon Kassing
+ *
+ */
+public class RedditToken {
+
+ /** Token type parameter. */
+ public static final String PARAM_TOKEN_TYPE = "token_type";
+
+ /** Access token. */
+ private String accessToken;
+
+ /** Refresh token. */
+ private String refreshToken;
+
+ /** Manager of the scopes that this token applies to. */
+ private RedditTokenCompleteScope scopes;
+
+ /** Token type. Only value possible (15-06-2015): bearer */
+ private String tokenType;
+
+ /** Time at which the token expires (seconds since UNIX epoch). */
+ private long expiration;
+
+ /** How long the token was valid starting from its creation (in seconds). */
+ private long expirationSpan;
+
+ /**
+ * @param token JSON response after an OAuth2 token request
+ */
+ protected RedditToken(OAuthJSONAccessTokenResponse token) {
+ this.accessToken = token.getAccessToken();
+ this.refreshToken = token.getRefreshToken();
+ this.expiration = currentTimeSeconds() + token.getExpiresIn();
+ this.expirationSpan = token.getExpiresIn();
+ this.scopes = new RedditTokenCompleteScope(token.getScope());
+ this.tokenType = token.getParam(PARAM_TOKEN_TYPE);
+ }
+
+ /**
+ * Reddit token constructor with specific user-provided parameters.
+ *
+ * @param accessToken Access token
+ * @param tokenType Type of token (commonly "bearer")
+ * @param expiresIn Expires in (seconds)
+ * @param scope Scope
+ */
+ protected RedditToken(String accessToken, String tokenType, long expiresIn, String scope) {
+ this.accessToken = accessToken;
+ this.refreshToken = null;
+ this.expiration = currentTimeSeconds() + expiresIn;
+ this.expirationSpan = expiresIn;
+ this.scopes = new RedditTokenCompleteScope(scope);
+ this.tokenType = tokenType;
+ }
+
+ /**
+ * Refresh this reddit token with data received from the new token.
+ *
+ * @param token Token received from a refresh request to reddit
+ */
+ public void refresh(OAuthJSONAccessTokenResponse token) {
+ this.accessToken = token.getAccessToken();
+ this.expiration = currentTimeSeconds() + token.getExpiresIn();
+ this.expirationSpan = token.getExpiresIn();
+ this.scopes = new RedditTokenCompleteScope(token.getScope());
+ this.tokenType = token.getParam(PARAM_TOKEN_TYPE);
+ }
+
+ /**
+ * Retrieve the access token.
+ *
+ * @return Access token (e.g. "jsdkjfskaj9f8-dnafk")
+ */
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ /**
+ * Retrieve the refresh token.
+ *
+ * @return Refresh token (e.g. "nvkJu9kjdada2-d98afj")
+ */
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ /**
+ * Retrieve whether the token is expired.
+ *
+ * @return Is the token expired?
+ */
+ public boolean isExpired() {
+ return expiration < currentTimeSeconds();
+ }
+
+ /**
+ * Check whether this token possess this particular authorization scope.
+ * If it does not support the scope, it means that the token does
+ * not have approval from the user to perform the actions belonging to
+ * that scope.
+ *
+ * @param scope Reddit scope
+ *
+ * @return Does this token support this scope?
+ */
+ public boolean hasScope(RedditScope scope) {
+ return this.scopes.has(scope);
+ }
+
+ /**
+ * Retrieve the expiration.
+ *
+ * @return Expiration time (seconds since UNIX epoch)
+ */
+ public long getExpiration() {
+ return expiration;
+ }
+ /**
+ * Retrieve the token type.
+ *
+ * @return Token type (e.g. "bearer")
+ */
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ /**
+ * Check whether the token can be refreshed.
+ * This means that a refresh token exists.
+ *
+ * @return Can the token be refreshed?
+ */
+ public boolean isRefreshable() {
+ return this.getRefreshToken() != null;
+ }
+
+ /**
+ * Check whether this token will expire within the given time frame (given in seconds).
+ *
+ * @param seconds Amount of seconds
+ *
+ * @return Will the token expire within the given time frame?
+ */
+ public boolean willExpireIn(long seconds) {
+ return currentTimeSeconds() + seconds > expiration;
+ }
+
+ /**
+ * Retrieve the original expiration span of the token starting from its creation.
+ *
+ * @return Expiration span (in seconds)
+ */
+ public long getExpirationSpan() {
+ return expirationSpan;
+ }
+
+ /**
+ * Retrieve the current time in seconds.
+ *
+ * Uses System.currentTimeMillis().
+ *
+ * @return Current time in seconds.
+ */
+ private static long currentTimeSeconds() {
+ return System.currentTimeMillis() / (long) 1000;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/app/RedditApp.java b/src/main/com/github/jreddit/oauth/app/RedditApp.java
new file mode 100644
index 0000000..9a09083
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/app/RedditApp.java
@@ -0,0 +1,52 @@
+package com.github.jreddit.oauth.app;
+
+public abstract class RedditApp {
+
+ private final String clientID;
+ private final String clientSecret;
+ private final String redirectURI;
+
+ /**
+ * Reddit Application.
+ *
+ * All information given in this constructor must
+ * match the information stated on reddit.
+ *
+ * @param clientID Client identifier (e.g. "p_jcolKysdMFud")
+ * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s")
+ * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth")
+ */
+ public RedditApp(String clientID, String clientSecret, String redirectURI) {
+ this.clientID = clientID;
+ this.clientSecret = clientSecret;
+ this.redirectURI = redirectURI;
+ }
+
+ /**
+ * Retrieve client identifier.
+ *
+ * @return Client identifier (e.g. "p_jcolKysdMFud")
+ */
+ public String getClientID() {
+ return clientID;
+ }
+
+ /**
+ * Retrieve client secret.
+ *
+ * @return Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s")
+ */
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ /**
+ * Retrieve redirect Uniform Resource Identifier.
+ *
+ * @return Redirect URI (e.g. "http://www.example.com/auth")
+ */
+ public String getRedirectURI() {
+ return redirectURI;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java b/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java
new file mode 100644
index 0000000..7f1217a
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/app/RedditInstalledApp.java
@@ -0,0 +1,19 @@
+package com.github.jreddit.oauth.app;
+
+public class RedditInstalledApp extends RedditApp {
+
+ /**
+ * Reddit Installed Application.
+ *
+ * All information given in this constructor must
+ * match the information stated on reddit. The secret is defaulted
+ * to an empty string.
+ *
+ * @param clientID Client identifier (e.g. "p_jcolKysdMFud")
+ * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth")
+ */
+ public RedditInstalledApp(String clientID, String redirectURI) {
+ super(clientID, "", redirectURI); // Empty string is the secret for an installed app
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java b/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java
new file mode 100644
index 0000000..295f792
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/app/RedditScriptApp.java
@@ -0,0 +1,19 @@
+package com.github.jreddit.oauth.app;
+
+public class RedditScriptApp extends RedditApp {
+
+ /**
+ * Reddit Script Application.
+ *
+ * All information given in this constructor must
+ * match the information stated on reddit.
+ *
+ * @param clientID Client identifier (e.g. "p_jcolKysdMFud")
+ * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s")
+ * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth")
+ */
+ public RedditScriptApp(String clientID, String clientSecret, String redirectURI) {
+ super(clientID, clientSecret, redirectURI);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/app/RedditWebApp.java b/src/main/com/github/jreddit/oauth/app/RedditWebApp.java
new file mode 100644
index 0000000..e3bbc7f
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/app/RedditWebApp.java
@@ -0,0 +1,19 @@
+package com.github.jreddit.oauth.app;
+
+public class RedditWebApp extends RedditApp {
+
+ /**
+ * Reddit Web Application.
+ *
+ * All information given in this constructor must
+ * match the information stated on reddit.
+ *
+ * @param clientID Client identifier (e.g. "p_jcolKysdMFud")
+ * @param clientSecret Client secret (e.g. "gko_LXEJKF89djs98fhFJkj9s")
+ * @param redirectURI Redirect URI (e.g. "http://www.example.com/auth")
+ */
+ public RedditWebApp(String clientID, String clientSecret, String redirectURI) {
+ super(clientID, clientSecret, redirectURI);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/client/RedditClient.java b/src/main/com/github/jreddit/oauth/client/RedditClient.java
new file mode 100644
index 0000000..aeede20
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/client/RedditClient.java
@@ -0,0 +1,47 @@
+package com.github.jreddit.oauth.client;
+
+import com.github.jreddit.oauth.RedditToken;
+import com.github.jreddit.request.RedditGetRequest;
+import com.github.jreddit.request.RedditPostRequest;
+
+public abstract class RedditClient {
+
+ /** API Domain of OAuth */
+ public static final String OAUTH_API_DOMAIN = "https://oauth.reddit.com";
+
+ /**
+ * Perform a POST reddit request authenticated with the given reddit token.
+ *
+ * Does the following: (a) generates the URI (including query parameters) and appends
+ * it to the reddit base, (b) adds the POST body parameters,
+ * (c) adds the authorization from the token to the request, and
+ * (d) executes the request.
+ *
+ * Exception handling: if any function raises an exception,
+ * it will be logged using SLF4J. The result would be null.
+ *
+ * @param rToken Reddit token
+ * @param request Reddit POST request
+ *
+ * @return Response from reddit (raw), if failed null
+ */
+ public abstract String post(RedditToken rToken, RedditPostRequest request);
+
+ /**
+ * Perform a GET reddit request authenticated with the given reddit token.
+ *
+ * Does the following: (a) generates the URI (including query parameters) and appends
+ * it to the reddit base, (b) adds the authorization from the token to
+ * the request, and (c) executes the request.
+ *
+ * Exception handling: if any function raises an exception,
+ * it will be logged using SLF4J. The result would be null.
+ *
+ * @param rToken Reddit token
+ * @param request Reddit GET request
+ *
+ * @return Response from reddit (raw), if failed null
+ */
+ public abstract String get(RedditToken rToken, RedditGetRequest request);
+
+}
diff --git a/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java b/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java
new file mode 100644
index 0000000..512f059
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/client/RedditHttpClient.java
@@ -0,0 +1,141 @@
+package com.github.jreddit.oauth.client;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.oauth.RedditToken;
+import com.github.jreddit.request.RedditGetRequest;
+import com.github.jreddit.request.RedditPostRequest;
+
+/**
+ * HTTP client implementation for a RedditClient.
+ *
+ * @author Simon Kassing
+ *
+ * @see RedditClient
+ */
+public class RedditHttpClient extends RedditClient {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RedditHttpClient.class);
+
+ private final String userAgent;
+
+ private final HttpClient httpClient;
+
+ /**
+ * @param userAgent User agent of your application
+ * @param httpClient HTTP client to use for the requests
+ */
+ public RedditHttpClient(String userAgent, HttpClient httpClient) {
+ this.userAgent = userAgent;
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public String post(RedditToken rToken, RedditPostRequest redditRequest) {
+
+ try {
+
+ // Create post request
+ HttpPost request = new HttpPost(OAUTH_API_DOMAIN + redditRequest.generateRedditURI());
+
+ // Add parameters to body
+ request.setEntity(new StringEntity(redditRequest.generateBody()));
+
+ // Add authorization
+ addAuthorization(request, rToken);
+
+ // Add user agent
+ addUserAgent(request);
+
+ // Add content type
+ request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+
+ return executeHttpRequest(request);
+
+ } catch (UnsupportedEncodingException uee) {
+ LOGGER.warn("Unsupported Encoding Exception thrown in POST request when encoding body", uee);
+ }
+
+ return null;
+
+ }
+
+ @Override
+ public String get(RedditToken rToken, RedditGetRequest redditRequest) {
+
+ // Create get request
+ HttpGet request = new HttpGet(OAUTH_API_DOMAIN + redditRequest.generateRedditURI());
+
+ // Add authorization
+ addAuthorization(request, rToken);
+
+ // Add user agent
+ addUserAgent(request);
+
+ return executeHttpRequest(request);
+
+ }
+
+ /**
+ * Execute the given HTTP request.
+ *
+ * @param request HTTP request
+ *
+ * @return Result, null if failed
+ */
+ private String executeHttpRequest(HttpUriRequest request) {
+ try {
+
+ // Attempt to do execute request
+ HttpResponse response = httpClient.execute(request);
+
+ // Return response if successful
+ if (response != null) {
+ return EntityUtils.toString(response.getEntity(), "UTF-8");
+ }
+
+ } catch (UnsupportedEncodingException uee) {
+ LOGGER.warn("Unsupported Encoding Exception thrown in request", uee);
+ } catch (ClientProtocolException cpe) {
+ LOGGER.warn("Client Protocol Exception thrown in request", cpe);
+ } catch (IOException ioe) {
+ LOGGER.warn("I/O Exception thrown in request", ioe);
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Add authorization to the HTTP request.
+ *
+ * @param request HTTP request
+ * @param rToken Reddit token (generally of the "bearer" type)
+ */
+ private void addAuthorization(HttpRequest request, RedditToken rToken) {
+ request.addHeader("Authorization", rToken.getTokenType() + " " + rToken.getAccessToken());
+ }
+
+ /**
+ * Add user agent to the HTTP request.
+ *
+ * @param request HTTP request
+ */
+ private void addUserAgent(HttpRequest request) {
+ request.addHeader("User-Agent", userAgent);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java b/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java
new file mode 100644
index 0000000..5bfeea8
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/client/RedditPoliteClient.java
@@ -0,0 +1,101 @@
+package com.github.jreddit.oauth.client;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.oauth.RedditToken;
+import com.github.jreddit.request.RedditGetRequest;
+import com.github.jreddit.request.RedditPostRequest;
+
+/**
+ * Wrapper for any reddit client, which makes it polite.
+ * Polite means that it will only send requests in an interval,
+ * and does not overload reddit. It will also mean you will get
+ * less denial messages from reddit *hint* *hint*.
+ *
+ * @author Simon Kassing
+ */
+public class RedditPoliteClient extends RedditClient {
+
+ /** Logger for this class. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(RedditPoliteClient.class);
+
+ /** Wrapped reddit client. */
+ private RedditClient redditClient;
+
+ /** Default interval in milliseconds (1 per second). */
+ private static final long DEFAULT_INTERVAL = 1000L;
+
+ /** Default interval in milliseconds. */
+ private long interval;
+
+ /** Last time a request was made. */
+ private long lastReqTime = 0;
+
+ /**
+ * Polite wrapper around the reddit client.
+ *
+ * @param redditClient Reddit client to wrap
+ */
+ public RedditPoliteClient(RedditClient redditClient) {
+ this(redditClient, DEFAULT_INTERVAL);
+ }
+
+ /**
+ * Polite wrapper around the reddit client with configurable time.
+ *
+ * @param redditClient Reddit client to wrap
+ * @param interval Interval in milliseconds
+ */
+ public RedditPoliteClient(RedditClient redditClient, long interval) {
+ this.redditClient = redditClient;
+ this.interval = interval;
+ }
+
+ @Override
+ public String post(RedditToken rToken, RedditPostRequest request) {
+ waitIfNeeded();
+ String result = redditClient.post(rToken, request);
+ noteTime();
+ return result;
+ }
+
+ @Override
+ public String get(RedditToken rToken, RedditGetRequest request) {
+ waitIfNeeded();
+ String result = redditClient.get(rToken, request);
+ noteTime();
+ return result;
+ }
+
+ /**
+ * Note the current time.
+ */
+ private void noteTime() {
+ lastReqTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Wait if required.
+ */
+ private void waitIfNeeded() {
+
+ // Calculate elapsed milliseconds
+ long elapsed = System.currentTimeMillis() - lastReqTime;
+
+ // If enough time has elapsed, no need to wait
+ if (elapsed >= interval) {
+ return;
+ }
+
+ // If not enough time was elapsed, wait the remainder
+ long toWait = interval - elapsed;
+ try {
+ Thread.sleep(toWait);
+ } catch (InterruptedException ie) {
+ LOGGER.warn("Interrupted Exception thrown while politely waiting for remainder of interval", ie);
+ }
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java b/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java
new file mode 100644
index 0000000..cbde8eb
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/exception/RedditOAuthException.java
@@ -0,0 +1,18 @@
+package com.github.jreddit.oauth.exception;
+
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
+
+public class RedditOAuthException extends Exception {
+
+ private static final long serialVersionUID = 2403104136770312353L;
+
+ public RedditOAuthException(OAuthSystemException e) {
+ super("A OAuth system exception was thrown when authenticating with reddit.", e);
+ }
+
+ public RedditOAuthException(OAuthProblemException e) {
+ super("A OAuth problem exception was thrown when authenticating with reddit.", e);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/param/RedditDuration.java b/src/main/com/github/jreddit/oauth/param/RedditDuration.java
new file mode 100644
index 0000000..fd60cc1
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/param/RedditDuration.java
@@ -0,0 +1,27 @@
+package com.github.jreddit.oauth.param;
+
+/**
+ * Enumerator for the duration of tokens.
+ *
+ * There are two possible values:
+ *
+ * - permanent: The token can be refreshed as many times as the application desires.
+ * - temporary: The token cannot be refreshed, and will last for one hour.
+ *
+ */
+public enum RedditDuration {
+
+ PERMANENT("permanent"),
+ TEMPORARY("temporary");
+
+ private final String value;
+
+ RedditDuration(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/param/RedditScope.java b/src/main/com/github/jreddit/oauth/param/RedditScope.java
new file mode 100644
index 0000000..c20767c
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/param/RedditScope.java
@@ -0,0 +1,48 @@
+package com.github.jreddit.oauth.param;
+
+/**
+ * Enumerator for the possible authorization scopes for reddit requests.
+ *
+ * These scopes are used to specify what actions an application
+ * can perform with the generated token. The scope can be combined
+ * together, so that a single token can have access to multiple scopes.
+ *
+ * @see RedditScopeBuilder
+ *
+ * @author Simon Kassing
+ */
+public enum RedditScope {
+
+ IDENTITY("identity"),
+ EDIT("edit"),
+ FLAIR("flair"),
+ HISTORY("history"),
+ MODCONFIG("modconfig"),
+ MODFLAIR("modflair"),
+ MODLOG("modlog"),
+ MODPOSTS("modposts"),
+ MODWIKI("modwiki"),
+ MYSUBREDDITS("mysubreddits"),
+ PRIVATEMESSAGE("privatemessages"),
+ READ("read"),
+ REPORT("report"),
+ SAVE("save"),
+ SUBMIT("submit"),
+ SUBSCRIBE("subscribe"),
+ VOTE("vote"),
+ WIKIEDIT("wikiedit"),
+ WIKIREAD("wikiread");
+
+ protected static final String SEPARATOR = ",";
+
+ private final String value;
+
+ RedditScope(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java b/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java
new file mode 100644
index 0000000..cf135e7
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/param/RedditScopeBuilder.java
@@ -0,0 +1,100 @@
+package com.github.jreddit.oauth.param;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Builder for the scopes of a request.
+ *
+ * @author Simon Kassing
+ *
+ */
+public class RedditScopeBuilder {
+
+ /** Set of reddit scopes. */
+ private Set scopes;
+
+ public RedditScopeBuilder() {
+ scopes = new HashSet();
+ }
+
+ /**
+ * Build a string for the request. Called upon by
+ * RedditToken when requesting a token that
+ * allows scope definition.
+ *
+ * @return String list of the scopes
+ */
+ public String build() {
+
+ // Add each scope to the scope list
+ String s = "";
+ for (RedditScope scope : scopes) {
+ s += scope.value() + RedditScope.SEPARATOR;
+ }
+
+ // Remove final separator
+ if (s.length() > 0) {
+ s = s.substring(0, s.length() - RedditScope.SEPARATOR.length());
+ }
+
+ // Return scope list
+ return s;
+
+ }
+
+ /**
+ * Add a scope to the builder. If the scope has already
+ * been added, it will not be added double.
+ *
+ * @param scope Reddit scope
+ *
+ * @return This builder
+ */
+ public RedditScopeBuilder addScope(RedditScope scope) {
+ scopes.add(scope);
+ return this;
+ }
+
+ /**
+ * Add multiple scopes to the builder. If a scope has already
+ * been added, it will not be added double.
+ *
+ * @param scopes Multiple scopes
+ *
+ * @return This builder
+ */
+ public RedditScopeBuilder addScopes(RedditScope... scopes) {
+ for (RedditScope scope : scopes) {
+ addScope(scope);
+ }
+ return this;
+ }
+
+ /**
+ * Remove a scope from the builder.
+ *
+ * @param scope Reddit scope
+ *
+ * @return This builder
+ */
+ public RedditScopeBuilder removeScope(RedditScope scope) {
+ scopes.remove(scope);
+ return this;
+ }
+
+ /**
+ * Remove multiple scopes from the builder.
+ *
+ * @param scopes Multiple scopes
+ *
+ * @return This builder
+ */
+ public RedditScopeBuilder removeScopes(RedditScope... scopes) {
+ for (RedditScope scope : scopes) {
+ removeScope(scope);
+ }
+ return this;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java b/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java
new file mode 100644
index 0000000..f12b693
--- /dev/null
+++ b/src/main/com/github/jreddit/oauth/param/RedditTokenCompleteScope.java
@@ -0,0 +1,48 @@
+package com.github.jreddit.oauth.param;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Manager of the scopes a response from reddit returns.
+ * Used by RedditToken to parse the list of scopes it receives.
+ *
+ * @see {@link com.github.jreddit.oauth.RedditToken}
+ *
+ * @author Simon Kassing
+ */
+public class RedditTokenCompleteScope {
+
+ /** Set of scopes. */
+ private Set scopes;
+
+ /**
+ * @param scopes List of scopes (e.g. "flair,edit")
+ */
+ public RedditTokenCompleteScope(String scopes) {
+
+ // Create set
+ this.scopes = new HashSet();
+
+ // Split up
+ String[] split = scopes.split(RedditScope.SEPARATOR);
+
+ // Add each to the set
+ for (String s : split) {
+ this.scopes.add(s);
+ }
+
+ }
+
+ /**
+ * Check whether it has this scope.
+ *
+ * @param scope Reddit scope
+ *
+ * @return Does it have this scope?
+ */
+ public boolean has(RedditScope scope) {
+ return scopes.contains(scope.value());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/entity/Comment.java b/src/main/com/github/jreddit/parser/entity/Comment.java
new file mode 100644
index 0000000..92c7cdb
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Comment.java
@@ -0,0 +1,269 @@
+package com.github.jreddit.parser.entity;
+
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToInteger;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString;
+
+import java.util.List;
+
+import org.json.simple.JSONObject;
+
+import com.github.jreddit.parser.entity.imaginary.CommentTreeElement;
+import com.github.jreddit.parser.entity.imaginary.MixedListingElement;
+
+/**
+ * A Reddit comment. Contains the edited timestamp, the body
+ *
+ * @author Benjamin Jakobus
+ * @author Raul Rene Lepsa
+ * @author Simon Kassing
+ */
+public class Comment extends Thing implements CommentTreeElement, MixedListingElement {
+
+ private String author; // Username of the author
+ private String parentId; // Parent identifier
+ private String subreddit; // Subreddit name
+ private String subredditId; // Subreddit identifier
+ private String linkId; // Submission (aka. link) identifier
+ private String bodyHTML; // The body with HTML markup
+ private Boolean scoreHidden; // Whether the score is hidden
+ private String body; // The actual body
+ private Boolean edited; // Edited timestamp
+ private double created; // Created timestamp
+ private double createdUTC; // Created UTC timestamp
+ private List replies; // Replies if retrieved
+ private Integer gilded; // Amount of times the comment been gilded
+ private Integer score; // Karma score
+ private Integer upvotes; // Number of upvotes that this body received
+ private Integer downvotes; // Number of downvotes that this body received
+
+ // Possible fields to add as well:
+// private String bannedBy;
+// String likes;
+// private String approvedBy;
+// private String authorFlairCSSClass;
+// private String authorFlairText;
+// String num_reports = null;
+// String distinguished = null;
+
+ public Comment(JSONObject obj) {
+ super(safeJsonToString(obj.get("name")));
+
+ this.setAuthor(safeJsonToString(obj.get("author")));
+ this.setParentId(safeJsonToString(obj.get("parent_id")));
+ this.setBody(safeJsonToString(obj.get("body")));
+ this.setEdited(safeJsonToBoolean(obj.get("edited")));
+ this.setCreated(safeJsonToDouble(obj.get("created")));
+ this.setCreatedUTC(safeJsonToDouble(obj.get("created_utc")));
+ this.setGilded(safeJsonToInteger(obj.get("gilded")));
+ this.setScore(safeJsonToInteger(obj.get("score")));
+ this.setUpvotes(safeJsonToInteger(obj.get("ups")));
+ this.setDownvotes(safeJsonToInteger(obj.get("downs")));
+ this.setSubreddit(safeJsonToString(obj.get("subreddit")));
+ this.setSubredditId(safeJsonToString(obj.get("subreddit_id")));
+ this.setLinkId(safeJsonToString(obj.get("link_id")));
+ this.setBodyHTML(safeJsonToString(obj.get("body_html")));
+ this.setScoreHidden(safeJsonToBoolean(obj.get("score_hidden")));
+
+ }
+
+ /**
+ * Retrieve the replies of this comment.
+ *
+ * @return Replies (will always initialized, and empty if there are no replies)
+ */
+ public List getReplies() {
+ return this.replies;
+ }
+
+ /**
+ * Set the replies of this comment.
+ *
+ * @param replies Comment tree of replies
+ */
+ public void setReplies(List replies) {
+ this.replies = replies;
+ }
+
+ /**
+ * Retrieve the username of the author.
+ *
+ * @return Username of the author
+ */
+ public String getAuthor() {
+ return author;
+ }
+
+ /**
+ * Set the username of the author
+ *
+ * @param author Username of the author
+ */
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getParentId() {
+ return parentId;
+ }
+
+ public void setParentId(String parentId) {
+ this.parentId = parentId;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public Boolean getEdited() {
+ return edited;
+ }
+
+ public void setEdited(Boolean edited) {
+ this.edited = edited;
+ }
+
+ public double getCreated() {
+ return created;
+ }
+
+ public void setCreated(double created) {
+ this.created = created;
+ }
+
+ public Integer getGilded() {
+ return gilded;
+ }
+
+ public void setGilded(Integer gilded) {
+ this.gilded = gilded;
+ }
+
+ public Integer getUpvotes() {
+ return upvotes;
+ }
+
+ public void setUpvotes(Integer upvotes) {
+ this.upvotes = upvotes;
+ }
+
+ public Integer getDownvotes() {
+ return downvotes;
+ }
+
+ public void setDownvotes(Integer downvotes) {
+ this.downvotes = downvotes;
+ }
+
+ public double getCreatedUTC() {
+ return createdUTC;
+ }
+
+ public void setCreatedUTC(double createdUTC) {
+ this.createdUTC = createdUTC;
+ }
+
+ public Integer getScore() {
+ return score;
+ }
+
+ public void setScore(Integer score) {
+ this.score = score;
+ }
+
+ /**
+ * @return the subreddit
+ */
+ public String getSubreddit() {
+ return subreddit;
+ }
+
+ /**
+ * @param subreddit the subreddit to set
+ */
+ public void setSubreddit(String subreddit) {
+ this.subreddit = subreddit;
+ }
+
+ /**
+ * @return the subredditId
+ */
+ public String getSubredditId() {
+ return subredditId;
+ }
+
+ /**
+ * @param subredditId the subredditId to set
+ */
+ public void setSubredditId(String subredditId) {
+ this.subredditId = subredditId;
+ }
+
+ /**
+ * @return the linkId
+ */
+ public String getLinkId() {
+ return linkId;
+ }
+
+ /**
+ * @param linkId the linkId to set
+ */
+ public void setLinkId(String linkId) {
+ this.linkId = linkId;
+ }
+
+ /**
+ * @return the bodyHTML
+ */
+ public String getBodyHTML() {
+ return bodyHTML;
+ }
+
+ /**
+ * @param bodyHTML the bodyHTML to set
+ */
+ public void setBodyHTML(String bodyHTML) {
+ this.bodyHTML = bodyHTML;
+ }
+
+ /**
+ * @return the scoreHidden
+ */
+ public Boolean isScoreHidden() {
+ return scoreHidden;
+ }
+
+ /**
+ * @param scoreHidden the scoreHidden to set
+ */
+ public void setScoreHidden(Boolean scoreHidden) {
+ this.scoreHidden = scoreHidden;
+ }
+
+ @Override
+ public String toString() {
+ return "Comment(" + identifier + ")<" + ((body.length() > 10) ? body.substring(0, 10) : body) + ">";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof Comment && this.getFullName().equals(((Comment) other).getFullName());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode() * this.getFullName().hashCode();
+ }
+
+ @Override
+ public int compareTo(Thing o) {
+ return this.getFullName().compareTo(o.getFullName());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/entity/Kind.java b/src/main/com/github/jreddit/parser/entity/Kind.java
new file mode 100644
index 0000000..68a64a3
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Kind.java
@@ -0,0 +1,44 @@
+package com.github.jreddit.parser.entity;
+
+/**
+ * Enumeration to represent the different types of Things.
+ */
+public enum Kind {
+
+ COMMENT("t1"), ACCOUNT("t2"), LINK("t3"), MESSAGE("t4"), SUBREDDIT("t5"), AWARD("t6"), PROMO_CAMPAIGN("t8"), MORE("more"), LISTING("listing");
+
+ private String value;
+
+ /**
+ * Type enumeration constructor.
+ * @param value String representation
+ */
+ Kind(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Retrieve the value of the type.
+ * @return Type String representation.
+ */
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Match a string with its respective kind.
+ *
+ * @param t String kind (e.g. "t1" or "t5")
+ *
+ * @return Match kind (null, it not found)
+ */
+ public static Kind match(String t) {
+ for (Kind k : Kind.values()) {
+ if (k.value().equals(t)) {
+ return k;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/entity/Message.java b/src/main/com/github/jreddit/parser/entity/Message.java
new file mode 100644
index 0000000..f31125f
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Message.java
@@ -0,0 +1,167 @@
+package com.github.jreddit.parser.entity;
+
+import org.json.simple.JSONObject;
+
+/**
+ * Encapsulates the private messages.
+ * Corresponds to the Kind.MESSAGES
, which is has the value t4 for the Reddit API
+ *
+ * @author Karan Goel
+ * @author Raul Rene Lepsa
+ * @author Simon Kassing
+ */
+public class Message {
+
+ // The ID of this message
+ private String id;
+
+ // Name - a combination of the Message Type (t4) and the ID of the message
+ private String fullName;
+
+ // Name of the author of the message
+ private String author;
+
+ // Recipient of the message
+ private String recipient;
+
+ // The body of the message
+ private String body;
+
+ // HTML version of the Body
+ private String bodyHtml;
+
+ // If the message was a comment or not
+ private boolean isComment;
+
+ // If it is a comment, it has a parent
+ private String parentId;
+
+ // Timestamp of when the message was created
+ private String created;
+
+ // UTC timestamp of when the message was created
+ private String createdUTC;
+
+ // The content of the message
+ private String context;
+
+ // The subject of the message
+ private String subject;
+
+ public Message(JSONObject jsonObject) {
+
+ this.setBody(jsonObject.get("body").toString());
+ this.setComment(Boolean.valueOf(jsonObject.get("was_comment").toString()));
+ this.setFullName(jsonObject.get("name").toString());
+ if (jsonObject.get("author") == null)
+ {
+ this.setAuthor("reddit");
+ } else {
+ this.setAuthor(jsonObject.get("author").toString());
+ }
+ this.setCreated(jsonObject.get("created").toString());
+ this.setRecipient(jsonObject.get("dest").toString());
+ this.setCreatedUTC(jsonObject.get("created_utc").toString());
+ this.setBodyHtml(jsonObject.get("body_html").toString());
+ this.setSubject(jsonObject.get("subject").toString());
+ this.setContext(jsonObject.get("context").toString());
+ this.setId(jsonObject.get("id").toString());
+
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getRecipient() {
+ return recipient;
+ }
+
+ public void setRecipient(String recipient) {
+ this.recipient = recipient;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public String getBodyHtml() {
+ return bodyHtml;
+ }
+
+ public void setBodyHtml(String bodyHtml) {
+ this.bodyHtml = bodyHtml;
+ }
+
+ public boolean isComment() {
+ return isComment;
+ }
+
+ public void setComment(boolean isComment) {
+ this.isComment = isComment;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ public String getCreated() {
+ return created;
+ }
+
+ public void setCreated(String created) {
+ this.created = created;
+ }
+
+ public String getCreatedUTC() {
+ return createdUTC;
+ }
+
+ public void setCreatedUTC(String createdUTC) {
+ this.createdUTC = createdUTC;
+ }
+
+ public String getContext() {
+ return context;
+ }
+
+ public void setContext(String context) {
+ this.context = context;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getParentId() {
+ return parentId;
+ }
+
+ public void setParentId(String parentId) {
+ this.parentId = parentId;
+ }
+}
diff --git a/src/main/com/github/jreddit/parser/entity/MessageType.java b/src/main/com/github/jreddit/parser/entity/MessageType.java
new file mode 100644
index 0000000..15965fe
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/MessageType.java
@@ -0,0 +1,25 @@
+package com.github.jreddit.parser.entity;
+
+/**
+ * Deal with different types of messages
+ *
+ * @author Raul Rene Lepsa
+ */
+public enum MessageType {
+
+ INBOX("inbox"), SENT("sent"), UNREAD("unread");
+
+ private String value;
+
+ MessageType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/src/main/com/github/jreddit/parser/entity/More.java b/src/main/com/github/jreddit/parser/entity/More.java
new file mode 100644
index 0000000..20452e1
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/More.java
@@ -0,0 +1,106 @@
+package com.github.jreddit.parser.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+import com.github.jreddit.parser.entity.imaginary.CommentTreeElement;
+import com.github.jreddit.parser.util.JsonUtils;
+
+/**
+ * MORE entity. Can only exist in a comment tree, and thus
+ * implements the CommenTreeElement interface.
+ *
+ * @author Simon Kassing
+ */
+public class More extends Thing implements CommentTreeElement {
+
+ /** List of comment identifiers (ID36) that are his children. */
+ private List children;
+
+ /** Counting number assigned by reddit (does not tell much in a comment tree). */
+ private Long count;
+
+ /** Parent comment fullname. */
+ private String parentId;
+
+ /**
+ * Construct a "More" thing.
+ *
+ * @param obj JSON object
+ */
+ public More(JSONObject obj) {
+ super(Kind.MORE.value() + "_NONE");
+
+ // The obj.get("name") and obj.get("id") are neglected, as these
+ // are already implicitly included in the children array.
+
+ // Retrieve count from JSON
+ this.count = JsonUtils.safeJsonToLong(obj.get("count"));
+
+ // Retrieve parent identifier from JSON
+ this.parentId = JsonUtils.safeJsonToString(obj.get("parent_id"));
+
+ // Iterate over children
+ this.children = new ArrayList();
+ JSONArray jsonChildren = (JSONArray) obj.get("children");
+ for (Object child : jsonChildren) {
+ this.children.add(JsonUtils.safeJsonToString(child));
+ }
+
+ }
+
+ /**
+ * Retrieve the children ID36 comment identifiers.
+ *
+ * @return The children (e.g. ["dja241", "dsfjak24"])
+ */
+ public List getChildren() {
+ return children;
+ }
+
+ /**
+ * Retrieve the counting number assigned by reddit (does not tell much in this case).
+ *
+ * @return The counting number
+ */
+ public Long getCount() {
+ return count;
+ }
+
+ /**
+ * Retrieve how many children (comments) the MORE hides.
+ *
+ * @return How many comments the more hides ({@link #getChildren()}'s size)
+ */
+ public int getChildrenSize() {
+ return children.size();
+ }
+
+ /**
+ * Retrieve the parent fullname comment identifier.
+ *
+ * @return The parent identifier (e.g. "t1_38942f")
+ */
+ public String getParentId() {
+ return parentId;
+ }
+
+ @Override
+ public int compareTo(Thing o) {
+ if (!(o instanceof More)) {
+ return 1;
+ } else {
+ return ((More) o).getChildren().equals(this.getChildren()) ? 0 : -1;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return "More()<" + this.getChildrenSize() + " more directly underneath>";
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/entity/Submission.java b/src/main/com/github/jreddit/parser/entity/Submission.java
new file mode 100644
index 0000000..7c02e00
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Submission.java
@@ -0,0 +1,546 @@
+package com.github.jreddit.parser.entity;
+
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToLong;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString;
+
+import org.json.simple.JSONObject;
+
+import com.github.jreddit.parser.entity.imaginary.MixedListingElement;
+
+
+/**
+ * This class represents a vote on a link submission on Reddit.
+ *
+ * @author Omer Elnour
+ * @author Andrei Sfat
+ * @author Raul Rene Lepsa
+ * @author Jonny Krysh
+ * @author Danny Tsegai
+ * @author Simon Kassing
+ */
+public class Submission extends Thing implements MixedListingElement {
+
+ /* All String values */
+ private String url;
+ private String permalink;
+ private String author;
+ private String title;
+ private String subreddit;
+ private String subredditId;
+ private String thumbnail;
+ private String selftext;
+ private String selftextHTML;
+ private String domain;
+ private String bannedBy;
+ private String approvedBy;
+ private String authorFlairCSSClass;
+ private String linkFlairCSSClass;
+ private String authorFlairText;
+ private String linkFlairText;
+ private String distinguished;
+ private String from;
+ private String fromId;
+ private String removalReason;
+ private String fromKind;
+
+ /* All Long values */
+ private Long gilded;
+ private Long commentCount;
+ private Long reportCount;
+ private Long score;
+ private Long upVotes;
+ private Long downVotes;
+
+ /* All Double values */
+ private Double created;
+ private Double createdUTC;
+ private Double upvoteRatio;
+
+ /* All Boolean values */
+ private Boolean visited;
+ private Boolean self;
+ private Boolean saved;
+ private Boolean edited;
+ private Boolean stickied;
+ private Boolean nsfw;
+ private Boolean hidden;
+ private Boolean clicked;
+ private Boolean likes;
+
+ /**
+ * Create a Submission from a JSONObject
+ *
+ * @param obj The JSONObject to load Submission data from
+ */
+ public Submission(JSONObject obj) {
+ super(safeJsonToString(obj.get("name")));
+
+ setURL(safeJsonToString(obj.get("url")));
+ setPermalink(safeJsonToString(obj.get("permalink")));
+ setAuthor(safeJsonToString(obj.get("author")));
+ setTitle(safeJsonToString(obj.get("title")));
+ setSubreddit(safeJsonToString(obj.get("subreddit")));
+ setSubredditId(safeJsonToString(obj.get("subreddit_id")));
+ setThumbnail(safeJsonToString(obj.get("thumbnail")));
+ setSelftext(safeJsonToString(obj.get("selftext")));
+ setSelftextHTML(safeJsonToString(obj.get("selftext_html")));
+ setDomain(safeJsonToString(obj.get("domain")));
+ setBannedBy(safeJsonToString(obj.get("banned_by")));
+ setApprovedBy(safeJsonToString(obj.get("approved_by")));
+ setAuthorFlairCSSClass(safeJsonToString(obj.get("author_flair_css_class")));
+ setLinkFlairCSSClass(safeJsonToString(obj.get("link_flair_css_class")));
+ setDistinguished(safeJsonToString(obj.get("distinguished")));
+ setAuthorFlairText(safeJsonToString(obj.get("author_flair_text")));
+ setLinkFlairText(safeJsonToString(obj.get("link_flair_text")));
+ setFrom(safeJsonToString(obj.get("from")));
+ setFromId(safeJsonToString(obj.get("from_id")));
+ setRemovalReason(safeJsonToString(obj.get("removal_reason")));
+ setFromKind(safeJsonToString(obj.get("from_kind")));
+
+ setGilded(safeJsonToLong(obj.get("gilded")));
+ setCommentCount(safeJsonToLong(obj.get("num_comments")));
+ setReportCount(safeJsonToLong(obj.get("num_reports")));
+ setScore(safeJsonToLong(obj.get("score")));
+ setUpVotes(safeJsonToLong(obj.get("ups")));
+ setDownVotes(safeJsonToLong(obj.get("downs")));
+
+ setCreated(safeJsonToDouble(obj.get("created")));
+ setCreatedUTC(safeJsonToDouble(obj.get("created_utc")));
+ setUpvoteRatio(safeJsonToDouble(obj.get("upvote_ratio")));
+
+ setVisited(safeJsonToBoolean(obj.get("visited")));
+ setSelf(safeJsonToBoolean(obj.get("is_self")));
+ setSaved(safeJsonToBoolean(obj.get("saved")));
+ setEdited(safeJsonToBoolean(obj.get("edited")));
+ setStickied(safeJsonToBoolean(obj.get("stickied")));
+ setNSFW(safeJsonToBoolean(obj.get("over_18")));
+ setHidden(safeJsonToBoolean(obj.get("hidden")));
+ setClicked(safeJsonToBoolean(obj.get("clicked")));
+ setLikes(safeJsonToBoolean(obj.get("likes")));
+
+ }
+
+ public String getFromKind() {
+ return fromKind;
+ }
+
+ private void setFromKind(String fromKind) {
+ this.fromKind = fromKind;
+ }
+
+ public String getRemovalReason() {
+ return removalReason;
+ }
+
+ private void setRemovalReason(String removalReason) {
+ this.removalReason = removalReason;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ private void setFrom(String from) {
+ this.from = from;
+ }
+
+ public String getFromId() {
+ return fromId;
+ }
+
+ private void setFromId(String fromId) {
+ this.fromId = fromId;
+ }
+
+ /**
+ * @return Up-vote ratio (total number of up-votes divided by total number of votes)
+ */
+ public Double getUpvoteRatio() {
+ return upvoteRatio;
+ }
+
+ private void setUpvoteRatio(Double upvoteRatio) {
+ this.upvoteRatio = upvoteRatio;
+ }
+
+
+ /**
+ * @return The CSS class of the author
+ */
+ public String getAuthorFlairCSSClass() {
+ return authorFlairCSSClass;
+ }
+
+ private void setAuthorFlairCSSClass(String authorFlairCSSClass) {
+ this.authorFlairCSSClass = authorFlairCSSClass;
+ }
+
+ /**
+ * @return The text of the flair of the author
+ */
+ public String getAuthorFlairText() {
+ return authorFlairText;
+ }
+
+ private void setAuthorFlairText(String authorFlairText) {
+ this.authorFlairText = authorFlairText;
+ }
+
+
+ /**
+ * @return Flair text of the submission ('link')
+ */
+ public String getLinkFlairText() {
+ return linkFlairText;
+ }
+
+ private void setLinkFlairText(String linkFlairText) {
+ this.linkFlairText = linkFlairText;
+ }
+
+
+ /**
+ * @return The CSS class of the submission ('link') flair
+ */
+ public String getLinkFlairCSSClass() {
+ return linkFlairCSSClass;
+ }
+
+ private void setLinkFlairCSSClass(String linkFlairCSSClass) {
+ this.linkFlairCSSClass = linkFlairCSSClass;
+ }
+
+ /**
+ * @return Whether the post is distinguished (e.g. by a "moderator")
+ */
+ public String getDistinguished() {
+ return distinguished;
+ }
+
+ private void setDistinguished(String distinguished) {
+ this.distinguished = distinguished;
+ }
+
+ /**
+ * @return Whether the user that authenticated the retrieval of this submission upvoted it
+ */
+ public Boolean getLikes() {
+ return likes;
+ }
+
+ private void setLikes(Boolean likes) {
+ this.likes = likes;
+ }
+
+ /**
+ * @return The username of the one that approved this submission to the subreddit it belongs to
+ */
+ public String getApprovedBy() {
+ return approvedBy;
+ }
+
+ private void setApprovedBy(String approvedBy) {
+ this.approvedBy = approvedBy;
+ }
+
+ /**
+ * @return Whether the user that authenticated the retrieval of this submission has hidden it
+ */
+ public Boolean isHidden() {
+ return hidden;
+ }
+
+ private void setHidden(Boolean hidden) {
+ this.hidden = hidden;
+ }
+
+ /**
+ * @return Whether the user that authenticated the retrieval of this submission has already clicked on it before
+ */
+ public Boolean isClicked() {
+ return clicked;
+ }
+
+ private void setClicked(Boolean clicked) {
+ this.clicked = clicked;
+ }
+
+ public void setUpVotes(Long upVotes) {
+ this.upVotes = upVotes;
+ }
+
+ /**
+ * @return The score (number of upvotes minus number of downvotes)
+ */
+ public Long getScore() {
+ return score;
+ }
+
+ private void setScore(Long score) {
+ this.score = score;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public void setCreatedUTC(Double createdUTC) {
+ this.createdUTC = createdUTC;
+ }
+
+ public void setDownVotes(Long downVotes) {
+ this.downVotes = downVotes;
+ }
+
+ public void setCommentCount(Long commentCount) {
+ this.commentCount = commentCount;
+ }
+
+ public void setSubreddit(String subreddit) {
+ this.subreddit = subreddit;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setURL(String url) {
+ this.url = url;
+ }
+
+ public String getURL() {
+ return url;
+ }
+
+ public String getPermalink() {
+ return permalink;
+ }
+
+ public void setPermalink(String permalink) {
+ this.permalink = permalink;
+ }
+
+ public Long getCommentCount() {
+ return commentCount;
+ }
+
+ public Long getUpVotes() {
+ return upVotes;
+ }
+
+ public Long getDownVotes() {
+ return downVotes;
+ }
+
+
+
+ public Double getCreatedUTC() {
+ return createdUTC;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ /**
+ * @return Title of the submission
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * @return Subreddit name (e.g. "programming')
+ */
+ public String getSubreddit() {
+ return subreddit;
+ }
+
+ /**
+ * @return Subreddit ID36 identifier
+ */
+ public String getSubredditId() {
+ return subredditId;
+ }
+
+ public void setSubredditId(String subredditId) {
+ this.subredditId = subredditId;
+ }
+
+ /**
+ * @return Thumbnail image URL
+ */
+ public String getThumbnail() {
+ return thumbnail;
+ }
+
+ private void setThumbnail(String thumbnail) {
+ this.thumbnail = thumbnail;
+ }
+
+ /**
+ * @return The self text of the submission (written by the author)
+ */
+ public String getSelftext() {
+ return selftext;
+ }
+
+ private void setSelftext(String selftext) {
+ this.selftext = selftext;
+ }
+
+ /**
+ * @return The self text of the submission in HTML (written by the author)
+ */
+ public String getSelftextHTML() {
+ return selftextHTML;
+ }
+
+ private void setSelftextHTML(String selftextHTML) {
+ this.selftextHTML = selftextHTML;
+ }
+
+ /**
+ * @return The domain of the URL this submission links to
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ private void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ /**
+ * @return The username of the moderator that banned this submission
+ */
+ public String getBannedBy() {
+ return bannedBy;
+ }
+
+ private void setBannedBy(String bannedBy) {
+ this.bannedBy = bannedBy;
+ }
+
+ /**
+ * @return Count of how many times this submission received gold ('was gilded')
+ */
+ public Long getGilded() {
+ return gilded;
+ }
+
+ private void setGilded(Long gilded) {
+ this.gilded = gilded;
+ }
+
+ /**
+ * @return Count of how many times this submission was reported
+ */
+ public Long getReportCount() {
+ return reportCount;
+ }
+
+ private void setReportCount(Long reportCount) {
+ this.reportCount = reportCount;
+ }
+
+ /**
+ * @return The created time-stamp (ms since Unix epoch)
+ */
+ public Double getCreated() {
+ return created;
+ }
+
+ private void setCreated(Double created) {
+ this.created = created;
+ }
+
+ /**
+ * @return Whether the user that authenticated the retrieval of this submission has already visited it before
+ */
+ public Boolean isVisited() {
+ return visited;
+ }
+
+ private void setVisited(Boolean visited) {
+ this.visited = visited;
+ }
+
+ /**
+ * @return Whether it is a self post
+ */
+ public Boolean isSelf() {
+ return self;
+ }
+
+ private void setSelf(Boolean self) {
+ this.self = self;
+ }
+
+ /**
+ * @return Whether the user that authenticated the retrieval of this submission has saved it
+ */
+ public Boolean isSaved() {
+ return saved;
+ }
+
+ private void setSaved(Boolean saved) {
+ this.saved = saved;
+ }
+
+ /**
+ * @return Whether the submission has been edited
+ */
+ public Boolean isEdited() {
+ return edited;
+ }
+
+ private void setEdited(Boolean edited) {
+ this.edited = edited;
+ }
+
+ /**
+ * @return Whether the submission is sticked to the top of the subreddit
+ */
+ public Boolean isStickied() {
+ return stickied;
+ }
+
+ private void setStickied(Boolean stickied) {
+ this.stickied = stickied;
+ }
+
+ /**
+ * @return Whether the post is Not Suited For Work (contains adult content)
+ */
+ public Boolean isNSFW() {
+ return nsfw;
+ }
+
+ private void setNSFW(Boolean nsfw) {
+ this.nsfw = nsfw;
+ }
+
+ @Override
+ public String toString() {
+ return "Submission(" + this.getFullName() + ")<" + title + ">";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof Submission && this.getFullName().equals(((Submission) other).getFullName());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode() * this.getFullName().hashCode();
+ }
+
+ @Override
+ public int compareTo(Thing o) {
+ return this.getFullName().compareTo(o.getFullName());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/com/github/jreddit/parser/entity/Subreddit.java b/src/main/com/github/jreddit/parser/entity/Subreddit.java
new file mode 100644
index 0000000..56622be
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Subreddit.java
@@ -0,0 +1,223 @@
+package com.github.jreddit.parser.entity;
+
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToBoolean;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToDouble;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToLong;
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString;
+
+import org.json.simple.JSONObject;
+
+/**
+ * Encapsulates a subreddit.
+ *
+ * @author Benjamin Jakobus
+ * @author Simon Kassing
+ */
+public class Subreddit extends Thing {
+
+ private String displayName;
+ private String title;
+ private String url;
+ private String description;
+ private String subredditType;
+
+ private double created;
+ private double createdUTC;
+
+ private Boolean nsfw;
+
+ private Long subscribers;
+
+ // Other possible fields
+
+ // Submit text HTML
+// String submit_text_html = null;
+
+ // Whether user is banned
+// Boolean user_is_banned = null;
+
+ // Submit text
+// String submit_text = "submit text for subreddit";
+
+ // Header image
+// String header_img = "http://a.thumbs.redditmedia.com/yyL5sveWcgkCPKbr.png";
+
+ // Description in HTML markup
+// String description_html = "<div>HTML description for subreddit</d>";
+
+ // Whether user is moderator
+// Boolean user_is_moderator = null;
+
+ // Header title
+// String header_title = "Header title for subreddit";
+
+ // Submit link title
+// String submit_link_label = "Submit link label";
+
+ // Accounts active
+// String accounts_active = null;
+
+ // Whether it allows public traffic
+// Boolean public_traffic = true;
+
+ // Size of header
+// JSONArray header_size = JsonHelpers.jsonArrayOf(160, 64);
+
+ // Submit text label
+// String submit_text_label = "Submit text label";
+
+ // Whether user is contributor
+// Boolean user_is_contributor = null;
+
+ // Public description
+// String public_description = "Public description of subreddit";
+
+ // Amount of minutes the comment score is hidden
+// long comment_score_hide_mins = 0;
+
+ // What types of submissions are allowed
+// String submission_type = "any";
+
+ // Whether the user is contributor
+// Boolean user_is_subscriber = null;
+
+ /**
+ * Create a Submission from a JSONObject
+ *
+ * @param obj The JSONObject to load Submission data from
+ */
+ public Subreddit(JSONObject obj) {
+ super(safeJsonToString(obj.get("name")));
+
+ setDisplayName(safeJsonToString(obj.get("display_name")));
+ setTitle(safeJsonToString(obj.get("title")));
+ setURL(safeJsonToString(obj.get("url")));
+ setCreated(safeJsonToDouble(obj.get("created")));
+ setCreatedUTC(safeJsonToDouble(obj.get("created_utc")));
+ setNSFW(safeJsonToBoolean(obj.get("over18")));
+ setSubscribers(safeJsonToLong(obj.get("subscribers")));
+ setDescription(safeJsonToString(obj.get("description")));
+ setSubredditType(safeJsonToString(obj.get("subreddit_type")));
+
+ }
+
+ private void setCreated(double created) {
+ this.created = created;
+ }
+
+ private void setCreatedUTC(double createdUTC) {
+ this.createdUTC = createdUTC;
+ }
+
+ private void setDescription(String description) {
+ this.description = description;
+ }
+
+ private void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ private void setNSFW(Boolean nsfw) {
+ this.nsfw = nsfw;
+ }
+
+ private void setSubscribers(long subscribers) {
+ this.subscribers = subscribers;
+ }
+
+ private void setTitle(String title) {
+ this.title = title;
+ }
+
+ private void setURL(String url) {
+ this.url = url;
+ }
+
+ /**
+ * @return Timestamp of when the subreddit was created.
+ */
+ public double getCreated() {
+ return created;
+ }
+
+ /**
+ * @return UTC timestamp of when the subreddit was created.
+ */
+ public double getCreatedUTC() {
+ return createdUTC;
+ }
+
+ /**
+ * @return Description detailing the subreddit.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * @return The subreddit's display name.
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * @return The number of subscribers for this subreddit.
+ */
+ public long getSubscribers() {
+ return subscribers;
+ }
+
+ /**
+ * @return The subreddit's title.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * @return The subreddit's URL.
+ */
+ public String getURL() {
+ return url;
+ }
+
+ /**
+ * @return True if the subreddit is marked as containing adult content; false if not.
+ */
+ public Boolean isNSFW() {
+ return nsfw;
+ }
+
+ /**
+ * @return The type of subreddit (e.g. "private" or "public")
+ */
+ public String getSubredditType() {
+ return subredditType;
+ }
+
+ public void setSubredditType(String type) {
+ this.subredditType = type;
+ }
+
+ @Override
+ public String toString() {
+ return "Subreddit(" + this.getFullName() + ")<" + this.getDisplayName() + ">";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return (other instanceof Subreddit && this.getFullName().equals(((Subreddit) other).getFullName()));
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode() * this.getFullName().hashCode();
+ }
+
+ @Override
+ public int compareTo(Thing o) {
+ return this.getFullName().compareTo(o.getFullName());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/com/github/jreddit/parser/entity/Thing.java b/src/main/com/github/jreddit/parser/entity/Thing.java
new file mode 100644
index 0000000..84086d5
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/Thing.java
@@ -0,0 +1,77 @@
+package com.github.jreddit.parser.entity;
+
+/**
+ * This class represents a reddit "thing"
+ *
+ * @author Omer Elnour
+ * @author Simon Kassing
+ * @see Reddit API Reference
+ */
+public abstract class Thing implements Comparable {
+
+ /**
+ * The kind of this thing.
+ *
+ * @see Reddit API Reference for full names (section 'kind prefixes')
+ */
+ protected final Kind kind;
+
+ /**
+ * The identifier of this thing.
+ *
+ * @see Reddit API Reference for full names (section 'identifier')
+ */
+ protected final String identifier;
+
+ /**
+ * The full name of this thing.
+ * Combination of its kind ({@link #getKind() getKind}) and its unique ID.
+ *
+ * @see Reddit API Reference for full names
+ */
+ protected final String fullName;
+
+ /**
+ * Constructor. Must be called.
+ * @param name Full name of the thing
+ */
+ public Thing(String name) {
+ assert name.contains("_") : "A full name must contain an underscore.";
+ this.fullName = name;
+ String[] split = name.split("_");
+ this.kind = Kind.match(split[0]);
+ this.identifier = split[1];
+ }
+
+ /**
+ * Retrieve the kind of this thing.
+ * Example: t3 indicates a kind 3 (a link).
+ *
+ * @see Reddit API Reference for full names (section 'kind prefixes')
+ */
+ public Kind getKind() {
+ return kind;
+ }
+
+ /**
+ * Retrieve the identifier of this thing.
+ * Example: 15bfi0.
+ *
+ * @see Reddit API Reference for full names (section 'identifier')
+ */
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ /**
+ * Retrieve the full name of this thing.
+ * Combination of its kind (see {@link #getKind() getKind}) and its unique ID, combined with a underscore.
+ * Example: t3_15bfi0 indicates a kind 3 (a link) and as unique identifier 15bfi0.
+ *
+ * @see Reddit API Reference for full names
+ */
+ public String getFullName() {
+ return fullName;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/com/github/jreddit/parser/entity/UserInfo.java b/src/main/com/github/jreddit/parser/entity/UserInfo.java
new file mode 100644
index 0000000..8ededa7
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/UserInfo.java
@@ -0,0 +1,212 @@
+package com.github.jreddit.parser.entity;
+
+import org.json.simple.JSONObject;
+
+/**
+ * Encapsulates user information (regarding karma, emails, identifiers, statuses, created time and current modhash)
+ *
+ * @author Raul Rene Lepsa
+ */
+public class UserInfo {
+
+ // User identifier
+ private String id;
+
+ // The user's name
+ private String name;
+
+ // Modhash token
+ private String modhash;
+
+ // Karma points for all the comments
+ private long commentKarma;
+
+ // Karma points for all the submitted links
+ private long linkKarma;
+
+ // Whether the user is a moderator
+ private boolean isMod;
+
+ // Whether or not the user has moderator email
+ private Boolean hasModMail;
+
+ // Whether the account is associated with an email address
+ private Boolean hasMail;
+
+ // Indicates whether the user has verified the email address
+ private Boolean hasVerifiedEmail;
+
+ // Whether the user is a gold member
+ private boolean isGold;
+
+ // Timestamp of the creation date
+ private double created;
+
+ // UTC timestamp of creation date
+ private double createdUTC;
+
+ // Indicates whether this user is friends with the currently connected one. Believe it or not, you can actually be
+ // friends with yourself. http://www.reddit.com/r/reddit.com/comments/duf7q/random_reddit_protip_you_can_add_yourself_as_a/
+ private boolean isFriend;
+
+ // Indicates whether the user is over 18
+ private Boolean over18;
+
+ public UserInfo() {
+
+ }
+
+ public UserInfo(JSONObject info) {
+
+ setHasMail((Boolean) info.get("has_mail"));
+ setHasModMail((Boolean) info.get("has_mod_mail"));
+ setCommentKarma((Long) info.get("comment_karma"));
+ setCreatedUTC((Double) info.get("created_utc"));
+ setGold((Boolean) info.get("is_gold"));
+ setLinkKarma((Long) info.get("link_karma"));
+ setMod((Boolean) info.get("is_mod"));
+ setFriend((Boolean) info.get("is_friend"));
+ setModhash((String) info.get("modhash"));
+ setHasVerifiedEmail((Boolean) info.get("has_verified_email"));
+ setId((String) info.get("id"));
+ setOver18((Boolean) info.get("over_18"));
+ setCreated((Double) info.get("created"));
+ setName((String) info.get("name"));
+
+ }
+
+ 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 String getModhash() {
+ return modhash;
+ }
+
+ public void setModhash(String modhash) {
+ this.modhash = modhash;
+ }
+
+ public long getCommentKarma() {
+ return commentKarma;
+ }
+
+ public void setCommentKarma(long commentKarma) {
+ this.commentKarma = commentKarma;
+ }
+
+ public long getLinkKarma() {
+ return linkKarma;
+ }
+
+ public void setLinkKarma(long linkKarma) {
+ this.linkKarma = linkKarma;
+ }
+
+ public boolean isMod() {
+ return isMod;
+ }
+
+ public void setMod(boolean isMod) {
+ this.isMod = isMod;
+ }
+
+ public Boolean getHasModMail() {
+ return hasModMail;
+ }
+
+ public void setHasModMail(Boolean hasModMail) {
+ this.hasModMail = hasModMail;
+ }
+
+ public Boolean getHasMail() {
+ return hasMail;
+ }
+
+ public void setHasMail(Boolean hasMail) {
+ this.hasMail = hasMail;
+ }
+
+ public Boolean isHasVerifiedEmail() {
+ return hasVerifiedEmail;
+ }
+
+ public void setHasVerifiedEmail(Boolean hasVerifiedEmail) {
+ this.hasVerifiedEmail = hasVerifiedEmail;
+ }
+
+ public boolean isGold() {
+ return isGold;
+ }
+
+ public void setGold(boolean isGold) {
+ this.isGold = isGold;
+ }
+
+ public double getCreated() {
+ return created;
+ }
+
+ public void setCreated(double created) {
+ this.created = created;
+ }
+
+ public double getCreatedUTC() {
+ return createdUTC;
+ }
+
+ public void setCreatedUTC(double createdUTC) {
+ this.createdUTC = createdUTC;
+ }
+
+ public boolean isFriend() {
+ return isFriend;
+ }
+
+ public void setFriend(boolean isFriend) {
+ this.isFriend = isFriend;
+ }
+
+ public Boolean getOver18() {
+ return over18;
+ }
+
+ public void setOver18(Boolean over18) {
+ this.over18 = over18;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ String newLine = System.getProperty("line.separator");
+
+ result.append("id: ").append(id).append(newLine)
+ .append("name: ").append(name).append(newLine)
+ .append("modhash: ").append(modhash).append(newLine)
+ .append("commentKarma: ").append(commentKarma).append(newLine)
+ .append("linkKarma: ").append(linkKarma).append(newLine)
+ .append("isModerator: ").append(isMod).append(newLine)
+ .append("hasModMail: ").append(hasModMail).append(newLine)
+ .append("hasMail: ").append(hasMail).append(newLine)
+ .append("hasVerifiedEmail: ").append(hasVerifiedEmail).append(newLine)
+ .append("isGold: ").append(isGold).append(newLine)
+ .append("Created: ").append(created).append(newLine)
+ .append("CreatedUTC: ").append(createdUTC).append(newLine)
+ .append("isFriend: ").append(isFriend).append(newLine)
+ .append("over18: ").append(over18);
+
+ return result.toString();
+ }
+}
diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java b/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java
new file mode 100644
index 0000000..5264564
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/imaginary/CommentTreeElement.java
@@ -0,0 +1,16 @@
+package com.github.jreddit.parser.entity.imaginary;
+
+/**
+ * Interface to abstract over the two possible elements in a
+ * comment tree, namely a {@link Comment} or a {@link More} thing. If an object
+ * is of the type of this interface, it means that it must
+ * be either an {@link Comment} or a {@link More} thing.
+ *
+ * @author Simon Kassing
+ *
+ * @see com.github.jreddit.parser.entity.Comment
+ * @see com.github.jreddit.parser.entity.More
+ */
+public interface CommentTreeElement {
+ // Empty because only an abstraction for either a Comment or a More thing
+}
diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java b/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java
new file mode 100644
index 0000000..5910fff
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/imaginary/FullSubmission.java
@@ -0,0 +1,35 @@
+package com.github.jreddit.parser.entity.imaginary;
+
+import java.util.List;
+
+import com.github.jreddit.parser.entity.Submission;
+
+public class FullSubmission {
+
+ private Submission submission;
+ private List commentTree;
+
+ public FullSubmission(Submission submission, List commentTree) {
+ this.submission = submission;
+ this.commentTree = commentTree;
+ }
+
+ /**
+ *
+ * @return the submission
+ */
+ public Submission getSubmission() {
+ return submission;
+ }
+
+ /**
+ *
+ * @return the commentTree
+ */
+ public List getCommentTree() {
+ return commentTree;
+ }
+
+
+
+}
diff --git a/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java b/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java
new file mode 100644
index 0000000..b0ee6e6
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/entity/imaginary/MixedListingElement.java
@@ -0,0 +1,16 @@
+package com.github.jreddit.parser.entity.imaginary;
+
+/**
+ * Interface to abstract over the two possible elements in a
+ * mixed listing, namely a {@link Comment} or a {@link Submission} thing.
+ * If an object is of the type of this interface, it means that it must
+ * be either of the respective {@link Comment} or {@link Submission} class.
+ *
+ * @author Simon Kassing
+ *
+ * @see com.github.jreddit.parser.entity.Comment
+ * @see com.github.jreddit.parser.entity.Submission
+ */
+public interface MixedListingElement {
+ // Empty because only an abstraction for either a Comment or a Submission thing
+}
diff --git a/src/main/com/github/jreddit/parser/exception/RedditParseException.java b/src/main/com/github/jreddit/parser/exception/RedditParseException.java
new file mode 100644
index 0000000..72caa35
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/exception/RedditParseException.java
@@ -0,0 +1,29 @@
+package com.github.jreddit.parser.exception;
+
+import org.json.simple.parser.ParseException;
+
+public class RedditParseException extends Exception {
+
+ private static final long serialVersionUID = -1031803118041533936L;
+
+ public RedditParseException(String custom) {
+ super("Could not parse response from reddit (" + custom + ")");
+ }
+
+ public RedditParseException(String custom, Throwable t) {
+ super("Could not parse response from reddit (" + custom + ")", t);
+ }
+
+ public RedditParseException() {
+ this("undefined (null) response");
+ }
+
+ public RedditParseException(int errorCode) {
+ this("contained HTTP error code: " + errorCode);
+ }
+
+ public RedditParseException(ParseException pe) {
+ this("invalid JSON format", pe);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java b/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java
new file mode 100644
index 0000000..e212502
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/CommentsListingParser.java
@@ -0,0 +1,55 @@
+package com.github.jreddit.parser.listing;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.exception.RedditParseException;
+
+public class CommentsListingParser extends RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommentsListingParser.class);
+
+ /**
+ * Parse JSON received from reddit into a list of comments.
+ * This parser expects the JSON to be of a listing of comments.
+ *
+ * Note: this parsing can only be performed on listings of comments, not on
+ * a comment tree of a submission.
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of comments
+ *
+ * @throws ParseException
+ * @throws RedditRequestException
+ */
+ public List parse(String jsonText) throws RedditParseException {
+
+ // Parse to a list of things
+ List things = this.parseGeneric(jsonText);
+
+ // List of comment and submission mixed elements
+ List comments = new LinkedList();
+
+ // Iterate over things
+ for (Thing t : things) {
+
+ if (t instanceof Comment) {
+ comments.add((Comment) t);
+ } else {
+ LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it.");
+ }
+
+ }
+
+ // Return resulting comments list
+ return comments;
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java b/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java
new file mode 100644
index 0000000..3cbf987
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/CommentsMoreParser.java
@@ -0,0 +1,84 @@
+package com.github.jreddit.parser.listing;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.More;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.entity.imaginary.CommentTreeElement;
+import com.github.jreddit.parser.exception.RedditParseException;
+
+public class CommentsMoreParser extends RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommentsMoreParser.class);
+
+ private static final JSONParser JSON_PARSER = new JSONParser();
+
+
+ /**
+ * Parse JSON received from reddit into a list of new additional comment tree elements.
+ * This parser expects the JSON to be of a listing of comments and more's.
+ *
+ * Note: this parsing can only be performed on listings of comments and more's, not on
+ * a comment tree of a submission.
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of comments
+ *
+ * @throws ParseException
+ * @throws RedditRequestException
+ */
+ public List parse(String jsonText) throws RedditParseException {
+
+ try {
+
+ // Parse JSON response
+ Object response = JSON_PARSER.parse(jsonText);
+
+ // Validate main
+ this.validate(response);
+
+ // Move to the main object
+ JSONObject main = (JSONObject) ((JSONObject) response).get("json");
+
+ // List of comment and more mixed elements
+ List elements = new LinkedList();
+
+ // If the main has data (it can happen that it does not, when no comments identifiers were passed along)
+ if (main.get("data") != null) {
+
+ // Parse to a list of things
+ List things = this.parseGeneric(main.toJSONString(), "things");
+
+ // Iterate over things
+ for (Thing t : things) {
+
+ if (t instanceof Comment) {
+ elements.add((Comment) t);
+ } else if (t instanceof More) {
+ elements.add((More) t);
+ } else {
+ LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it.");
+ }
+
+ }
+
+ }
+
+ // Return resulting element list
+ return elements;
+
+ } catch (ParseException pe) {
+ throw new RedditParseException(pe);
+ }
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/MixedListingParser.java b/src/main/com/github/jreddit/parser/listing/MixedListingParser.java
new file mode 100644
index 0000000..bb36a27
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/MixedListingParser.java
@@ -0,0 +1,62 @@
+package com.github.jreddit.parser.listing;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.Submission;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.entity.imaginary.MixedListingElement;
+import com.github.jreddit.parser.exception.RedditParseException;
+
+/**
+ * Parser for a listing that has both submissions and comments mixed together.
+ *
+ * @author Simon Kassing
+ *
+ * @see MixedListingElement
+ */
+public class MixedListingParser extends RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MixedListingParser.class);
+
+ /**
+ * Parse JSON received from reddit into a list of submissions and comments.
+ * This parser expects the JSON to be of a listing of submissions and comments.
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of submissions
+ *
+ * @throws ParseException
+ */
+ public List parse(String jsonText) throws RedditParseException {
+
+ // Parse to a list of things
+ List things = this.parseGeneric(jsonText);
+
+ // List of comment and submission mixed elements
+ List mixedElements = new LinkedList();
+
+ // Iterate over things
+ for (Thing t : things) {
+
+ if (t instanceof Comment) {
+ mixedElements.add((Comment) t);
+ } else if (t instanceof Submission) {
+ mixedElements.add((Submission) t);
+ } else {
+ LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it.");
+ }
+
+ }
+
+ // Return result
+ return mixedElements;
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/RedditListingParser.java b/src/main/com/github/jreddit/parser/listing/RedditListingParser.java
new file mode 100644
index 0000000..73f5853
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/RedditListingParser.java
@@ -0,0 +1,193 @@
+package com.github.jreddit.parser.listing;
+
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.Kind;
+import com.github.jreddit.parser.entity.More;
+import com.github.jreddit.parser.entity.Submission;
+import com.github.jreddit.parser.entity.Subreddit;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.exception.RedditParseException;
+import com.github.jreddit.parser.util.JsonUtils;
+
+public class RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RedditListingParser.class);
+
+ protected static final JSONParser JSON_PARSER = new JSONParser();
+
+ /**
+ * Validate that it is indeed the starting of a listing of reddit things.
+ *
+ * @param response Object returned by JSON parser
+ *
+ * @throws RedditRequestException If the response is not valid listing of reddit things
+ */
+ public void validate(Object response) throws RedditParseException {
+
+ // Check for null
+ if (response == null) {
+ throw new RedditParseException();
+ }
+
+ // Check it is a JSON response
+ if (!(response instanceof JSONObject)) {
+ throw new RedditParseException("not a JSON response");
+ }
+
+ // Cast to JSON object
+ JSONObject jsonResponse = ((JSONObject) response);
+
+ // Check for error
+ if (jsonResponse.get("error") != null) {
+ throw new RedditParseException(JsonUtils.safeJsonToInteger(jsonResponse.get("error")));
+ }
+
+ // Check that data exists
+ if (jsonResponse.get("data") == null && jsonResponse.get("json") == null) {
+ throw new RedditParseException("data is missing from listing");
+ }
+
+ }
+
+ /**
+ * Parse JSON received from reddit into a list of things.
+ * This parser expects the JSON to be of a listing of things, and supports
+ * the following things: More, Comment, Submission, and Subreddit.
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of things
+ *
+ * @throws ParseException
+ */
+ public List parseGeneric(String jsonText) throws RedditParseException {
+ return parseGeneric(jsonText, "children");
+ }
+
+ /**
+ * Parse JSON received from reddit into a list of things.
+ * This parser expects the JSON to be of a listing of things, and supports
+ * the following things: More, Comment, Submission, and Subreddit.
+ *
+ * Note: if it encounters an invalid element (e.g. missing kind or data), it will
+ * log a warning using SLF4J and would return null.
+ *
+ * @param jsonText JSON Text
+ * @param listingName Name of the listing name within the data
+ *
+ * @return Parsed list of things
+ *
+ * @throws ParseException
+ */
+ public List parseGeneric(String jsonText, String listingName) throws RedditParseException {
+
+ try {
+
+ // List of submissions
+ List things = new LinkedList();
+
+ // Send request to reddit server via REST client
+ Object response = JSON_PARSER.parse(jsonText);
+
+ // Check for reddit error, can throw a RedditError
+ validate(response);
+
+ // Cast to a JSON object
+ JSONObject object = (JSONObject) response;
+
+ // Get the array of children
+ JSONArray array = (JSONArray) ((JSONObject) object.get("data")).get(listingName);
+
+ // Iterate over array of children
+ for (Object element : array) {
+
+ // Get the element
+ JSONObject data = (JSONObject) element;
+
+ // Make sure it is of the correct kind
+ String kindData = safeJsonToString(data.get("kind"));
+ Object objData = data.get("data");
+
+ // If no kind is given
+ if (kindData == null) {
+ LOGGER.warn("Kind data missing, skipping it.");
+
+ // If no data is given
+ } else if (objData == null || !(objData instanceof JSONObject)) {
+ LOGGER.warn("Object data missing, skipping it.");
+
+ } else {
+
+ // Attempt to match
+ Kind kind = Kind.match(kindData);
+
+ // Parse the thing
+ Thing thing = parseThing(kind, ((JSONObject) data.get("data")));
+
+ // Show warning if failed
+ if (thing == null) {
+ LOGGER.warn("Encountered invalid kind for a listing (" + kindData + "), skipping it.");
+
+ } else {
+ things.add(thing);
+ }
+
+ }
+
+ }
+
+ // Finally return list of submissions
+ return things;
+
+ } catch (ParseException pe) {
+ throw new RedditParseException(pe);
+ }
+
+ }
+
+ /**
+ * Parse the data into a thing if possible.
+ *
+ * @param kind Kind of data
+ * @param data Data for the thing
+ * @return The thing generated from the data, if failed null
+ */
+ private Thing parseThing(Kind kind, JSONObject data) {
+
+ // For a comment
+ if (kind == Kind.COMMENT) {
+ return new Comment(data);
+
+ // For a submission
+ } else if (kind == Kind.LINK) {
+ return new Submission(data);
+
+ // For a subreddit
+ } else if (kind == Kind.SUBREDDIT) {
+ return new Subreddit(data);
+
+ // For a more
+ } else if (kind == Kind.MORE) {
+ return new More(data);
+
+ // In all other cases (null, or of a different type)
+ } else {
+ return null;
+ }
+
+ }
+
+
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java b/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java
new file mode 100644
index 0000000..ff2eaa1
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/SubmissionsListingParser.java
@@ -0,0 +1,51 @@
+package com.github.jreddit.parser.listing;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Submission;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.exception.RedditParseException;
+
+public class SubmissionsListingParser extends RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionsListingParser.class);
+
+ /**
+ * Parse JSON received from reddit into a list of submissions.
+ * This parser expects the JSON to be of a listing of submissions ('links').
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of submissions
+ *
+ * @throws ParseException
+ */
+ public List parse(String jsonText) throws RedditParseException {
+
+ // Parse to a list of things
+ List things = this.parseGeneric(jsonText);
+
+ // List of comment and submission mixed elements
+ List submissions = new LinkedList();
+
+ // Iterate over things
+ for (Thing t : things) {
+
+ if (t instanceof Submission) {
+ submissions.add((Submission) t);
+ } else {
+ LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it.");
+ }
+
+ }
+
+ // Return resulting comments list
+ return submissions;
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java b/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java
new file mode 100644
index 0000000..9637dda
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/listing/SubredditsListingParser.java
@@ -0,0 +1,51 @@
+package com.github.jreddit.parser.listing;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.jreddit.parser.entity.Subreddit;
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.parser.exception.RedditParseException;
+
+public class SubredditsListingParser extends RedditListingParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SubredditsListingParser.class);
+
+ /**
+ * Parse JSON received from reddit into a list of subreddits.
+ * This parser expects the JSON to be of a listing of subreddits.
+ *
+ * @param jsonText JSON Text
+ * @return Parsed list of subreddits
+ *
+ * @throws ParseException
+ */
+ public List parse(String jsonText) throws RedditParseException {
+
+ // Parse to a list of things
+ List things = this.parseGeneric(jsonText);
+
+ // List of comment and submission mixed elements
+ List subreddits = new LinkedList();
+
+ // Iterate over things
+ for (Thing t : things) {
+
+ if (t instanceof Subreddit) {
+ subreddits.add((Subreddit) t);
+ } else {
+ LOGGER.warn("Encountered an unexpected reddit thing (" + t.getKind().value() + "), skipping it.");
+ }
+
+ }
+
+ // Return resulting comments list
+ return subreddits;
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java b/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java
new file mode 100644
index 0000000..b142959
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/single/FullSubmissionParser.java
@@ -0,0 +1,156 @@
+package com.github.jreddit.parser.single;
+
+import static com.github.jreddit.parser.util.JsonUtils.safeJsonToString;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.Kind;
+import com.github.jreddit.parser.entity.More;
+import com.github.jreddit.parser.entity.Submission;
+import com.github.jreddit.parser.entity.imaginary.CommentTreeElement;
+import com.github.jreddit.parser.entity.imaginary.FullSubmission;
+import com.github.jreddit.parser.exception.RedditParseException;
+import com.github.jreddit.parser.util.JsonUtils;
+
+public class FullSubmissionParser {
+
+ protected static final JSONParser JSON_PARSER = new JSONParser();
+
+ /**
+ * Parse JSON received from reddit into a full submission.
+ * A full submissions means it has both (a) the submission, and (b) the comment tree.
+ *
+ * @param jsonText JSON Text
+ * @return Full submission
+ *
+ * @throws ParseException
+ */
+ public FullSubmission parse(String jsonText) throws RedditParseException {
+
+ try {
+
+ // Parse JSON text
+ Object response = JSON_PARSER.parse(jsonText);
+
+ // Validate response
+ validate(response);
+
+ // Create submission (casting with JSON is horrible)
+ JSONObject main = (JSONObject) ((JSONArray) response).get(0);
+ Submission submission = new Submission((JSONObject) ((JSONObject) ((JSONArray)((JSONObject) main.get("data")).get("children")).get(0)).get("data"));
+
+ // Create comment tree
+ JSONObject mainTree = (JSONObject) ((JSONArray) response).get(1);
+ List commentTree = parseRecursive(mainTree);
+
+ // Return the set of submission and its comment tree
+ return new FullSubmission(submission, commentTree);
+
+ } catch (ParseException pe) {
+ throw new RedditParseException(pe);
+ }
+
+ }
+
+ /**
+ * Parse a JSON object consisting of comments and add them
+ * to the already existing list of comments. This does NOT create
+ * a new comment list.
+ *
+ * @param comments List of comments
+ * @param object JSON Object
+ */
+ protected List parseRecursive(JSONObject main) throws RedditParseException {
+
+ List commentTree = new ArrayList();
+
+ // Iterate over the comment tree results
+ JSONArray array = (JSONArray) ((JSONObject) main.get("data")).get("children");
+ for (Object element : array) {
+
+ // Get the element
+ JSONObject data = (JSONObject) element;
+
+ // Make sure it is of the correct kind
+ String kind = safeJsonToString(data.get("kind"));
+
+ // If it is a comment
+ if (kind != null && kind.equals(Kind.COMMENT.value())) {
+
+ // Create comment
+ Comment comment = new Comment( (JSONObject) data.get("data") );
+
+ // Retrieve replies
+ Object replies = ((JSONObject) data.get("data")).get("replies");
+
+ // If it is an JSON object
+ if (replies instanceof JSONObject) {
+ comment.setReplies(parseRecursive( (JSONObject) replies ));
+
+ // If there are no replies, end with an empty one
+ } else {
+ comment.setReplies(new ArrayList());
+ }
+
+ // Add comment to the tree
+ commentTree.add(comment);
+ }
+
+ // If it is a more
+ if (kind != null && kind.equals(Kind.MORE.value())) {
+
+ // Add to comment tree
+ commentTree.add(new More((JSONObject) data.get("data")));
+
+ }
+
+ }
+
+ return commentTree;
+
+ }
+
+ /**
+ * Validate that it is in fact a full submission.
+ *
+ * @param response Object from the JSON parser
+ *
+ * @throws RedditRequestException If the JSON is in incorrect format
+ */
+ public void validate(Object response) throws RedditParseException {
+
+ // Check for null
+ if (response == null) {
+ throw new RedditParseException();
+ }
+
+ // Check it is a JSON response
+ if (response instanceof JSONObject) {
+
+ // Cast to JSON object
+ JSONObject jsonResponse = (JSONObject) response;
+
+ // Check for error
+ if (jsonResponse.get("error") != null) {
+ throw new RedditParseException(JsonUtils.safeJsonToInteger(jsonResponse.get("error")));
+ } else {
+ throw new RedditParseException("invalid json format, started with object (should start with array)");
+ }
+
+ }
+
+ // It must start with an array
+ if (!(response instanceof JSONArray)) {
+ throw new RedditParseException("invalid json format, did not start with array");
+ }
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java b/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java
new file mode 100644
index 0000000..3f43486
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/util/CommentTreeUtils.java
@@ -0,0 +1,96 @@
+package com.github.jreddit.parser.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.imaginary.CommentTreeElement;
+
+public class CommentTreeUtils {
+
+ private CommentTreeUtils() {
+ // Empty to disallow the invocation of the default constructor for this utility class
+ }
+
+ /**
+ * Flatten the comment tree.
+ * The original comment tree is not overwritten.
+ *
+ * @param cs List of comments that you get returned from one of the other methods here
+ *
+ * @return Flattened comment tree.
+ */
+ public static List flattenCommentTree(List commentTree) {
+ List target = new ArrayList();
+ flattenCommentTree(commentTree, target);
+ return target;
+ }
+
+ /**
+ * Flatten the comment tree.
+ * The original comment tree is not overwritten.
+ *
+ * @param cs List of comments that you get returned from one of the other methods here
+ * @param target List in which to place the flattened comment tree.
+ */
+ private static void flattenCommentTree(List commentTree, List target) {
+ for (CommentTreeElement c : commentTree) {
+ target.add(c);
+ if (c instanceof Comment) {
+ flattenCommentTree(((Comment)c).getReplies(), target);
+ }
+ }
+ }
+
+ /**
+ * Get printable version of the given comment tree.
+ *
+ * @param cs List of comment tree elements
+ *
+ * @return Printable comment tree
+ */
+ public static String printCommentTree(List cs) {
+ StringBuilder builder = new StringBuilder();
+ for (CommentTreeElement c : cs) {
+ builder.append(printCommentTree(c, 0));
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Get printable version of a comment at a specific level.
+ *
+ * Note: uses unsafe recursion
+ *
+ * @param c Comment
+ * @param level Level to place at
+ *
+ * @return Printable comment tree
+ */
+ private static String printCommentTree(CommentTreeElement c, int level) {
+
+ // Initialize empty buffer
+ StringBuilder builder = new StringBuilder();
+
+ // Add tabulation
+ for (int i = 0; i < level; i++) {
+ builder.append("\t");
+ }
+
+ // Comment string
+ builder.append(c.toString());
+ builder.append("\n");
+
+ // Iterate over children
+ if (c instanceof Comment) {
+ for (CommentTreeElement child : ((Comment) c).getReplies()) {
+ builder.append(printCommentTree(child, level + 1));
+ }
+ }
+
+ // Return the buffer
+ return builder.toString();
+
+ }
+
+}
diff --git a/src/main/com/github/jreddit/parser/util/JsonUtils.java b/src/main/com/github/jreddit/parser/util/JsonUtils.java
new file mode 100644
index 0000000..b0c776e
--- /dev/null
+++ b/src/main/com/github/jreddit/parser/util/JsonUtils.java
@@ -0,0 +1,98 @@
+package com.github.jreddit.parser.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Safe utilities (not throwing exceptions) for the conversion of JSON
+ * data into basic types such as Integer, Boolean, Long, and Double.
+ */
+public final class JsonUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtils.class);
+
+ private JsonUtils() {
+ // forbid creating JsonUtils instance
+ }
+
+ /**
+ * Safely converts an object into string (used because sometimes JSONObject's get() method returns null).
+ *
+ * @param obj The object to convert.
+ * @return The string.
+ */
+ public static String safeJsonToString(Object obj) {
+ return obj == null ? null : obj.toString();
+ }
+
+ /**
+ * Safely converts an object into an integer
+ *
+ * @param obj The object to convert.
+ * @return an Integer representing the integer value of the Object (null if the object cannot be converted to an Integer)
+ */
+ public static Integer safeJsonToInteger(Object obj) {
+ Integer intValue = null;
+
+ try {
+ String str = safeJsonToString(obj);
+ intValue = str != null ? Integer.parseInt(str) : null;
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Safe JSON conversion to Integer failed", e);
+ }
+
+ return intValue;
+ }
+
+ /**
+ * Safely converts an object into an double
+ *
+ * @param obj The object to convert.
+ * @return a Double representing the double value of the Object (null if the object cannot be converted to Double)
+ */
+ public static Double safeJsonToDouble(Object obj) {
+ Double doubleValue = null;
+
+ try {
+ String str = safeJsonToString(obj);
+ doubleValue = str != null ? Double.parseDouble(str) : null;
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Safe JSON conversion to Double failed", e);
+ }
+
+ return doubleValue;
+ }
+
+
+ /**
+ * Safely converts an object into an boolean
+ *
+ * @param obj The object to convert.
+ * @return a Boolean representing the boolean value of the Object (null only if the object was also null)
+ */
+ public static Boolean safeJsonToBoolean(Object obj) {
+ String str = safeJsonToString(obj);
+ Boolean booleanValue = str != null ? Boolean.parseBoolean(str) : null;
+ return booleanValue;
+ }
+
+ /**
+ * Safely converts an object into an long
+ *
+ * @param obj The object to convert.
+ * @return a Long representing the long value of the Object (null if the object cannot be converted to Long)
+ */
+ public static Long safeJsonToLong(Object obj) {
+ Long longValue = null;
+
+ try {
+ String str = safeJsonToString(obj);
+ longValue = str != null ? Long.parseLong(str) : null;
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Safe JSON conversion to Long failed", e);
+ }
+
+ return longValue;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/RedditGetRequest.java b/src/main/com/github/jreddit/request/RedditGetRequest.java
new file mode 100644
index 0000000..a0852ac
--- /dev/null
+++ b/src/main/com/github/jreddit/request/RedditGetRequest.java
@@ -0,0 +1,48 @@
+package com.github.jreddit.request;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.github.jreddit.request.util.KeyValueFormatter;
+
+public abstract class RedditGetRequest {
+
+ /** Mapping of all request parameters. */
+ private Map parameters;
+
+ /**
+ * Default constructor.
+ */
+ public RedditGetRequest() {
+ parameters = new HashMap();
+ }
+
+ /**
+ * Add a parameter to the request.
+ * If the key of the parameter already exists, the previous value will be overwritten.
+ *
+ * @param key Key of the parameter (e.g. "limit")
+ * @param value Value of the parameter (e.g. "100")
+ */
+ protected void addParameter(String key, String value) {
+ parameters.put(key, value);
+ }
+
+ /**
+ * Generate the query parameters to be added to an URL.
+ *
+ * @return Parameters (e.g. "limit=100&sort=top")
+ */
+ protected String generateParameters() {
+ return KeyValueFormatter.format(parameters, true);
+ }
+
+ /**
+ * Generate the URI of the request.
+ * Be sure to call {@link #generateParameters()} to add the parameters to the end of the URL.
+ *
+ * @return Reddit Uniform Resource Identifier (e.g. "/usr/endpoint?limit=100&sort=top")
+ */
+ public abstract String generateRedditURI();
+
+}
diff --git a/src/main/com/github/jreddit/request/RedditPostRequest.java b/src/main/com/github/jreddit/request/RedditPostRequest.java
new file mode 100644
index 0000000..5f76922
--- /dev/null
+++ b/src/main/com/github/jreddit/request/RedditPostRequest.java
@@ -0,0 +1,86 @@
+package com.github.jreddit.request;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.github.jreddit.request.util.KeyValueFormatter;
+
+public abstract class RedditPostRequest {
+
+ /** Mapping of all request query parameters. */
+ private Map queryParameters;
+
+ /** Mapping of all request body parameters. */
+ private Map bodyParameters;
+
+ /**
+ * Default constructor.
+ */
+ public RedditPostRequest() {
+ queryParameters = new HashMap();
+ bodyParameters = new HashMap();
+ }
+
+ /**
+ * Add a parameter to the query of the request.
+ * If the key of the parameter already exists, the previous value will be overwritten.
+ *
+ * @param key Key of the parameter (e.g. "limit")
+ * @param value Value of the parameter (e.g. "100")
+ */
+ protected void addQueryParameter(String key, String value) {
+ queryParameters.put(key, value);
+ }
+
+ /**
+ * Add a parameter to the body of the request.
+ * If the key of the parameter already exists, the previous value will be overwritten.
+ *
+ * @param key Key of the parameter (e.g. "id")
+ * @param value Value of the parameter (e.g. "dajkjsf8")
+ */
+ protected void addBodyParameter(String key, String value) {
+ bodyParameters.put(key, value);
+ }
+
+ /**
+ * Generate the query parameters to be added to an URI.
+ *
+ * Note: values are encoded.
+ *
+ * @return Query parameters (e.g. "limit=100&sort=top")
+ */
+ protected String generateQueryParameters() {
+ return KeyValueFormatter.format(queryParameters, true);
+ }
+
+ /**
+ * Generate the body parameters to be added.
+ *
+ * Note: values are encoded.
+ *
+ * @return Body parameters (e.g. "limit=100&sort=top")
+ */
+ protected String generateBodyParameters() {
+ return KeyValueFormatter.format(bodyParameters, true);
+ }
+
+ /**
+ * Generate the URI of the request.
+ * Be sure to call {@link #generateQueryParameters()} in your implementation
+ * to add the parameters to the end of the URL.
+ *
+ * @return Reddit Uniform Resource Identifier (e.g. "/usr/endpoint?limit=100&sort=top")
+ */
+ public abstract String generateRedditURI();
+
+ /**
+ * Generate the body for a POST request using the POST parameters.
+
+ * @return Body (e.g. "limit=100&sort=top" for parameters limit: 100 and sort: "top")
+ */
+ public String generateBody() {
+ return generateBodyParameters();
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/MarkActionRequest.java b/src/main/com/github/jreddit/request/action/MarkActionRequest.java
new file mode 100644
index 0000000..9e2e640
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/MarkActionRequest.java
@@ -0,0 +1,16 @@
+package com.github.jreddit.request.action;
+
+import com.github.jreddit.request.RedditPostRequest;
+
+public abstract class MarkActionRequest extends RedditPostRequest {
+
+ /**
+ * Action request.
+ *
+ * @param fullname The fullname of the target (e.g. "t3_djkfsjka")
+ */
+ public MarkActionRequest(String fullname) {
+ this.addBodyParameter("id", fullname);
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java b/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java
new file mode 100644
index 0000000..7e3e050
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/flair/DeleteFlairRequest.java
@@ -0,0 +1,19 @@
+package com.github.jreddit.request.action.flair;
+
+import com.github.jreddit.request.RedditPostRequest;
+
+public class DeleteFlairRequest extends RedditPostRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/deleteflair.json?";
+
+ public DeleteFlairRequest(String username) {
+ this.addBodyParameter("name", username);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/HideRequest.java b/src/main/com/github/jreddit/request/action/mark/HideRequest.java
new file mode 100644
index 0000000..016189d
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/HideRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class HideRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/hide.json?";
+
+ public HideRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java b/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java
new file mode 100644
index 0000000..2f9efeb
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/MarkNsfwRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class MarkNsfwRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/marknsfw.json?";
+
+ public MarkNsfwRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/ReportRequest.java b/src/main/com/github/jreddit/request/action/mark/ReportRequest.java
new file mode 100644
index 0000000..030ca42
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/ReportRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class ReportRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/report.json?";
+
+ public ReportRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/SaveRequest.java b/src/main/com/github/jreddit/request/action/mark/SaveRequest.java
new file mode 100644
index 0000000..c743480
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/SaveRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class SaveRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/save.json?";
+
+ public SaveRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java b/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java
new file mode 100644
index 0000000..782adb1
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/UnhideRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class UnhideRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/unhide.json?";
+
+ public UnhideRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java b/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java
new file mode 100644
index 0000000..16c2f14
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/UnmarkNsfwRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class UnmarkNsfwRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/unmarknsfw.json?";
+
+ public UnmarkNsfwRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java b/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java
new file mode 100644
index 0000000..42d363c
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/UnsaveRequest.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class UnsaveRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/unsave.json?";
+
+ public UnsaveRequest(String fullname) {
+ super(fullname);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/action/mark/VoteRequest.java b/src/main/com/github/jreddit/request/action/mark/VoteRequest.java
new file mode 100644
index 0000000..3306ecf
--- /dev/null
+++ b/src/main/com/github/jreddit/request/action/mark/VoteRequest.java
@@ -0,0 +1,27 @@
+package com.github.jreddit.request.action.mark;
+
+import com.github.jreddit.request.action.MarkActionRequest;
+
+
+public class VoteRequest extends MarkActionRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/vote.json?";
+
+ /**
+ * Vote request constructor.
+ *
+ * @param fullname Fullname of what to vote on
+ * @param direction Direction (must be -1, 0, or 1)
+ */
+ public VoteRequest(String fullname, int direction) {
+ super(fullname);
+ this.addBodyParameter("dir", String.valueOf(direction));
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return ENDPOINT_FORMAT;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/ListingRequest.java b/src/main/com/github/jreddit/request/retrieval/ListingRequest.java
new file mode 100644
index 0000000..c36687f
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/ListingRequest.java
@@ -0,0 +1,62 @@
+package com.github.jreddit.request.retrieval;
+
+import com.github.jreddit.parser.entity.Thing;
+import com.github.jreddit.request.RedditGetRequest;
+
+public abstract class ListingRequest extends RedditGetRequest {
+
+ /**
+ * @param count Starting count for first element of listing
+ * @return This request
+ */
+ public ListingRequest setCount(int count) {
+ this.addParameter("count", String.valueOf(count));
+ return this;
+ }
+
+ /**
+ * @param limit Maximum number of listing elements. This does not mean exactly this parameter will be returned. An upper bound (~100) is imposed by reddit.
+ * @return This request
+ */
+ public ListingRequest setLimit(int limit) {
+ this.addParameter("limit", String.valueOf(limit));
+ return this;
+ }
+
+ /**
+ * @param after The thing in a listing after which the newly requested listing should start.
+ * @return This request
+ */
+ public ListingRequest setAfter(Thing after) {
+ return setAfter(after.getFullName());
+ }
+
+ /**
+ * @param after The fullname of a thing in a listing after which the newly requested listing should start.
+ * @return This request
+ * @see ListingRequest#setAfter(Thing) The usage of setAfter(Thing) is preferred over this method
+ */
+ public ListingRequest setAfter(String after) {
+ this.addParameter("after", after);
+ return this;
+ }
+
+ /**
+ * @param before The thing in a listing before which the newly requested listing should end.
+ * @return This request
+ */
+ public ListingRequest setBefore(Thing before) {
+ return setBefore(before.getFullName());
+ }
+
+ /**
+ * @param before The fullname of a thing in a listing before which the newly requested listing should end.
+ * @return This request
+ * @see ListingRequest#setBefore(Thing) The usage of setBefore(Thing) is preferred over this method
+ */
+ public ListingRequest setBefore(String before) {
+ this.addParameter("before", before);
+ return this;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java
new file mode 100644
index 0000000..9aabc4b
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/comments/CommentsOfUserRequest.java
@@ -0,0 +1,40 @@
+package com.github.jreddit.request.retrieval.comments;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.TimeSpan;
+import com.github.jreddit.request.retrieval.param.UserOverviewSort;
+
+public class CommentsOfUserRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/user/%s/comments.json?%s";
+
+ String username;
+
+ /**
+ * @param username Username of a user (e.g. "JohnM")
+ */
+ public CommentsOfUserRequest(String username) {
+ this.username = username;
+ }
+
+ public CommentsOfUserRequest setSort(UserOverviewSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ public CommentsOfUserRequest setShowGiven() {
+ this.addParameter("show", "given");
+ return this;
+ }
+
+ public CommentsOfUserRequest setTime(TimeSpan time) {
+ this.addParameter("t", time.value());
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, username, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java b/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java
new file mode 100644
index 0000000..bbe9477
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/comments/MoreCommentsRequest.java
@@ -0,0 +1,37 @@
+package com.github.jreddit.request.retrieval.comments;
+
+import java.util.List;
+
+import com.github.jreddit.request.RedditGetRequest;
+import com.github.jreddit.request.retrieval.param.CommentSort;
+import com.github.jreddit.request.util.KeyValueFormatter;
+
+public class MoreCommentsRequest extends RedditGetRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/api/morechildren.json?%s";
+
+ /**
+ * @param submissionFullname Fullname of the submission (e.g. "t3_dajdkjf")
+ * @param commentIdentifiers List of comment ID36 identifiers (e.g. ["jdafid9", "jdafid10"])
+ *
+ * @see {@link com.github.jreddit.parser.entity.More#getChildren()} is typically used to retrieve the 2nd parameter
+ */
+ public MoreCommentsRequest(String submissionFullname, List commentIdentifiers) {
+ // Neglected optional "id" parameter, as it is only relevant for HTML
+ this.addParameter("api_type", "json");
+ this.addParameter("link_id", submissionFullname);
+ this.addParameter("children", KeyValueFormatter.formatCommaSeparatedList(commentIdentifiers));
+ }
+
+ public MoreCommentsRequest setSort(CommentSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java b/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java
new file mode 100644
index 0000000..80da04c
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/mixed/FullSubmissionRequest.java
@@ -0,0 +1,110 @@
+package com.github.jreddit.request.retrieval.mixed;
+
+import com.github.jreddit.parser.entity.Comment;
+import com.github.jreddit.parser.entity.Submission;
+import com.github.jreddit.request.RedditGetRequest;
+import com.github.jreddit.request.retrieval.param.CommentSort;
+
+public class FullSubmissionRequest extends RedditGetRequest {
+
+ private static final String ENDPOINT_FORMAT = "/comments/%s.json?%s";
+
+ private String submissionIdentifier;
+
+ /**
+ * @param submissionIdentifier Submission ID36 identifier (e.g. "dfjIuf")
+ *
+ * @see {@link FullSubmissionRequest(Submission)} is preferred over this constructor
+ */
+ public FullSubmissionRequest(String submissionIdentifier) {
+ this.submissionIdentifier = submissionIdentifier;
+ }
+
+ /**
+ * @param submission Submission ID36 identifier (e.g. "dfjIuf")
+ */
+ public FullSubmissionRequest(Submission submission) {
+ this(submission.getIdentifier());
+ }
+
+ public FullSubmissionRequest setSort(CommentSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ public FullSubmissionRequest setLimit(int limit) {
+ this.addParameter("limit", String.valueOf(limit));
+ return this;
+ }
+
+ /**
+ * Set the comment that will be the (highlighted) focal point of the
+ * returned view and context will be the number of parents shown.
+ *
+ * @param commentIdentifier Comment ID36 identifier
+ *
+ * @return This builder
+ *
+ * @see Comment#getIdentifier()
+ * @see {@link #setContext(int)}
+ */
+ public FullSubmissionRequest setComment(String commentIdentifier) {
+ this.addParameter("comment", commentIdentifier);
+ return this;
+ }
+
+ /**
+ * Set the number of parents shown. This will only affect the result if
+ * {@link #setComment(String)} has been set.
+ *
+ * @param context Maximum number of parents shown (integer between 0 and 8)
+ *
+ * @return This builder
+ */
+ public FullSubmissionRequest setContext(int context) {
+ this.addParameter("context", String.valueOf(context));
+ return this;
+ }
+
+ /**
+ * Set the maximum depth of subtrees in the thread.
+ *
+ * @param depth An integer indicating maximum depth
+ *
+ * @return This builder
+ */
+ public FullSubmissionRequest setDepth(int depth) {
+ this.addParameter("depth", String.valueOf(depth));
+ return this;
+ }
+
+ /**
+ * Set whether or not to show the edits in the comments.
+ *
+ * @param showEdits Should the edits be shown?
+ *
+ * @return This builder
+ */
+ public FullSubmissionRequest setShowEdits(boolean showEdits) {
+ this.addParameter("showedits", String.valueOf(showEdits));
+ return this;
+ }
+
+ /**
+ * Set whether or not the "more" buttons should be shown.
+ *
+ * @param showMore Should the more buttons be shown?
+ *
+ * @return This builder
+ */
+ public FullSubmissionRequest setShowMore(boolean showMore) {
+ this.addParameter("showmore", String.valueOf(showMore));
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, submissionIdentifier, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java
new file mode 100644
index 0000000..f0acb28
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/mixed/MixedOfUserRequest.java
@@ -0,0 +1,63 @@
+package com.github.jreddit.request.retrieval.mixed;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.TimeSpan;
+import com.github.jreddit.request.retrieval.param.UserMixedCategory;
+import com.github.jreddit.request.retrieval.param.UserOverviewSort;
+
+public class MixedOfUserRequest extends ListingRequest {
+
+ /** Endpoint format. */
+ private static final String ENDPOINT_FORMAT = "/user/%s/%s.json?%s";
+
+ private UserMixedCategory category;
+ private String username;
+
+ /**
+ * @param username Username of a user (e.g. "JohnM")
+ * @param category Category of mixed things
+ */
+ public MixedOfUserRequest(String username, UserMixedCategory category) {
+ this.username = username;
+ this.category = category;
+ }
+
+ /**
+ * Set the sorting method.
+ *
+ * Note: This only works for Overview
+ *
+ * @param sort Sorting method
+ *
+ * @return This builder
+ */
+ public MixedOfUserRequest setSort(UserOverviewSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ /**
+ * Set the time span.
+ *
+ * Note: This only works for Overview, and then specifically for the top/controversial sorting method.
+ *
+ * @param time Time span
+ *
+ * @return This builder
+ */
+ public MixedOfUserRequest setTime(TimeSpan time) {
+ this.addParameter("t", time.value());
+ return this;
+ }
+
+ public MixedOfUserRequest setShowGiven() {
+ this.addParameter("show", "given");
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, username, category.value(), this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java b/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java
new file mode 100644
index 0000000..4d1021c
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/CommentSort.java
@@ -0,0 +1,29 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enum to represent comment sorts on Reddit. You see these on a page that lists comments.
+ *
+ * @author Evin Ugur
+ * @author Raul Rene Lepsa
+ * @author Simon Kassing
+ */
+public enum CommentSort {
+
+ CONFIDENCE("confidence"),
+ NEW("new"),
+ TOP("top"),
+ CONTROVERSIAL("controversial"),
+ OLD("old"),
+ QA("qa");
+
+ private final String value;
+
+ CommentSort(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java b/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java
new file mode 100644
index 0000000..7437459
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/QuerySyntax.java
@@ -0,0 +1,22 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enum to represent the possible query syntaxes.
+ * @author Simon Kassing
+ */
+public enum QuerySyntax {
+
+ CLOUDSEARCH ("cloudsearch"),
+ LUCENE ("lucene"),
+ PLAIN ("plain");
+
+ private final String value;
+
+ QuerySyntax(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java b/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java
new file mode 100644
index 0000000..0f7396d
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/SearchSort.java
@@ -0,0 +1,25 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enumeration to represent the different sort methods for submission search.
+ * @author Simon Kassing
+ */
+public enum SearchSort {
+
+ HOT("hot"),
+ RELEVANCE("relevance"),
+ NEW("new"),
+ TOP("top"),
+ COMMENTS("comments");
+
+ private final String value;
+
+ SearchSort(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java b/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java
new file mode 100644
index 0000000..ba37c0a
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/SubmissionSort.java
@@ -0,0 +1,27 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enum to represent submission sorts on Reddit. You see these on a page that lists Submissions.
+ *
+ * @author Evin Ugur
+ * @author Raul Rene Lepsa
+ * @author Simon Kassing
+ */
+public enum SubmissionSort {
+
+ HOT("hot"),
+ NEW("new"),
+ RISING("rising"),
+ CONTROVERSIAL("controversial"),
+ TOP("top");
+
+ private final String value;
+
+ SubmissionSort(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java b/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java
new file mode 100644
index 0000000..e7b97aa
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/SubredditsView.java
@@ -0,0 +1,25 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enumeration to represent the different subreddit categories.
+ * @author Simon Kassing
+ */
+public enum SubredditsView {
+
+ NEW("new"),
+ POPULAR("popular"),
+ MINE_SUBSCRIBER("mine/subscriber"),
+ MINE_CONTRIBUTOR("mine/contributor"),
+ MINE_MODERATOR("mine/moderator");
+
+ private final String value;
+
+ SubredditsView(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java b/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java
new file mode 100644
index 0000000..1d9315d
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/TimeSpan.java
@@ -0,0 +1,26 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enumeration to represent the different submission search times.
+ * @author Simon Kassing
+ */
+public enum TimeSpan {
+
+ HOUR("hour"),
+ DAY("day"),
+ WEEK("week"),
+ MONTH("month"),
+ YEAR("year"),
+ ALL("all");
+
+ private final String value;
+
+ TimeSpan(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java b/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java
new file mode 100644
index 0000000..0d1d520
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/UserMixedCategory.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.retrieval.param;
+
+public enum UserMixedCategory {
+
+ OVERVIEW("overview"),
+ GILDED_RECEIVED("gilded"),
+ GILDED_GIVEN("gilded/given"),
+ SAVED("saved");
+
+ private final String value;
+
+ UserMixedCategory(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java b/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java
new file mode 100644
index 0000000..2f0ba28
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/UserOverviewSort.java
@@ -0,0 +1,24 @@
+package com.github.jreddit.request.retrieval.param;
+
+/**
+ * Enumeration to represent the different sort methods for the user overview.
+ * @author Simon Kassing
+ */
+public enum UserOverviewSort {
+
+ NEW("new"),
+ HOT("hot"),
+ TOP("top"),
+ COMMENTS("controversial");
+
+ private final String value;
+
+ UserOverviewSort(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java b/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java
new file mode 100644
index 0000000..dc09074
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/param/UserSubmissionsCategory.java
@@ -0,0 +1,20 @@
+package com.github.jreddit.request.retrieval.param;
+
+public enum UserSubmissionsCategory {
+
+ SUBMITTED("submitted"),
+ UPVOTED("upvoted"),
+ DOWNVOTED("downvoted"),
+ HIDDEN("hidden");
+
+ private final String value;
+
+ UserSubmissionsCategory(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java
new file mode 100644
index 0000000..f0f0a53
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfSubredditRequest.java
@@ -0,0 +1,32 @@
+package com.github.jreddit.request.retrieval.submissions;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.SubmissionSort;
+
+public class SubmissionsOfSubredditRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/r/%s/%s.json?%s";
+
+ private SubmissionSort sort;
+ private String subreddit;
+
+ /**
+ * @param subreddit Subreddit (e.g. "funny")
+ * @param sort Sorting method
+ */
+ public SubmissionsOfSubredditRequest(String subreddit, SubmissionSort sort) {
+ this.subreddit = subreddit;
+ this.sort = sort;
+ }
+
+ public SubmissionsOfSubredditRequest setShowAll() {
+ this.addParameter("show", "all");
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, subreddit, sort.value(), this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java
new file mode 100644
index 0000000..5dd07dc
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsOfUserRequest.java
@@ -0,0 +1,44 @@
+package com.github.jreddit.request.retrieval.submissions;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.TimeSpan;
+import com.github.jreddit.request.retrieval.param.UserOverviewSort;
+import com.github.jreddit.request.retrieval.param.UserSubmissionsCategory;
+
+public class SubmissionsOfUserRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/user/%s/%s.json?%s";
+
+ private UserSubmissionsCategory category;
+ private String username;
+
+ /**
+ * @param username Username of a user (e.g. "JohnM")
+ * @param category Category of user submissions
+ */
+ public SubmissionsOfUserRequest(String username, UserSubmissionsCategory category) {
+ this.username = username;
+ this.category = category;
+ }
+
+ public SubmissionsOfUserRequest setSort(UserOverviewSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ public SubmissionsOfUserRequest setTime(TimeSpan time) {
+ this.addParameter("t", time.value());
+ return this;
+ }
+
+ public SubmissionsOfUserRequest setShowGiven() {
+ this.addParameter("show", "given");
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, username, category.value(), this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java
new file mode 100644
index 0000000..1d170a5
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/submissions/SubmissionsSearchRequest.java
@@ -0,0 +1,44 @@
+package com.github.jreddit.request.retrieval.submissions;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.QuerySyntax;
+import com.github.jreddit.request.retrieval.param.SearchSort;
+import com.github.jreddit.request.retrieval.param.TimeSpan;
+
+public class SubmissionsSearchRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/search.json?%s";
+
+ /**
+ * @param query Mandatory search query (e.g. "programming Java"), its syntax depends on what is set using {@link #setSyntax(QuerySyntax)}.
+ */
+ public SubmissionsSearchRequest(String query) {
+ this.addParameter("q", query);
+ }
+
+ public SubmissionsSearchRequest setSyntax(QuerySyntax syntax) {
+ this.addParameter("syntax", syntax.value());
+ return this;
+ }
+
+ public SubmissionsSearchRequest setSort(SearchSort sort) {
+ this.addParameter("sort", sort.value());
+ return this;
+ }
+
+ public SubmissionsSearchRequest setTimeSpan(TimeSpan time) {
+ this.addParameter("t", time.value());
+ return this;
+ }
+
+ public SubmissionsSearchRequest setShowAll() {
+ this.addParameter("show", "all");
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java
new file mode 100644
index 0000000..f92b149
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsOfUserRequest.java
@@ -0,0 +1,29 @@
+package com.github.jreddit.request.retrieval.subreddits;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+import com.github.jreddit.request.retrieval.param.SubredditsView;
+
+public class SubredditsOfUserRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/subreddits/%s.json?%s"; // ApiEndpointUtils.SUBREDDITS_GET
+
+ private SubredditsView view;
+
+ /**
+ * @param view View of the subreddits
+ */
+ public SubredditsOfUserRequest(SubredditsView view) {
+ this.view = view;
+ }
+
+ public SubredditsOfUserRequest setShowAll() {
+ this.addParameter("show", "all");
+ return this;
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, view, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java
new file mode 100644
index 0000000..8b3c597
--- /dev/null
+++ b/src/main/com/github/jreddit/request/retrieval/subreddits/SubredditsSearchRequest.java
@@ -0,0 +1,21 @@
+package com.github.jreddit.request.retrieval.subreddits;
+
+import com.github.jreddit.request.retrieval.ListingRequest;
+
+public class SubredditsSearchRequest extends ListingRequest {
+
+ private static final String ENDPOINT_FORMAT = "/subreddits/search.json?%s";
+
+ /**
+ * @param query Search query (e.g. "programming")
+ */
+ public SubredditsSearchRequest(String query) {
+ this.addParameter("q", query);
+ }
+
+ @Override
+ public String generateRedditURI() {
+ return String.format(ENDPOINT_FORMAT, this.generateParameters());
+ }
+
+}
diff --git a/src/main/com/github/jreddit/request/util/KeyValueFormatter.java b/src/main/com/github/jreddit/request/util/KeyValueFormatter.java
new file mode 100644
index 0000000..f6ee955
--- /dev/null
+++ b/src/main/com/github/jreddit/request/util/KeyValueFormatter.java
@@ -0,0 +1,86 @@
+package com.github.jreddit.request.util;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KeyValueFormatter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(KeyValueFormatter.class);
+
+ private KeyValueFormatter() {
+ // Left empty to prevent instantiation of this utility class
+ }
+
+ /**
+ * Format a mapping of key-value pairs to a string, separating key from value using an
+ * equals (=) sign, and separating pairs using an ampersand (&) sign.
+ *
+ * @param keyValueParameters Mapping of key-value pairs
+ * @param encodeUTF8 Whether or not the values should be encoded in UTF-8
+ *
+ * @return Formatted string of key-value pairs (e.g. "a=1&b=something")
+ */
+ public static String format(Map keyValueParameters, boolean encodeUTF8) {
+
+ // Key set
+ Set keys = keyValueParameters.keySet();
+
+ // Iterate over keys
+ String paramsString = "";
+ boolean start = true;
+ for (String key : keys) {
+
+ // Add separation ampersand
+ if (!start) {
+ paramsString = paramsString.concat("&");
+ } else {
+ start = false;
+ }
+
+ // Retrieve value
+ String value = keyValueParameters.get(key);
+
+ // Encode key
+ if (encodeUTF8) {
+ try {
+ value = URLEncoder.encode(value, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.warn("Unsupported Encoding Exception thrown when encoding value", e);
+ }
+ }
+
+ // Add key-value pair
+ paramsString = paramsString.concat(key + "=" + value);
+
+ }
+
+ // Return final parameter string
+ return paramsString;
+
+ }
+
+ /**
+ * Format a comma separated list of the given list.
+ *
+ * @param list List of strings
+ *
+ * @return Comma-separated list string (e.g. "a,b,c,d")
+ */
+ public static String formatCommaSeparatedList(List list) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < list.size(); i++) {
+ if (i != 0) {
+ builder.append(",");
+ }
+ builder.append(list.get(i));
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/sh/adb/RandomRedditMemesAPI/Main.java b/src/main/sh/adb/RandomRedditMemesAPI/Main.java
index f73a2ae..05e2079 100644
--- a/src/main/sh/adb/RandomRedditMemesAPI/Main.java
+++ b/src/main/sh/adb/RandomRedditMemesAPI/Main.java
@@ -10,7 +10,6 @@ import java.io.OutputStream;
import java.net.InetSocketAddress;
public class Main {
-
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("", new ErrorHandler());
@@ -19,7 +18,6 @@ public class Main {
server.setExecutor(null);
server.start();
}
-
}
class CommunityHandler implements HttpHandler {
@Override
diff --git a/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java b/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java
index 1340afb..3740705 100644
--- a/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java
+++ b/src/main/sh/adb/RandomRedditMemesAPI/RedditAPI.java
@@ -1,5 +1,12 @@
package sh.adb.RandomRedditMemesAPI;
-public class RedditAPI {
+import org.json.simple.parser.ParseException;
+
+import com.github.jreddit.oauth.RedditOAuthAgent;
+import com.github.jreddit.oauth.RedditToken;
+import com.github.jreddit.oauth.app.RedditApp;
+import com.github.jreddit.oauth.app.RedditInstalledApp;
+import com.github.jreddit.oauth.exception.RedditOAuthException;
-}
\ No newline at end of file
+public class RedditAPI {
+}