add com.github.jreddit:1.0.4 package

master
adb 3 years ago
parent 3f65168b94
commit 4b47d907cd

@ -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 {

@ -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.<br>
* <br>
* Communicates with reddit to retrieve tokens, and converts them
* into {@link RedditToken}s, which are used internally by jReddit. This class
* supports both the <i>code grant flow</i> and <i>implicit grant flow</i>.
*
* @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.<br>
* <br>
* 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 <i>code flow</i> Uniform Resource Locator (URI) for a
* reddit user to authorize your application.<br>
* <br>
* The user will, after authorization, receive a <i>code</i>. This can be turned into
* a <i>RedditToken</i> using {@link #token(String)}.
*
* @param scopeBuilder Authorization scope builder (must not be <i>null</i>)
* @param duration Duration that the token can last
*
* @return The URI users need to visit and retrieve the <i>code</i> from
*
* @see {@link #token(String)} for converting the <i>code</i> into a usable <i>RedditToken</i>
*/
public synchronized String generateCodeFlowURI(RedditScopeBuilder scopeBuilder, RedditDuration duration) {
// Set parameters
Map<String, String> params = new HashMap<String, String>();
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 <i>implicit flow</i> Uniform Resource Locator (URI) for a
* reddit user to authorize your application.<br>
* <br>
* The user will, after authorization, receive token information. This can be turned into
* a <i>RedditToken</i> using {@link #tokenFromInfo(String, String, long, String)}.
*
* @param scopeBuilder Authorization scope builder (must not be <i>null</i>)
*
* @return The URI users need to visit and retrieve the <i>token information</i> from
*
* @see {@link #tokenFromInfo(String, String, long, String)} for converting the
* <i>token information</i> into <i>RedditToken</i>
*/
public synchronized String generateImplicitFlowURI(RedditScopeBuilder scopeBuilder) {
// Set parameters
Map<String, String> params = new HashMap<String, String>();
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 (<i>code</i> flow).<br>
* <br>
* Retrieve a token for a specific user, meaning that the token is <u>coupled to a user</u>.
* 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.<br>
* <br>
* This is <u>only</u> possible for tokens retrieved through the <u>code flow</u>
* 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 (<i>app-only</i> flow).<br>
* <br>
* Retrieve a token for the <u>application-only</u>, meaning that the
* token is <u>not coupled to any user</u>. The token is typically only
* <u>valid for a short period of time</u> (at the moment of writing: 1 hour).
* After it has expired, the token will no longer work. You must request a <u>new</u>
* token in that case. Refreshing an application-only token is not possible.
*
* @param confidential <i>True</i>: confidential clients (web apps / scripts) not acting on
* behalf of one or more logged out users. <i>False</i>: 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 <i>implicit grant flow</i>.<br>
* <br>
* <b>WARNING:</b> 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 <i>RedditToken</i> 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 <i>RedditToken</i>.<br>
* <br>
* 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.<br>
* <br>
* <i>Note: Per RFC 7009, this request will return a success (204) response
* even if the passed in token was never valid.</i>
*
* @param token <i>RedditToken</i> 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;
}
}

@ -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.<br>
* <br>
* This class wraps the information received from reddit following
* the request for a token.<br>
* A token has three dimensions:
* <ul>
* <li><b>scope:</b> what can it be used for?
* <li><b>expiration:</b> how long can it be used?
* <li><b>refreshable:</b> can its duration be prolonged?
* </ul>
*
* @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 <i>System.currentTimeMillis()</i>.
*
* @return Current time in seconds.
*/
private static long currentTimeSeconds() {
return System.currentTimeMillis() / (long) 1000;
}
}

@ -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.<br>
* <br>
* <i>All</i> information given in this constructor <i>must</i>
* 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;
}
}

@ -0,0 +1,19 @@
package com.github.jreddit.oauth.app;
public class RedditInstalledApp extends RedditApp {
/**
* Reddit Installed Application.<br>
* <br>
* <i>All</i> information given in this constructor <i>must</i>
* 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
}
}

@ -0,0 +1,19 @@
package com.github.jreddit.oauth.app;
public class RedditScriptApp extends RedditApp {
/**
* Reddit Script Application.<br>
* <br>
* <i>All</i> information given in this constructor <i>must</i>
* 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);
}
}

@ -0,0 +1,19 @@
package com.github.jreddit.oauth.app;
public class RedditWebApp extends RedditApp {
/**
* Reddit Web Application.<br>
* <br>
* <i>All</i> information given in this constructor <i>must</i>
* 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);
}
}

@ -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.<br>
* <br>
* 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.<br>
* <br>
* <i>Exception handling: if any function raises an exception,
* it will be logged using SLF4J. The result would be null.</i>
*
* @param rToken Reddit token
* @param request Reddit POST request
*
* @return Response from reddit (raw), if failed <i>null</i>
*/
public abstract String post(RedditToken rToken, RedditPostRequest request);
/**
* Perform a GET reddit request authenticated with the given reddit token.<br>
* <br>
* 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.<br>
* <br>
* <i>Exception handling: if any function raises an exception,
* it will be logged using SLF4J. The result would be null.</i>
*
* @param rToken Reddit token
* @param request Reddit GET request
*
* @return Response from reddit (raw), if failed <i>null</i>
*/
public abstract String get(RedditToken rToken, RedditGetRequest request);
}

@ -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 <i>RedditClient</i>.
*
* @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, <i>null</i> 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);
}
}

