1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-13 01:36:18 +00:00

Initial commit

This commit is contained in:
jendib
2013-07-27 18:33:20 +02:00
parent 41cb6dd9ae
commit 9b74bd8194
156 changed files with 72879 additions and 0 deletions

130
docs-web-common/pom.xml Normal file
View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../docs-parent</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>docs-web-common</artifactId>
<packaging>jar</packaging>
<name>Docs Web Commons</name>
<dependencies>
<!-- Dependencies to Docs -->
<dependency>
<groupId>com.sismics.docs</groupId>
<artifactId>docs-core</artifactId>
</dependency>
<!-- Dependencies to Jersey -->
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-bundle</artifactId>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-multipart</artifactId>
</dependency>
<!-- Other external dependencies -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.grizzly</groupId>
<artifactId>grizzly-servlet-webserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.jersey.jersey-test-framework</groupId>
<artifactId>jersey-test-framework-grizzly2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.subethamail</groupId>
<artifactId>subethasmtp-wiser</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Install test jar -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,54 @@
package com.sismics.rest.exception;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Jersey exception encapsulating an error from the client (BAD_REQUEST).
*
* @author jtremeaux
*/
public class ClientException extends WebApplicationException {
/**
* Serial UID.
*/
private static final long serialVersionUID = 1L;
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(ClientException.class);
/**
* Constructor of ClientException.
*
* @param type Error type (e.g. AlreadyExistingEmail, ValidationError)
* @param message Human readable error message
* @param e Readable error message
* @throws JSONException
*/
public ClientException(String type, String message, Exception e) throws JSONException {
this(type, message);
log.error(type + ": " + message, e);
}
/**
* Constructor of ClientException.
*
* @param type Error type (e.g. AlreadyExistingEmail, ValidationError)
* @param message Human readable error message
* @throws JSONException
*/
public ClientException(String type, String message) throws JSONException {
super(Response.status(Status.BAD_REQUEST).entity(new JSONObject()
.put("type", type)
.put("message", message)).build());
}
}

View File

@@ -0,0 +1,31 @@
package com.sismics.rest.exception;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
/**
* Unauthorized access to the resource exception.
*
* @author jtremeaux
*/
public class ForbiddenClientException extends WebApplicationException {
/**
* Serial UID.
*/
private static final long serialVersionUID = 1L;
/**
* Constructor of ForbiddenClientException.
*
* @throws JSONException
*/
public ForbiddenClientException() throws JSONException {
super(Response.status(Status.FORBIDDEN).entity(new JSONObject()
.put("type", "ForbiddenError")
.put("message", "You don't have access to this resource")).build());
}
}

View File

@@ -0,0 +1,53 @@
package com.sismics.rest.exception;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
/**
* Jersey exception encapsulating an error from the client (INTERNAL_SERVER_ERROR).
*
* @author jtremeaux
*/
public class ServerException extends WebApplicationException {
/**
* Serial UID.
*/
private static final long serialVersionUID = 1L;
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(ServerException.class);
/**
* Constructor of ClientException.
*
* @param type Error type (e.g. DatabaseError)
* @param message Human readable error message
* @param e Inner exception
* @throws JSONException
*/
public ServerException(String type, String message, Exception e) throws JSONException {
this(type, message);
log.error(type + ": " + message, e);
}
/**
* Constructor of ClientException.
*
* @param type Error type (e.g. DatabaseError)
* @param message Human readable error message
* @throws JSONException
*/
public ServerException(String type, String message) throws JSONException {
super(Response.status(Status.INTERNAL_SERVER_ERROR).entity(new JSONObject()
.put("type", type)
.put("message", message)).build());
}
}

View File

@@ -0,0 +1,45 @@
package com.sismics.rest.resource;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Generic exception mapper that transforms all unknown exception into ServerError.
*
* @author jtremeaux
*/
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(GenericExceptionMapper.class);
@Override
public Response toResponse(Exception e) {
if (e instanceof WebApplicationException) {
return ((WebApplicationException) e).getResponse();
}
log.error("Unknown error", e);
JSONObject entity = new JSONObject();
try {
entity.put("type", "UnknownError");
entity.put("message", "Unknown server error");
} catch (JSONException e2) {
log.error("Error building response", e2);
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(entity)
.build();
}
}

View File

