package edu.lmu.cs.headmaster.ws.resource;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import edu.lmu.cs.headmaster.ws.dao.UserDao;
import edu.lmu.cs.headmaster.ws.types.Role;
import edu.lmu.cs.headmaster.ws.util.ServiceException;
/**
* A base class for the resources, supplying error keys, a logger, a validation method, and fields
* for JAX-RS context objects.
*/
public class AbstractResource {
// Error keys. No resource returns user-displayable text, since that is the responsibility
// of a client. Instead, the services provide mnemonic error keys. We've purposely
// refrained from using a properties file (they're not key-value pairs), a separate enum
// (we'd need long references or static imports), or injection (they're not variables,
// but constants).
public static final String SKIP_TOO_SMALL = "skip.too.small";
public static final String MAX_TOO_LARGE = "max.too.large";
public static final String QUERY_REQUIRED = "query.required";
public static final String QUERY_INCOMPLETE = "query.parameters.missing";
public static final String ARGUMENT_CONFLICT = "argument.conflict";
public static final String MALFORMED_ARGUMENT_DATE = "argument.date.malformed";
public static final String MISSING_ARGUMENT_DATE = "argument.date.missing";
public static final String UNSUPPORTED_ENCODING = "encoding.not.supported";
public static final String INVALID_USER = "user.invalid";
public static final String USER_FORBIDDEN = "user.forbidden";
protected Logger logger = Logger.getLogger(getClass());
// URI information automatically injected by JAX-RS framework on each resource call.
@Context
protected UriInfo uriInfo;
// Security context automatically injected by JAX-RS framework on each resource call.
@Context
protected SecurityContext securityContext;
// A dao for security checking.
protected UserDao userDao;
// Every resource needs a user dao.
protected AbstractResource(UserDao userDao) {
this.userDao = userDao;
}
/**
* Checks that a condition is true and throws a <code>ServiceException</code> with the given
* integer HTTP response code if it is not. Example:
* <pre>
* validate(student != null, 404, NO_STUDENT);
* </pre>
*/
protected void validate(boolean condition, int httpStatus, String errorKey) {
if (!condition) {
throw new ServiceException(httpStatus, errorKey);
}
}
/**
* Convenience method that takes a <code>Response.Status</code> instead of an int.
*/
protected void validate(boolean condition, Response.Status httpStatus, String errorKey) {
validate(condition, httpStatus.getStatusCode(), errorKey);
}
/**
* Utility method for checking a paginated request's parameters for validity.
*/
protected void validatePagination(int skip, int max, int minimumSkip, int maximumSkip) {
validate(skip >= minimumSkip, Response.Status.BAD_REQUEST, SKIP_TOO_SMALL);
validate(max <= maximumSkip, Response.Status.BAD_REQUEST, MAX_TOO_LARGE);
}
/**
* Utility method for checking an input interval's validity.
*/
protected Interval validateInterval(String startDate, String endDate) {
validate(startDate != null, Response.Status.BAD_REQUEST, MISSING_ARGUMENT_DATE);
validate(endDate != null, Response.Status.BAD_REQUEST, MISSING_ARGUMENT_DATE);
try {
return new Interval(new DateTime(URLDecoder.decode(startDate, "UTF-8")),
new DateTime(URLDecoder.decode(endDate, "UTF-8")));
} catch(IllegalArgumentException iae) {
throw new ServiceException(Response.Status.BAD_REQUEST, MALFORMED_ARGUMENT_DATE);
} catch(UnsupportedEncodingException uee) {
throw new ServiceException(Response.Status.INTERNAL_SERVER_ERROR, UNSUPPORTED_ENCODING);
}
}
/**
* Logs the currently accessed uri. While this data can also be found in the web server's
* logs, it can be useful in providing some context for debugging when reading the regular
* application logs.
*/
protected void logServiceCall() {
// Sometimes there is no uriInfo, such as when the resource call is
// invoked directly as a Java method.
if (logger.isDebugEnabled() && (uriInfo != null)) {
logger.debug("Invoking " + uriInfo.getAbsolutePath());
}
}
/**
* Preprocesses a query for a URI by trimming, urldecoding, and validating that the skip and
* max parameters make sense.
*
* @return the processed query.
* @throws a ServiceException resulting in a HTTP 400 if the query parameter is missing or a
* ServiceException resulting in an HTTP 500 if the JVM for any reason doesn't understand
* the encoding scheme used to decoding the URL.
*/
protected String preprocessQuery(String q, int skip, int max, int minimumSkip, int maximumSkip) {
try {
// We trim before checking validity.
String query = StringUtils.trimToNull(q != null ? URLDecoder.decode(q, "UTF-8") : null);
validate(query != null, Response.Status.BAD_REQUEST, QUERY_REQUIRED);
validatePagination(skip, max, minimumSkip, maximumSkip);
return query;
} catch (UnsupportedEncodingException e) {
throw new ServiceException(Response.Status.INTERNAL_SERVER_ERROR, UNSUPPORTED_ENCODING);
}
}
/**
* Preprocesses a query for a URI by trimming, urldecoding, and validating that the skip and
* max parameters make sense. Allows for a null query value.
*
* @return the processed query.
* @throws a ServiceException resulting in an HTTP 500 if the JVM for any reason doesn't understand
* the encoding scheme used to decoding the URL.
*/
protected String preprocessNullableQuery(String q, int skip, int max, int minimumSkip, int maximumSkip) {
try {
// We trim before checking validity.
String query = StringUtils.trimToNull(q != null ? URLDecoder.decode(q, "UTF-8") : null);
validatePagination(skip, max, minimumSkip, maximumSkip);
return query;
} catch (UnsupportedEncodingException e) {
throw new ServiceException(Response.Status.INTERNAL_SERVER_ERROR, UNSUPPORTED_ENCODING);
}
}
/**
* Returns a datetime object for a string in a null-safe way.
*
* @return null if the input is null, or else the datetime object for the string produced
* by Joda Time.
* @throws ServiceException to generate an HTTP 400 with the message for a malformed date
* argument if Joda Time cannot convert the input string.
*/
protected DateTime toDateTime(String dateString) {
try {
return dateString == null ? null : new DateTime(dateString);
} catch (IllegalArgumentException iae) {
throw new ServiceException(Response.Status.BAD_REQUEST, MALFORMED_ARGUMENT_DATE);
}
}
/**
* Checks whether the current user can see privileged information.
*/
protected void validatePrivilegedUserCredentials() {
logger.debug("Checking for privileged user credentials.");
validate(securityContext.isUserInRole(Role.HEADMASTER.name()) ||
securityContext.isUserInRole(Role.FACULTY.name()) ||
securityContext.isUserInRole(Role.STAFF.name()), Response.Status.FORBIDDEN,
USER_FORBIDDEN);
}
}