@ -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 <i>polite</i>.
* 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);
}
}
}

@ -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);
}
}

@ -0,0 +1,27 @@
package com.github.jreddit.oauth.param;
/**
* Enumerator for the duration of tokens.<br>
* <br>
* There are two possible values:
* <ul>
* <li><b>permanent:</b> The token can be refreshed as many times as the application desires.</li>
* <li><b>temporary:</b> The token cannot be refreshed, and will last for <i>one hour</i>.</li>
* </ul>
*/
public enum RedditDuration {
PERMANENT("permanent"),
TEMPORARY("temporary");
private final String value;
RedditDuration(String value) {
this.value = value;
}
public String value() {
return this.value;
}
}

@ -0,0 +1,48 @@
package com.github.jreddit.oauth.param;
/**
* Enumerator for the possible authorization scopes for reddit requests.<br>
* <br>
* 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;
}
}

@ -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<RedditScope> scopes;
public RedditScopeBuilder() {
scopes = new HashSet<RedditScope>();
}
/**
* Build a string for the request. Called upon by
* <i>RedditToken</i> 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;
}
}

@ -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 <i>RedditToken</i> 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<String> scopes;
/**
* @param scopes List of scopes (e.g. "flair,edit")
*/
public RedditTokenCompleteScope(String scopes) {
// Create set
this.scopes = new HashSet<String>();
// 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());
}
}

@ -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<CommentTreeElement> 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 <i>always</i> initialized, and empty if there are no replies)
*/
public List<CommentTreeElement> getReplies() {
return this.replies;
}
/**
* Set the replies of this comment.
*
* @param replies Comment tree of replies
*/
public void setReplies(List<CommentTreeElement> 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());
}
}

@ -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;
}
}

@ -0,0 +1,167 @@
package com.github.jreddit.parser.entity;
import org.json.simple.JSONObject;
/**
* Encapsulates the private messages.
* Corresponds to the <code>Kind.MESSAGES</code>, 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;
}
}

@ -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;
}
}

@ -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 <i>CommenTreeElement</i> interface.
*
* @author Simon Kassing
*/
public class More extends Thing implements CommentTreeElement {
/** List of comment identifiers (ID36) that are his children. */
private List<String> 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<String>();
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<String> 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>";
}
}

@ -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());
}
}

@ -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 = "&lt;div&gt;HTML description for subreddit&lt;/d&gt;";
// 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());
}
}

@ -0,0 +1,77 @@
package com.github.jreddit.parser.entity;
/**
* This class represents a reddit "thing"
*
* @author <a href="http://www.omrlnr.com">Omer Elnour</a>
* @author Simon Kassing
* @see <a href="http://www.reddit.com/dev/api#fullname">Reddit API Reference</a>
*/
public abstract class Thing implements Comparable<Thing> {
/**
* The kind of this thing.
*
* @see <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names (section 'kind prefixes')</a>
*/
protected final Kind kind;
/**
* The identifier of this thing.
*
* @see <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names (section 'identifier')</a>
*/
protected final String identifier;
/**
* The full name of this thing.
* Combination of its kind ({@link #getKind() getKind}) and its unique ID.
*
* @see <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names</a>
*/
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 <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names (section 'kind prefixes')</a>
*/
public Kind getKind() {
return kind;
}
/**
* Retrieve the identifier of this thing.
* Example: 15bfi0.
*
* @see <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names (section 'identifier')</a>
*/
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 <a href="http://www.reddit.com/dev/api#fullnames">Reddit API Reference for full names</a>
*/
public String getFullName() {
return fullName;
}
}

@ -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();
}
}

@ -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 <i>must</i>
* 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
}

@ -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<CommentTreeElement> commentTree;
public FullSubmission(Submission submission, List<CommentTreeElement> commentTree) {
this.submission = submission;
this.commentTree = commentTree;
}
/**
*
* @return the submission
*/
public Submission getSubmission() {
return submission;
}
/**
*
* @return the commentTree
*/
public List<CommentTreeElement> getCommentTree() {
return commentTree;
}
}

@ -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 <i>must</i>
* 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
}

@ -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);
}
}