@@ -0,0 +1,40 @@
package com.sismics.rest.util;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
/**
* JSON utilities.
*
* @author jtremeaux
*/
public class JsonUtil {
/**
* Fix of {@see JsonObject.append()}, which seems to create nested arrays.
*
* @param o JSON Object
* @param key Key containing the array of null
* @param value Value to append
* @return Updated object
* @throws JSONException
*/
public static JSONObject append(JSONObject o, String key, JSONObject value) throws JSONException {
Object prevValue = o.opt(key);
if (prevValue == null) {
o.put(key, new JSONArray().put(value));
} else if (!(prevValue instanceof JSONArray)){
throw new JSONException("JSONObject[" + key + "] is not a JSONArray.");
} else {
JSONArray newArray = new JSONArray();
JSONArray oldArray = ((JSONArray) prevValue);
for (int i = 0; i < oldArray.length(); i++) {
newArray.put(oldArray.get(i));
}
newArray.put(value);
o.put(key, newArray);
}
return o;
}
}

View File

@@ -0,0 +1,214 @@
package com.sismics.rest.util;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.joda.time.DateTime;
import com.google.common.base.Strings;
import com.sismics.docs.core.dao.file.theme.ThemeDao;
import com.sismics.docs.core.dao.jpa.LocaleDao;
import com.sismics.docs.core.model.jpa.Locale;
import com.sismics.rest.exception.ClientException;
/**
* Utility class to validate parameters.
*
* @author jtremeaux
*/
public class ValidationUtil {
private static Pattern EMAIL_PATTERN = Pattern.compile(".+@.+\\..+");
private static Pattern HTTP_URL_PATTERN = Pattern.compile("https?://.+");
private static Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9_]+");
/**
* Checks that the argument is not null.
*
* @param s Object tu validate
* @param name Name of the parameter
* @throws JSONException
*/
public static void validateRequired(Object s, String name) throws JSONException {
if (s == null) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be set", name));
}
}
/**
* Validate a string length.
*
* @param s String to validate
* @param name Name of the parameter
* @param lengthMin Minimum length (or null)
* @param lengthMax Maximum length (or null)
* @param nullable True if the string can be empty or null
* @return String without white spaces
* @throws ClientException
*/
public static String validateLength(String s, String name, Integer lengthMin, Integer lengthMax, boolean nullable) throws JSONException {
s = StringUtils.strip(s);
if (nullable && StringUtils.isEmpty(s)) {
return s;
}
if (s == null) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be set", name));
}
if (lengthMin != null && s.length() < lengthMin) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be more than {1} characters", name, lengthMin));
}
if (lengthMax != null && s.length() > lengthMax) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be more than {1} characters", name, lengthMax));
}
return s;
}
/**
* Validate a string length. The string mustn't be empty.
*
* @param s String to validate
* @param name Name of the parameter
* @param lengthMin Minimum length (or null)
* @param lengthMax Maximum length (or null)
* @return String without white spaces
* @throws ClientException
*/
public static String validateLength(String s, String name, Integer lengthMin, Integer lengthMax) throws JSONException {
return validateLength(s, name, lengthMin, lengthMax, false);
}
/**
* Checks if the string is not null and is not only whitespaces.
*
* @param s String to validate
* @param name Name of the parameter
* @return String without white spaces
* @throws JSONException
*/
public static String validateStringNotBlank(String s, String name) throws JSONException {
return validateLength(s, name, 1, null, false);
}
/**
* Checks if the string is an email.
*
* @param s String to validate
* @param name Name of the parameter
* @throws JSONException
*/
public static void validateEmail(String s, String name) throws JSONException {
if (!EMAIL_PATTERN.matcher(s).matches()) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be an email", name));
}
}
/**
* Validates that the provided string matches an URL with HTTP or HTTPS scheme.
*
* @param s String to validate
* @param name Name of the parameter
* @return Stripped URL
* @throws JSONException
*/
public static String validateHttpUrl(String s, String name) throws JSONException {
s = StringUtils.strip(s);
if (!HTTP_URL_PATTERN.matcher(s).matches()) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be an HTTP(s) URL", name));
}
return s;
}
/**
* Checks if the string uses only alphanumerical or underscore characters.
*
* @param s String to validate
* @param name Name of the parameter
* @throws JSONException
*/
public static void validateAlphanumeric(String s, String name) throws JSONException {
if (!ALPHANUMERIC_PATTERN.matcher(s).matches()) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must have only alphanumeric or underscore characters", name));
}
}
/**
* Validates and parses a date.
*
* @param s String to validate
* @param name Name of the parameter
* @param nullable True if the string can be empty or null
* @return Parsed date
* @throws JSONException
*/
public static Date validateDate(String s, String name, boolean nullable) throws JSONException {
if (Strings.isNullOrEmpty(s)) {
if (!nullable) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be set", name));
} else {
return null;
}
}
try {
return new DateTime(Long.parseLong(s)).toDate();
} catch (NumberFormatException e) {
throw new ClientException("ValidationError", MessageFormat.format("{0} must be a date", name));
}
}
/**
* Validates a locale.
*
* @param localeId String to validate
* @param name Name of the parameter
* @return String without white spaces
* @param nullable True if the string can be empty or null
* @throws ClientException
*/
public static String validateLocale(String localeId, String name, boolean nullable) throws JSONException {
localeId = StringUtils.strip(localeId);
if (StringUtils.isEmpty(localeId)) {
if (!nullable) {
throw new ClientException("ValidationError", MessageFormat.format("{0} is required", name));
} else {
return null;
}
}
LocaleDao localeDao = new LocaleDao();
Locale locale = localeDao.getById(localeId);
if (locale == null) {
throw new ClientException("ValidationError", "Locale not found: " + localeId);
}
return localeId;
}
/**
* Validates a theme.
*
* @param themeId ID of the theme to validate
* @param name Name of the parameter
* @return String without white spaces
* @param nullable True if the string can be empty or null
* @throws ClientException
*/
public static String validateTheme(String themeId, String name, boolean nullable) throws JSONException {
themeId = StringUtils.strip(themeId);
if (StringUtils.isEmpty(themeId)) {
if (!nullable) {
throw new ClientException("ValidationError", MessageFormat.format("{0} is required", name));
} else {
return null;
}
}
ThemeDao themeDao = new ThemeDao();
List<String> themeList = themeDao.findAll();
if (!themeList.contains(themeId)) {
throw new ClientException("ValidationError", "Theme not found: " + themeId);
}
return themeId;
}
}

