package org.swellrt.server.box.servlet;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.validator.EmailValidator;
import org.waveprotocol.box.server.account.AccountData;
import org.waveprotocol.box.server.account.HumanAccountData;
import org.waveprotocol.box.server.account.HumanAccountDataImpl;
import org.waveprotocol.box.server.authentication.PasswordDigest;
import org.waveprotocol.box.server.authentication.SessionManager;
import org.waveprotocol.box.server.persistence.AccountAttachmentStore;
import org.waveprotocol.box.server.persistence.AccountAttachmentStore.Attachment;
import org.waveprotocol.box.server.persistence.AccountStore;
import org.waveprotocol.box.server.persistence.AttachmentUtil;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.wave.model.wave.InvalidParticipantAddress;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.util.logging.Log;
import com.google.gson.JsonParseException;
import com.google.inject.Inject;
import com.typesafe.config.Config;
/**
* Service for creating and editing accounts.
*
* @author pablojan@gmail.com (Pablo Ojanguren)
*
* Create new account
*
* POST /account { id : <ParticipantId>, password : <String>, ... }
*
*
* Edit account profile (empty values are deleted)
*
* POST /account/{ParticipantId.name} { ... }
*
*
* Get account profile
*
* GET /account/{ParticipantId.name}
*
*
* Get multiple account profiles
*
* GET /account?p=user1@domain;user2@domain
*/
@SuppressWarnings("deprecation")
public class AccountService extends BaseService {
public static class AccountServiceData extends ServiceData {
public String id;
public String name;
public String password;
public String email;
public String avatarData;
public String avatarUrl;
public String locale;
public String sessionId;
public String transientSessionId;
public String domain;
public AccountServiceData() {
}
public AccountServiceData(String id) {
this.id = id;
}
}
private static final Log LOG = Log.get(AccountService.class);
private final AccountStore accountStore;
private final AccountAttachmentStore attachmentAccountStore;
private final String domain;
@Inject
public AccountService(SessionManager sessionManager, AccountStore accountStore,
AccountAttachmentStore attachmentAccountStore,
Config config) {
this(sessionManager, accountStore, attachmentAccountStore, config.getString("core.wave_server_domain"));
}
protected AccountService(SessionManager sessionManager, AccountStore accountStore,
AccountAttachmentStore attachmentAccountStore, String waveDomain) {
super(sessionManager);
this.accountStore = accountStore;
this.attachmentAccountStore = attachmentAccountStore;
this.domain = waveDomain;
}
protected ParticipantId getParticipantFromRequest(HttpServletRequest req)
throws InvalidParticipantAddress {
String[] pathTokens = SwellRtServlet.getCleanPathInfo(req).split("/");
String participantToken = pathTokens.length > 2 ? pathTokens[2] : null;
if (participantToken == null) throw new InvalidParticipantAddress("null", "Address is null");
ParticipantId participantId =
participantToken.contains("@") ? ParticipantId.of(participantToken) : ParticipantId
.of(participantToken + "@" + domain);
return participantId;
}
protected String getAvatarFileFromRequest(HttpServletRequest req)
throws InvalidParticipantAddress {
String[] pathTokens = SwellRtServlet.getCleanPathInfo(req).split("/");
String avatarFileName = pathTokens.length > 4 ? pathTokens[4] : null;
return avatarFileName;
}
protected Collection<ParticipantId> getParticipantsFromRequestQuery(HttpServletRequest req) {
try {
String query = URLDecoder.decode(req.getParameter("p"), "UTF-8");
String[] participantAddresses = query.split(";");
List<ParticipantId> participantIds= new ArrayList<ParticipantId>();
for (String address : participantAddresses) {
try {
participantIds.add(ParticipantId.ofUnsafe(address));
} catch (IllegalArgumentException e) {
// Ignore
}
}
return participantIds;
} catch (Exception e) {
return null;
}
}
protected void createAccount(HttpServletRequest req, HttpServletResponse response)
throws ServiceException, IOException {
UrlBuilder urlBuilder = ServiceUtils.getUrlBuilder(req);
try {
AccountServiceData userData = getRequestServiceData(req);
if (userData.id == null) {
sendResponseError(response, HttpServletResponse.SC_BAD_REQUEST, RC_MISSING_PARAMETER);
return;
}
ParticipantId participantId =
userData.id.contains("@") ? ParticipantId.of(userData.id) : ParticipantId.of(userData.id
+ "@" + domain);
AccountData accountData = accountStore.getAccount(participantId);
if (accountData != null) {
sendResponseError(response, HttpServletResponse.SC_FORBIDDEN, RC_ACCOUNT_ALREADY_EXISTS);
return;
}
if (userData.password == null) {
userData.password = "";
}
HumanAccountDataImpl account =
new HumanAccountDataImpl(participantId, new PasswordDigest(
userData.password.toCharArray()));
if (userData.email != null) {
if (!EmailValidator.getInstance().isValid(userData.email)) {
sendResponseError(response, HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_EMAIL_ADDRESS);
return;
}
account.setEmail(userData.email);
}
if (userData.locale != null) account.setLocale(userData.locale);
else {
account.setLocale(req.getLocale().toString());
}
if (userData.avatarData != null) {
// Store avatar
String avatarFileId = storeAvatar(participantId, userData.avatarData, null);
account.setAvatarFileId(avatarFileId);
}
accountStore.putAccount(account);
sendResponse(response, toServiceData(urlBuilder, account));
return;
} catch (PersistenceException e) {
throw new ServiceException("Can't write account to storage", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INTERNAL_SERVER_ERROR ,e);
} catch (JsonParseException e) {
throw new ServiceException("Can't parse JSON", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INVALID_JSON_SYNTAX ,e);
} catch (InvalidParticipantAddress e) {
throw new ServiceException("Can't get participant from request" ,HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_ACCOUNT_ID_SYNTAX, e);
}
}
protected void updateAccount(HttpServletRequest req, HttpServletResponse response)
throws ServiceException, IOException {
// POST /account/joe update user's account
ParticipantId loggedInUser = sessionManager.getLoggedInUser(req);
UrlBuilder urlBuilder = ServiceUtils.getUrlBuilder(req);
try {
ParticipantId participantId = getParticipantFromRequest(req);
// if the account exists, only the user can modify the profile
if (!participantId.equals(loggedInUser)) {
sendResponseError(response, HttpServletResponse.SC_FORBIDDEN, RC_ACCOUNT_NOT_LOGGED_IN);
return;
}
// Modify
AccountServiceData userData = getRequestServiceData(req);
if (participantId.isAnonymous()) {
updateAccountInSession(req, userData);
sendResponse(response, "");
} else {
AccountData accountData = accountStore.getAccount(participantId);
if (accountData == null) {
sendResponseError(response, HttpServletResponse.SC_FORBIDDEN, RC_ACCOUNT_NOT_FOUND);
return;
}
HumanAccountData account = accountData.asHuman();
updateAccountInStore(participantId, userData, account);
sendResponse(response, toServiceData(urlBuilder, account));
}
} catch (PersistenceException e) {
throw new ServiceException("Can't write account to storage", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INTERNAL_SERVER_ERROR ,e);
} catch (JsonParseException e) {
throw new ServiceException("Can't parse JSON", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INVALID_JSON_SYNTAX ,e);
} catch (InvalidParticipantAddress e) {
throw new ServiceException("Can't get participant from request" ,HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_ACCOUNT_ID_SYNTAX, e);
}
}
protected void updateAccountInSession(HttpServletRequest req, AccountServiceData receivedData) {
Map<String, String> sessionProps = new HashMap<String, String>();
if (receivedData.has("email") && !receivedData.email.isEmpty()) {
sessionProps.put("email", receivedData.email);
}
if (receivedData.has("name") && !receivedData.name.isEmpty()) {
sessionProps.put("name", receivedData.name);
}
sessionManager.setSessionProperties(req, sessionProps);
}
protected void updateAccountInStore(ParticipantId participantId, AccountServiceData receivedData, HumanAccountData account) throws ServiceException, IOException, PersistenceException {
if (receivedData.has("email")) {
try {
if (receivedData.email.isEmpty())
account.setEmail(null);
else
account.setEmail(receivedData.email);
} catch (IllegalArgumentException e) {
throw new ServiceException("Invalid email address", HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_EMAIL_ADDRESS);
}
}
if (receivedData.has("locale")) account.setLocale(receivedData.locale);
if (receivedData.has("avatarData")) {
if (receivedData.avatarData == null || receivedData.avatarData.isEmpty()
|| "data:".equals(receivedData.avatarData)) {
// Delete avatar
deleteAvatar(account.getAvatarFileId());
account.setAvatarFileId(null);
} else {
String avatarFileId =
storeAvatar(participantId, receivedData.avatarData, account.getAvatarFileName());
account.setAvatarFileId(avatarFileId);
}
}
if (receivedData.has("name")) account.setName(receivedData.name);
accountStore.putAccount(account);
}
/**
* Returns an avatar image from the provided participant
*
* @param avatarOwnerAddress
* @param req
* @param response
* @throws IOException
*/
protected void getAvatar(HttpServletRequest req, HttpServletResponse response) throws ServiceException, IOException {
// GET /account/joe/avatar/[filename]
try {
// We require an open session, at least anonymous
checkAnySession(req);
ParticipantId avatarOwnerId = getParticipantFromRequest(req);
// Retrieve the avatar's owner account data
AccountData accountData = accountStore.getAccount(avatarOwnerId);
if (accountData == null) {
throw new ServiceException("Can't retrieve user account", HttpServletResponse.SC_NOT_FOUND, "ACCOUNT_NOT_FOUND");
}
String fileName = getAvatarFileFromRequest(req);
if (fileName == null) {
throw new ServiceException("Can't extract avatar file from request", HttpServletResponse.SC_BAD_REQUEST, "ATTACHMENT_FILE_NAME_ERROR");
}
Attachment avatar = attachmentAccountStore.getAvatar(fileName);
if (avatar == null) {
LOG.warning("Avatar file not found: " + fileName);
throw new ServiceException("Avatar file not found", HttpServletResponse.SC_NOT_FOUND, "ACCOUNT_ATTACHMENT_NOT_FOUND");
}
response.setContentType(accountData.asHuman().getAvatarMimeType());
response.setContentLength((int) avatar.getSize());
response.setStatus(HttpServletResponse.SC_OK);
response.setDateHeader("Last-Modified", Calendar.getInstance().getTimeInMillis());
AttachmentUtil.writeTo(avatar.getInputStream(), response.getOutputStream());
} catch (PersistenceException e) {
throw new ServiceException("Can't read avatar from storage", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INTERNAL_SERVER_ERROR ,e);
} catch (InvalidParticipantAddress e) {
throw new ServiceException("Can't get participant from request" ,HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_ACCOUNT_ID_SYNTAX, e);
}
}
protected void getParticipantAccount(HttpServletRequest req, HttpServletResponse response)
throws ServiceException, IOException {
UrlBuilder urlBuilder = ServiceUtils.getUrlBuilder(req);
try {
ParticipantId loggedInUser = getLoggedInUser(req);
ParticipantId participantId = getParticipantFromRequest(req);
if (!participantId.equals(loggedInUser)) {
// GET /account/joe retrieve user's account data only public fields
AccountData accountData = accountStore.getAccount(participantId);
if (accountData == null) {
sendResponseError(response, HttpServletResponse.SC_NOT_FOUND, RC_ACCOUNT_NOT_FOUND);
}
sendResponse(response, toPublicServiceData(urlBuilder, accountData.asHuman()));
} else {
// GET /account/joe retrieve user's account data including private
// fields
if (participantId.isAnonymous()) {
Map<String, String> properties = sessionManager.getSessionProperties(req);
AccountServiceData data = new AccountServiceData();
data.id = participantId.getAddress();
data.email = properties.containsKey("email") ? properties.get("email") : null;
data.name = properties.containsKey("name") && !properties.get("name").isEmpty() ? properties.get("name") : "Anonymous";
sendResponse(response, data);
} else {
AccountData accountData = accountStore.getAccount(participantId);
if (accountData == null) {
sendResponseError(response, HttpServletResponse.SC_NOT_FOUND, RC_ACCOUNT_NOT_FOUND);
}
sendResponse(response, toServiceData(urlBuilder, accountData.asHuman()));
}
return;
}
} catch (PersistenceException e) {
throw new ServiceException("Can't read account from storage", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INTERNAL_SERVER_ERROR ,e);
} catch (InvalidParticipantAddress e) {
throw new ServiceException("Can't get participant from request" ,HttpServletResponse.SC_BAD_REQUEST, RC_INVALID_ACCOUNT_ID_SYNTAX, e);
}
}
protected void queryParticipantAccount(HttpServletRequest req, HttpServletResponse response)
throws ServiceException, IOException {
// GET /account?p=joe@local.net;tom@local.net
// We require an open session, at least anonymous
checkAnySession(req);
Collection<ParticipantId> participantsQuery = getParticipantsFromRequestQuery(req);
if (participantsQuery == null) {
sendResponseError(response, HttpServletResponse.SC_BAD_REQUEST, RC_MISSING_PARAMETER);
return;
}
sendResponse(response,
toPublicServiceData(ServiceUtils.getUrlBuilder(req), participantsQuery, accountStore));
}
@Override
public void execute(HttpServletRequest req, HttpServletResponse response) throws IOException {
String[] pathTokens = SwellRtServlet.getCleanPathInfo(req).split("/");
String participantToken = pathTokens.length > 2 ? pathTokens[2] : null;
String participantOp = pathTokens.length > 3 ? pathTokens[3] : null;
try {
if (req.getMethod().equals("POST") && participantToken == null) {
createAccount(req, response);
} else if (req.getMethod().equals("POST") && participantToken != null) {
updateAccount(req, response);
} else if (req.getMethod().equals("GET")) {
if (participantToken != null) {
if (participantOp != null && participantOp.equals("avatar"))
getAvatar(req, response);
else
getParticipantAccount(req, response);
} else {
queryParticipantAccount(req, response);
}
}
} catch (ServiceException e) {
sendResponseError(response, e.getHttpResponseCode(), e.getServiceResponseCode());
return;
} catch (IOException e) {
sendResponseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, RC_INTERNAL_SERVER_ERROR);
return;
}
}
protected String storeAvatar(ParticipantId participantId, String avatarData,
String currentAvatarFileName) throws IOException {
if (!avatarData.startsWith("data:")) {
throw new IOException("Avatar data syntax is not a valida RFC 2397 data URI");
}
// Store avatar first and get the storage's file name
int dataUriSeparatorIndex = avatarData.indexOf(";");
String mimeType = avatarData.substring("data:".length(), dataUriSeparatorIndex);
// Remove the base64, prefix
String base64Data = avatarData.substring(dataUriSeparatorIndex + 8, avatarData.length());
return attachmentAccountStore.storeAvatar(participantId, mimeType, base64Data,
currentAvatarFileName);
}
protected void deleteAvatar(String avatarFileId) {
}
protected AccountServiceData getRequestServiceData(HttpServletRequest request)
throws JsonParseException, IOException {
StringWriter writer = new StringWriter();
IOUtils.copy(request.getInputStream(), writer, Charset.forName("UTF-8"));
String json = writer.toString();
if (json == null) throw new JsonParseException("Null JSON message");
return (AccountServiceData) ServiceData.fromJson(json, AccountServiceData.class);
}
protected Collection<AccountServiceData> toPublicServiceData(UrlBuilder urlBuilder,
Collection<ParticipantId> participants, AccountStore accountStore) {
List<AccountServiceData> accountServiceDataList = new ArrayList<AccountServiceData>();
for (ParticipantId p : participants) {
try {
AccountData accountData = accountStore.getAccount(p);
if (accountData != null && accountData.isHuman()) {
accountServiceDataList.add(toPublicServiceData(urlBuilder, accountData.asHuman()));
}
} catch (PersistenceException e) {
// Ignore missing accounts
}
}
return accountServiceDataList;
}
protected static String getAvatarUrl(UrlBuilder urlBuilder, HumanAccountData account) {
if (account.getAvatarFileName() == null) return null;
return urlBuilder.build(
"/account/" + account.getId().getName() + "/avatar/" + account.getAvatarFileName(), null);
}
protected static AccountServiceData toServiceData(UrlBuilder urlBuilder, HumanAccountData account) {
AccountServiceData data = new AccountServiceData();
data.id = account.getId().getAddress();
data.email = account.getEmail() == null ? "" : account.getEmail();
String avatarUrl = getAvatarUrl(urlBuilder, account);
data.avatarUrl = avatarUrl == null ? "" : avatarUrl;
data.locale = account.getLocale() == null ? "" : account.getLocale();
data.name = account.getName() == null ? "" : account.getName();
return data;
}
protected static AccountServiceData toPublicServiceData(UrlBuilder urlBuilder,
HumanAccountData account) {
AccountServiceData data = new AccountServiceData();
data.id = account.getId().getAddress();
String avatarUrl = getAvatarUrl(urlBuilder, account);
data.avatarUrl = avatarUrl == null ? "" : avatarUrl;
data.locale = account.getLocale() == null ? "" : account.getLocale();
data.name = account.getName() == null ? "" : account.getName();
return data;
}
}