@ -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.<br>
* <br>
* <i>Note: this parsing can only be performed on listings of comments, not on
* a comment tree of a submission.</i>
*
* @param jsonText JSON Text
* @return Parsed list of comments
*
* @throws ParseException
* @throws RedditRequestException
*/
public List<Comment> parse(String jsonText) throws RedditParseException {
// Parse to a list of things
List<Thing> things = this.parseGeneric(jsonText);
// List of comment and submission mixed elements
List<Comment> comments = new LinkedList<Comment>();
// 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;
}
}

@ -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.<br>
* <br>
* <i>Note: this parsing can only be performed on listings of comments and more's, not on
* a comment tree of a submission.</i>
*
* @param jsonText JSON Text
* @return Parsed list of comments
*
* @throws ParseException
* @throws RedditRequestException
*/
public List<CommentTreeElement> 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<CommentTreeElement> elements = new LinkedList<CommentTreeElement>();
// 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<Thing> 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);
}
}
}

@ -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<MixedListingElement> parse(String jsonText) throws RedditParseException {
// Parse to a list of things
List<Thing> things = this.parseGeneric(jsonText);
// List of comment and submission mixed elements
List<MixedListingElement> mixedElements = new LinkedList<MixedListingElement>();
// 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;
}
}

@ -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: <i>More</i>, <i>Comment</i>, <i>Submission</i>, and <i>Subreddit</i>.
*
* @param jsonText JSON Text
* @return Parsed list of things
*
* @throws ParseException
*/
public List<Thing> 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: <i>More</i>, <i>Comment</i>, <i>Submission</i>, and <i>Subreddit</i>.<br>
* <br>
* <i>Note: if it encounters an invalid element (e.g. missing kind or data), it will
* log a warning using SLF4J and would return null.</i>
*
* @param jsonText JSON Text
* @param listingName Name of the listing name within the data
*
* @return Parsed list of things
*
* @throws ParseException
*/
public List<Thing> parseGeneric(String jsonText, String listingName) throws RedditParseException {
try {
// List of submissions
List<Thing> things = new LinkedList<Thing>();
// 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 <i>null</i>
*/
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;
}
}
}

@ -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<Submission> parse(String jsonText) throws RedditParseException {
// Parse to a list of things
List<Thing> things = this.parseGeneric(jsonText);
// List of comment and submission mixed elements
List<Submission> submissions = new LinkedList<Submission>();
// 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;
}
}

@ -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<Subreddit> parse(String jsonText) throws RedditParseException {
// Parse to a list of things
List<Thing> things = this.parseGeneric(jsonText);
// List of comment and submission mixed elements
List<Subreddit> subreddits = new LinkedList<Subreddit>();
// 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;
}
}

@ -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<CommentTreeElement> 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<CommentTreeElement> parseRecursive(JSONObject main) throws RedditParseException {
List<CommentTreeElement> commentTree = new ArrayList<CommentTreeElement>();
// 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<CommentTreeElement>());
}
// 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");
}
}
}

@ -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<CommentTreeElement> flattenCommentTree(List<CommentTreeElement> commentTree) {
List<CommentTreeElement> target = new ArrayList<CommentTreeElement>();
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<CommentTreeElement> commentTree, List<CommentTreeElement> 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<CommentTreeElement> 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.<br>
* <br>
* <i>Note: uses unsafe recursion</i>
*
* @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();
}
}

@ -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;
}
}

@ -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<String, String> parameters;
/**
* Default constructor.
*/
public RedditGetRequest() {
parameters = new HashMap<String, String>();
}
/**
* 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();
}

@ -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<String, String> queryParameters;
/** Mapping of all request body parameters. */
private Map<String, String> bodyParameters;
/**
* Default constructor.
*/
public RedditPostRequest() {
queryParameters = new HashMap<String, String>();
bodyParameters = new HashMap<String, String>();
}
/**
* 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.<br>
* <br>
* <i>Note: values are encoded.</i>
*
* @return Query parameters (e.g. "limit=100&sort=top")
*/
protected String generateQueryParameters() {
return KeyValueFormatter.format(queryParameters, true);
}
/**
* Generate the body parameters to be added.<br>
* <br>
* <i>Note: values are encoded.</i>
*
* @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();
}
}

@ -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);
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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());
}
}

@ -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<String> 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());
}
}

@ -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 <i>context</i> 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());
}
}

@ -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.<br>
* <br>
* <i>Note: This only works for Overview</i>
*
* @param sort Sorting method
*
* @return This builder
*/
public MixedOfUserRequest setSort(UserOverviewSort sort) {
this.addParameter("sort", sort.value());
return this;
}
/**
* Set the time span.<br>
* <br>
* <i>Note: This only works for Overview, and then specifically for the top/controversial sorting method.</i>
*
* @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());
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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());
}
}

@ -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());
}
}

@ -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());
}
}

@ -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());
}
}

@ -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());
}
}

@ -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<String, String> keyValueParameters, boolean encodeUTF8) {
// Key set
Set<String> 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<String> 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();
}
}

@ -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

@ -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;
}
public class RedditAPI {
}

Loading…
Cancel
Save