View File

@@ -0,0 +1,79 @@
package com.sismics.security;
import java.util.Locale;
import org.joda.time.DateTimeZone;
/**
* Anonymous principal.
*
* @author jtremeaux
*/
public class AnonymousPrincipal implements IPrincipal {
public static final String ANONYMOUS = "anonymous";
/**
* User locale.
*/
private Locale locale;
/**
* User timezone.
*/
private DateTimeZone dateTimeZone;
/**
* Constructor of AnonymousPrincipal.
*/
public AnonymousPrincipal() {
// NOP
}
@Override
public String getId() {
return null;
}
@Override
public String getName() {
return ANONYMOUS;
}
@Override
public boolean isAnonymous() {
return true;
}
@Override
public Locale getLocale() {
return locale;
}
/**
* Setter of locale.
*
* @param locale locale
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
@Override
public DateTimeZone getDateTimeZone() {
return dateTimeZone;
}
@Override
public String getEmail() {
return null;
}
/**
* Setter of dateTimeZone.
*
* @param dateTimeZone dateTimeZone
*/
public void setDateTimeZone(DateTimeZone dateTimeZone) {
this.dateTimeZone = dateTimeZone;
}
}

View File

@@ -0,0 +1,48 @@
package com.sismics.security;
import java.security.Principal;
import java.util.Locale;
import org.joda.time.DateTimeZone;
/**
* Interface of principals.
*
* @author jtremeaux
*/
public interface IPrincipal extends Principal {
/**
* Checks if the principal is anonymous.
*
* @return True if the principal is anonymous.
*/
boolean isAnonymous();
/**
* Returns the ID of the connected user, or null if the user is anonymous
*
* @return ID of the connected user
*/
public String getId();
/**
* Returns the locale of the principal.
*
* @return Locale of the principal
*/
public Locale getLocale();
/**
* Returns the timezone of the principal.
*
* @return Timezone of the principal
*/
public DateTimeZone getDateTimeZone();
/**
* Returns the email of the principal.
*
* @return Email of the principal
*/
public String getEmail();
}

View File

@@ -0,0 +1,148 @@
package com.sismics.security;
import java.util.Locale;
import java.util.Set;
import org.joda.time.DateTimeZone;
/**
* Authenticated users principal.
*
* @author jtremeaux
*/
public class UserPrincipal implements IPrincipal {
/**
* ID of the user.
*/
private String id;
/**
* Username of the user.
*/
private String name;
/**
* Locale of the principal.
*/
private Locale locale;
/**
* Timezone of the principal.
*/
private DateTimeZone dateTimeZone;
/**
* Email of the principal.
*/
private String email;
/**
* User base functions.
*/
private Set<String> baseFunctionSet;
/**
* Constructor of UserPrincipal.
*
* @param id ID of the user
* @param name Usrename of the user
*/
public UserPrincipal(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean isAnonymous() {
return false;
}
@Override
public String getId() {
return id;
}
/**
* Setter of id.
*
* @param id id
*/
public void setId(String id) {
this.id = id;
}
@Override
public String getName() {
return name;
}
/**
* Setter of name.
*
* @param name name
*/
public void setName(String name) {
this.name = name;
}
@Override
public Locale getLocale() {
return locale;
}
/**
* Setter of locale.
*
* @param locale locale
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
@Override
public DateTimeZone getDateTimeZone() {
return dateTimeZone;
}
/**
* Setter of dateTimeZone.
*
* @param dateTimeZone dateTimeZone
*/
public void setDateTimeZone(DateTimeZone dateTimeZone) {
this.dateTimeZone = dateTimeZone;
}
@Override
public String getEmail() {
return email;
}
/**
* Setter of email.
*
* @param email email
*/
public void setEmail(String email) {
this.email = email;
}
/**
* Getter of baseFunctionSet.
*
* @return baseFunctionSet
*/
public Set<String> getBaseFunctionSet() {
return baseFunctionSet;
}
/**
* Setter of baseFunctionSet.
*
* @param baseFunctionSet baseFunctionSet
*/
public void setBaseFunctionSet(Set<String> baseFunctionSet) {
this.baseFunctionSet = baseFunctionSet;
}
}

View File

@@ -0,0 +1,151 @@
package com.sismics.util.filter;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Locale;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Level;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.util.DirectoryUtil;
import com.sismics.docs.core.util.TransactionUtil;
import com.sismics.util.EnvironmentUtil;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.jpa.EMF;
/**
* Filter used to process a couple things in the request context.
*
* @author jtremeaux
*/
public class RequestContextFilter implements Filter {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(RequestContextFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Force the locale in order to not depend on the execution environment
Locale.setDefault(new Locale(Constants.DEFAULT_LOCALE_ID));
// Injects the webapp root
String webappRoot = filterConfig.getServletContext().getRealPath("/");
EnvironmentUtil.setWebappRoot(webappRoot);
// Initialize the app directory
File baseDataDirectory = null;
try {
baseDataDirectory = DirectoryUtil.getBaseDataDirectory();
} catch (Exception e) {
log.error("Error initializing base data directory", e);
}
if (log.isInfoEnabled()) {
log.info(MessageFormat.format("Using base data directory: {0}", baseDataDirectory.toString()));
}
// Initialize file logger
RollingFileAppender fileAppender = new RollingFileAppender();
fileAppender.setName("FILE");
fileAppender.setFile(DirectoryUtil.getLogDirectory() + File.separator + "docs.log");
fileAppender.setLayout(new PatternLayout("%d{DATE} %p %l %m %n"));
fileAppender.setThreshold(Level.INFO);
fileAppender.setAppend(true);
fileAppender.setMaxFileSize("5MB");
fileAppender.setMaxBackupIndex(5);
fileAppender.activateOptions();
org.apache.log4j.Logger.getRootLogger().addAppender(fileAppender);
// Initialize the application context
TransactionUtil.handle(new Runnable() {
@Override
public void run() {
AppContext.getInstance();
}
});
}
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
EntityManager em = null;
try {
em = EMF.get().createEntityManager();
} catch (Exception e) {
throw new ServletException("Cannot create entity manager", e);
}
ThreadLocalContext context = ThreadLocalContext.get();
context.setEntityManager(em);
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
ThreadLocalContext.cleanup();
log.error("An exception occured, rolling back current transaction", e);
// If an unprocessed error comes up from the application layers (Jersey...), rollback the transaction (should not happen)
if (em.isOpen()) {
if (em.getTransaction() != null && em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
try {
em.close();
} catch (Exception ce) {
log.error("Error closing entity manager", ce);
}
}
throw new ServletException(e);
}
ThreadLocalContext.cleanup();
// No error processing the request : commit / rollback the current transaction depending on the HTTP code
if (em.isOpen()) {
if (em.getTransaction() != null && em.getTransaction().isActive()) {
HttpServletResponse r = (HttpServletResponse) response;
int statusClass = r.getStatus() / 100;
if (statusClass == 2 || statusClass == 3) {
try {
em.getTransaction().commit();
} catch (Exception e) {
log.error("Error during commit", e);
r.sendError(500);
}
} else {
em.getTransaction().rollback();
}
try {
em.close();
} catch (Exception e) {
log.error("Error closing entity manager", e);
}
}
}
}
}

View File

@@ -0,0 +1,180 @@
package com.sismics.util.filter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.jpa.AuthenticationToken;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.security.AnonymousPrincipal;
import com.sismics.security.UserPrincipal;
import com.sismics.util.LocaleUtil;
/**
* This filter is used to authenticate the user having an active session via an authentication token stored in database.
* The filter extracts the authentication token stored in a cookie.
* If the ocokie exists and the token is valid, the filter injects a UserPrincipal into a request attribute.
* If not, the user is anonymous, and the filter injects a AnonymousPrincipal into the request attribute.
*
* @author jtremeaux
*/
public class TokenBasedSecurityFilter implements Filter {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class);
/**
* Name of the cookie used to store the authentication token.
*/
public static final String COOKIE_NAME = "auth_token";
/**
* Name of the attribute containing the principal.
*/
public static final String PRINCIPAL_ATTRIBUTE = "principal";
/**
* Lifetime of the authentication token in seconds, since login.
*/
public static final int TOKEN_LONG_LIFETIME = 3600 * 24 * 365 * 20;
/**
* Lifetime of the authentication token in seconds, since last connection.
*/
public static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// Get the value of the client authentication token
HttpServletRequest request = (HttpServletRequest) req;
String authToken = null;
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (COOKIE_NAME.equals(cookie.getName())) {
authToken = cookie.getValue();
}
}
}
// Get the corresponding server token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
AuthenticationToken authenticationToken = null;
if (authToken != null) {
authenticationToken = authenticationTokenDao.get(authToken);
}
if (authenticationToken == null) {
injectAnonymousUser(request);
} else {
// Check if the token is still valid
if (isTokenExpired(authenticationToken)) {
try {
injectAnonymousUser(request);
// Destroy the expired token
authenticationTokenDao.delete(authToken);
} catch (Exception e) {
if (log.isErrorEnabled()) {
log.error(MessageFormat.format("Error deleting authentication token {0} ", authToken), e);
}
}
} else {
// Check if the user is still valid
UserDao userDao = new UserDao();
User user = userDao.getById(authenticationToken.getUserId());
if (user != null && user.getDeleteDate() == null) {
injectAuthenticatedUser(request, user);
// Update the last connection date
authenticationTokenDao.updateLastConnectionDate(authenticationToken.getId());
} else {
injectAnonymousUser(request);
}
}
}
filterChain.doFilter(request, response);
}
/**
* Returns true if the token is expired.
*
* @param authenticationToken Authentication token
* @return Token expired
*/
private boolean isTokenExpired(AuthenticationToken authenticationToken) {
final long now = new Date().getTime();
final long creationDate = authenticationToken.getCreationDate().getTime();
if (authenticationToken.isLongLasted()) {
return now >= creationDate + ((long) TOKEN_LONG_LIFETIME) * 1000L;
} else {
long date = authenticationToken.getLastConnectionDate() != null ?
authenticationToken.getLastConnectionDate().getTime() : creationDate;
return now >= date + ((long) TOKEN_SESSION_LIFETIME) * 1000L;
}
}
/**
* Inject an authenticated user into the request attributes.
*
* @param request HTTP request
* @param user User to inject
*/
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
// Add locale
Locale locale = LocaleUtil.getLocale(user.getLocaleId());
userPrincipal.setLocale(locale);
// Add base functions
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(user.getRoleId());
userPrincipal.setBaseFunctionSet(baseFunctionSet);
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
}
/**
* Inject an anonymous user into the request attributes.
*
* @param request HTTP request
*/
private void injectAnonymousUser(HttpServletRequest request) {
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
anonymousPrincipal.setLocale(request.getLocale());
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
}
}

View File

@@ -0,0 +1,115 @@
package com.sismics.docs.rest;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.List;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.StaticHttpHandler;
import org.junit.After;
import org.junit.Before;
import org.subethamail.wiser.Wiser;
import org.subethamail.wiser.WiserMessage;
import com.sismics.docs.rest.descriptor.JerseyTestWebAppDescriptorFactory;
import com.sismics.docs.rest.util.ClientUtil;
import com.sun.jersey.test.framework.JerseyTest;
/**
* Base class of integration tests with Jersey.
*
* @author jtremeaux
*/
public abstract class BaseJerseyTest extends JerseyTest {
/**
* Test email server.
*/
protected Wiser wiser;
/**
* Test HTTP server.
*/
HttpServer httpServer;
/**
* Utility class for the REST client.
*/
protected ClientUtil clientUtil;
/**
* Constructor of BaseJerseyTest.
*/
public BaseJerseyTest() {
super(JerseyTestWebAppDescriptorFactory.build());
this.clientUtil = new ClientUtil(resource());
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
wiser = new Wiser();
wiser.setPort(2500);
wiser.start();
String httpRoot = URLDecoder.decode(new File(getClass().getResource("/").getFile()).getAbsolutePath(), "utf-8");
httpServer = HttpServer.createSimpleServer(httpRoot, "localhost", 9997);
// Disable file cache to fix https://java.net/jira/browse/GRIZZLY-1350
((StaticHttpHandler) httpServer.getServerConfiguration().getHttpHandlers().keySet().iterator().next()).setFileCacheEnabled(false);
httpServer.start();
}
@Override
@After
public void tearDown() throws Exception {
super.tearDown();
wiser.stop();
httpServer.stop();
}
/**
* Extracts an email from the queue and consumes the email.
*
* @return Text of the email
* @throws MessagingException
* @throws IOException
*/
protected String popEmail() throws MessagingException, IOException {
List<WiserMessage> wiserMessageList = wiser.getMessages();
if (wiserMessageList.isEmpty()) {
return null;
}
WiserMessage wiserMessage = wiserMessageList.get(wiserMessageList.size() - 1);
wiserMessageList.remove(wiserMessageList.size() - 1);
MimeMessage message = wiserMessage.getMimeMessage();
ByteArrayOutputStream os = new ByteArrayOutputStream();
message.writeTo(os);
String body = os.toString();
return body;
}
/**
* Encodes a string to "quoted-printable" characters to compare with the contents of an email.
*
* @param input String to encode
* @return Encoded string
* @throws MessagingException
* @throws IOException
*/
protected String encodeQuotedPrintable(String input) throws MessagingException, IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream os = MimeUtility.encode(baos, "quoted-printable");
os.write(input.getBytes());
os.close();
return baos.toString();
}
}

View File

@@ -0,0 +1,35 @@
package com.sismics.docs.rest.descriptor;
import java.io.File;
import com.sismics.util.filter.RequestContextFilter;
import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sun.jersey.test.framework.WebAppDescriptor;
/**
* Jersey tests Webapp descriptor.
*
* @author jtremeaux
*/
public class JerseyTestWebAppDescriptorFactory {
private static String basePath = new File("src/main/webapp").getAbsolutePath();
/**
* Constructs a new descriptor.
*
* @return Descriptor
*/
public static WebAppDescriptor build() {
// Target the base path to the Webapp resources
System.setProperty("user.dir", basePath);
System.setProperty("test", "true");
return new WebAppDescriptor.Builder("com.sismics.docs.rest.resource")
.contextPath("docs")
.addFilter(RequestContextFilter.class, "requestContextFilter")
.addFilter(TokenBasedSecurityFilter.class, "tokenBasedSecurityFilter")
.initParam("com.sun.jersey.spi.container.ContainerRequestFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
.initParam("com.sun.jersey.spi.container.ContainerResponseFilters", "com.sun.jersey.api.container.filter.LoggingFilter")
.build();
}
}

View File

@@ -0,0 +1,41 @@
package com.sismics.docs.rest.filter;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.core.Cookie;
import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientRequest;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.filter.ClientFilter;
/**
* Filter to add the authentication token into a cookie.
*
* @author jtremeaux
*/
public class CookieAuthenticationFilter extends ClientFilter {
private String authToken;
public CookieAuthenticationFilter(String authToken) {
this.authToken = authToken;
}
@Override
public ClientResponse handle(ClientRequest request) throws ClientHandlerException {
Cookie cookie = new Cookie(TokenBasedSecurityFilter.COOKIE_NAME, authToken);
List<Object> cookieList = new ArrayList<Object>();
cookieList.add(cookie);
if (authToken != null) {
request.getHeaders().put("Cookie", cookieList);
}
ClientResponse response = getNext().handle(request);
if (response.getCookies() != null) {
cookieList.addAll(response.getCookies());
}
return response;
}
}

View File

@@ -0,0 +1,113 @@
package com.sismics.docs.rest.util;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import junit.framework.Assert;
import com.sismics.docs.rest.filter.CookieAuthenticationFilter;
import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.core.util.MultivaluedMapImpl;
/**
* REST client utilities.
*
* @author jtremeaux
*/
public class ClientUtil {
private WebResource resource;
/**
* Constructor of ClientUtil.
*
* @param webResource Resource corresponding to the base URI of REST resources.
*/
public ClientUtil(WebResource resource) {
this.resource = resource;
}
/**
* Creates a user.
*
* @param username Username
*/
public void createUser(String username) {
// Login admin to create the user
String adminAuthenticationToken = login("admin", "admin", false);
// Create the user
WebResource userResource = resource.path("/user");
userResource.addFilter(new CookieAuthenticationFilter(adminAuthenticationToken));
MultivaluedMap<String, String> postParams = new MultivaluedMapImpl();
postParams.putSingle("username", username);
postParams.putSingle("email", username + "@docs.com");
postParams.putSingle("password", "12345678");
postParams.putSingle("time_zone", "Asia/Tokyo");
ClientResponse response = userResource.put(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
// Logout admin
logout(adminAuthenticationToken);
}
/**
* Connects a user to the application.
*
* @param username Username
* @param password Password
* @param remember Remember user
* @return Authentication token
*/
public String login(String username, String password, Boolean remember) {
WebResource userResource = resource.path("/user/login");
MultivaluedMap<String, String> postParams = new MultivaluedMapImpl();
postParams.putSingle("username", username);
postParams.putSingle("password", password);
postParams.putSingle("remember", remember.toString());
ClientResponse response = userResource.post(ClientResponse.class, postParams);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
return getAuthenticationCookie(response);
}
/**
* Connects a user to the application.
*
* @param username Username
* @return Authentication token
*/
public String login(String username) {
return login(username, "12345678", false);
}
/**
* Disconnects a user from the application.
*
* @param authenticationToken Authentication token
*/
public void logout(String authenticationToken) {
WebResource userResource = resource.path("/user/logout");
userResource.addFilter(new CookieAuthenticationFilter(authenticationToken));
ClientResponse response = userResource.post(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
}
/**
* Extracts the authentication token of the response.
*
* @param response Response
* @return Authentication token
*/
public String getAuthenticationCookie(ClientResponse response) {
String authToken = null;
for (NewCookie cookie : response.getCookies()) {
if (TokenBasedSecurityFilter.COOKIE_NAME.equals(cookie.getName())) {
authToken = cookie.getValue();
}
}
return authToken;
}
}

View File

@@ -0,0 +1,34 @@
package com.sismics.docs.rest.util;
import junit.framework.Assert;
import org.junit.Test;
import com.sismics.rest.exception.ClientException;
import com.sismics.rest.util.ValidationUtil;
/**
* Test the validations.
*
* @author jtremeaux
*/
public class TestValidationUtil {
@Test
public void testValidateHttpUrlFail() throws Exception {
ValidationUtil.validateHttpUrl("http://www.google.com", "url");
ValidationUtil.validateHttpUrl("https://www.google.com", "url");
ValidationUtil.validateHttpUrl(" https://www.google.com ", "url");
try {
ValidationUtil.validateHttpUrl("ftp://www.google.com", "url");
Assert.fail();
} catch (ClientException e) {
// NOP
}
try {
ValidationUtil.validateHttpUrl("http://", "url");
Assert.fail();
} catch (ClientException e) {
// NOP
}
}
}