package net.cloudcodex.server.service;
import static net.cloudcodex.shared.Errors.IMPOSSIBLE_EXISTS;
import static net.cloudcodex.shared.Errors.NOT_FOUND_CAMPAIGN;
import static net.cloudcodex.shared.Errors.NOT_FOUND_CHARACTER;
import static net.cloudcodex.shared.Errors.NOT_FOUND_USER;
import static net.cloudcodex.shared.Errors.REQUIRED;
import static net.cloudcodex.shared.Errors.USER_USURPATION_GM;
import static net.cloudcodex.shared.Errors.USER_USURPATION_NPC;
import static net.cloudcodex.shared.Errors.USER_USURPATION_PC;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import net.cloudcodex.server.Context;
import net.cloudcodex.server.data.Data;
import net.cloudcodex.server.data.Data.Campaign;
import net.cloudcodex.server.data.Data.CharacterNote;
import net.cloudcodex.server.data.Data.User;
import net.cloudcodex.server.data.campaign.character.CharacterDescriptionSDO;
import net.cloudcodex.server.data.campaign.character.CharacterSDO;
import net.cloudcodex.shared.Errors;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Transaction;
/**
* Service for Campaign
* @author Thomas
*/
public class CampaignService extends AbstractCampaignService {
/**
* @param store Google AppEngine datastore.
*/
public CampaignService(DatastoreService store) {
super(store);
}
/**
* Creates a campaign.
*
* @param name name of the campaign to create.
* @param master game master
* @return the newly created campaign object.
*/
public Data.Campaign createCampaign(Context context, String name,
String game, String description, String icon,
Data.User master) {
if(master == null || name == null) {
logger.severe("missing param");
context.addError(REQUIRED);
return null;
}
// check the campaign doesn't already exist
Campaign campaign = dao.getCampaignByName(context, name);
if(campaign != null) {
logger.severe("campaign '" + name + "' alreayd exists");
context.addError(IMPOSSIBLE_EXISTS, name);
return null;
}
campaign = new Campaign();
campaign.setName(name);
campaign.setGame(game);
campaign.setMaster(master);
campaign.setDate(new Date());
campaign.setDescription(description);
campaign.setIcon(icon);
campaign.setDate(new Date());
dao.save(context, campaign);
return campaign;
}
/**
* Updates a campaign.
*
* @param campaignId id of the campaign to update.
* @param name name of the campaign
* @param master game master
* @return the newly created campaign object.
*/
public Data.Campaign updateCampaign(Context context, long campaignId,
String name, String game, String description, String icon,
Data.User master) {
if(master == null || name == null) {
logger.severe("missing param");
context.addError(REQUIRED);
return null;
}
// Check campaign exists.
final Data.Campaign campaign = dao.readCampaign(context, campaignId);
if(campaign == null) {
logger.severe("Campaign not found " + campaignId);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
// Check current user is campaign GM
final Data.User user = context.getUser();
if(!isGameMaster(context, campaign)) {
logger.severe("Unauthorized access by " + user.getKey() + " on " + campaignId);
context.addError(USER_USURPATION_GM);
return null;
}
// check the campaign doesn't already exist
Campaign otherCampaign = dao.getCampaignByName(context, name);
if(otherCampaign != null) {
logger.severe("campaign '" + name + "' alreayd exists");
context.addError(IMPOSSIBLE_EXISTS, name);
return null;
}
campaign.setName(name);
campaign.setGame(game);
campaign.setDescription(description);
campaign.setIcon(icon);
dao.save(context, campaign);
return campaign;
}
public Data.Character createCharacter(Context context, Campaign campaign, String name,
User owner, String icon, String description) {
return createCharacter(context, campaign, name, owner, icon, description, false);
}
/**
* Creates a character if its doesn't already exists.
*
* @param campaign campaign of the character.
* @param name name of the character.
* @param owner owner (user) of the character.
* @return the newly created character.
*/
public Data.Character createCharacter(Context context, Campaign campaign, String name,
User owner, String icon, String description, boolean profile) {
if(campaign == null || name == null) {
logger.severe("missing param");
context.addError(REQUIRED);
return null;
}
name = name.trim();
// check the character doesn't already exist
Data.Character character =
dao.getCharacterByName(context, campaign.getKey(), name);
if(character != null) {
logger.severe("character '" + name
+ "' alreayd exists in " + campaign.getKey());
context.addError(IMPOSSIBLE_EXISTS, name, campaign.getKey());
return null;
}
character = new Data.Character(campaign);
character.setName(name);
character.setOwner(owner);
character.setOwnerNickname(owner == null ? null : owner.getNickname());
character.setDate(new Date());
character.setIcon(icon);
character.setDescription(description);
character.setProfile(profile);
dao.save(context, character);
return character;
}
/**
* Add a user to a campaign.
*/
public CharacterSDO inviteToCampaign(Context context, long campaignId, String email, String name) {
if(email == null || name == null) {
logger.severe("missing param");
context.addError(REQUIRED);
return null;
}
// Check campaign exists.
final Data.Campaign campaign = dao.readCampaign(context, campaignId);
if(campaign == null) {
logger.severe("Campaign not found " + campaignId);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
// Check current user is campaign GM
final Data.User user = context.getUser();
if(!isGameMaster(context, campaign)) {
logger.severe("Unauthorized access by " + user.getKey() + " on " + campaignId);
context.addError(USER_USURPATION_GM);
return null;
}
// Check the user to add
final Data.User player = dao.getUserByEmail(context, email);
if(player == null) {
logger.severe("User not found " + email);
context.addError(NOT_FOUND_USER, email);
return null;
}
// Check the GM is not inviting itself
if(StringUtils.equals(player.getEmail(), context.getUser().getEmail())) {
logger.severe("GM is inviting itself");
context.addError(Errors.IMPOSSIBLE_YOURSELF);
return null;
}
// Create the character.
final Data.Character character =
createCharacter(context, campaign, name, player, null, null);
if(character == null) {
return null;
}
final CharacterSDO sdo = new CharacterSDO();
sdo.setCampaign(campaign);
sdo.setCharacter(character);
return sdo;
}
/**
* Creates a NPC.
* @param context execution context.
* @param campaignId campaign's id.
* @param name NPC's name, cannot be null.
* @param icon NPC's icon.
* @param description NPC's description.
* @param <code>true</code> for profiles.
* @return the newly created NPC.
*/
public CharacterSDO createNPC(Context context, long campaignId,
String name, String icon, String description, boolean profile) {
if(name == null) {
logger.severe("no name specified");
context.addError(REQUIRED, "name");
return null;
}
// Check campaign exists.
final Data.Campaign campaign = dao.readCampaign(context, campaignId);
if(campaign == null) {
logger.severe("Campaign not found " + campaignId);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
// Check current user is campaign GM
final Data.User user = context.getUser();
if(!campaign.getMaster().equals(user.getKey())) {
logger.severe("Unauthorized access by "
+ user.getKey() + " on " + campaignId);
context.addError(USER_USURPATION_GM);
return null;
}
// Create the character.
final Data.Character character =
createCharacter(context, campaign, name, null, icon, description, profile);
if(character == null) {
return null;
}
final CharacterSDO sdo = new CharacterSDO();
sdo.setCampaign(campaign);
sdo.setCharacter(character);
return sdo;
}
/**
* Returns the campaign's characters, only GM cans call this method.
* @param context execution context.
* @param campaignId campaign id.
* @return the campaign's characters.
*/
public List<CharacterSDO> getCampaignCharacters(Context context, long campaignId) {
final Campaign campaign = dao.readCampaign(context, campaignId);
if(campaign == null) {
logger.severe("Campaign not found " + campaignId);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
final Key campaignKey = campaign.getKey();
// Check current user is campaign GM
final Data.User user = context.getUser();
if(!campaign.getMaster().equals(user.getKey())) {
logger.severe("Unauthorized access by "
+ user.getKey() + " on " + campaignKey);
context.addError(USER_USURPATION_GM);
return null;
}
// Get all characters
final List<Data.Character> characters = dao.asListOfCharacters(
context, dao.queryCharacter(campaignKey), null);
if(characters == null || characters.isEmpty()) {
return null;
}
// Aggregate all data.
final List<CharacterSDO> sdos = new ArrayList<CharacterSDO>();
for(Data.Character character : characters) {
final CharacterSDO sdo = new CharacterSDO();
sdo.setCampaign(campaign);
sdo.setCharacter(character);
sdos.add(sdo);
}
return sdos.isEmpty() ? null : sdos;
}
/**
* Consult the current user character header.
*
* @param context execution context.
* @param campaignId campaign's id.
* @param characterId character's id.
* @return the character's header.
*/
public CharacterSDO getCharacter(Context context, long campaignId, long characterId) {
final Campaign campaign = dao.readCampaign(context, campaignId);
if(campaign == null) {
logger.severe("Campaign not found " + campaignId);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
final Key campaignKey = campaign.getKey();
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
// Check the character exists
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(NOT_FOUND_CHARACTER, characterId);
return null;
}
// check its not a NPC
if(character.getOwner() == null) {
logger.severe("User " + context.getUser().getKey()
+ " tried to get character description as NPC");
context.addError(USER_USURPATION_NPC);
return null;
}
// check user is its owner.
if(!isOwner(context, character) && !isGameMaster(context, campaign)) {
logger.severe("User " + context.getUser().getKey()
+ " tried to get character description as "
+ characterKey);
context.addError(USER_USURPATION_PC);
return null;
}
final CharacterSDO sdo = new CharacterSDO();
sdo.setCampaign(campaign);
sdo.setCharacter(character);
return sdo;
}
/**
* @param context execution context.
* @param campaignId camapign's id.
* @param characterId character's id.
* @param timestamp last character description timestamp, may be null.
* @return the character description, or just the changes if timestamp is not null.
*/
public CharacterDescriptionSDO getCharacterDescription(
Context context, long campaignId, long characterId,
Long byCharacterId, Date timestamp) {
final Key campaignKey = Campaign.createKey(campaignId);
final Data.Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Invalid campaign " + campaignKey);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
// Check the user is GM
final boolean master = isGameMaster(context, campaign);
if(!master && byCharacterId == null) {
logger.severe("User " + context.getUser().getKey()
+ " tried to get character description as GM");
context.addError(USER_USURPATION_GM);
return null;
}
// Done before reading to avoid a concurrent insert never read after.
final Date readTimestamp = new Date();
final Query query;
if(timestamp != null) {
// Get notes with timestamp field > timestamp param
query = dao.addCharacterNoteFilterOnTimestamp(
dao.queryCharacterNote(characterKey),
FilterOperator.GREATER_THAN, timestamp);
} else {
query = dao.queryCharacterNote(characterKey);
}
final CharacterDescriptionSDO description = new CharacterDescriptionSDO();
description.setTimestamp(readTimestamp);
description.setCampaignId(campaignId);
description.setCharacterId(characterId);
// Get the character entity to test if its a profile or not.
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(NOT_FOUND_CHARACTER, characterId);
return null;
}
if(!Boolean.TRUE.equals(character.getProfile())) {
if(master) {
// GM sees all notes
description.setNotes(dao.asListOfCharacterNotes(context, query, null));
// GMM sees the character's sheet.
description.setSheet(character.getSheet());
} else {
// check the character viewing exists
final Key byCharacterKey = Data.Character.createKey(campaignKey, byCharacterId);
final Data.Character byCharacter = dao.readCharacter(context, byCharacterKey);
if(byCharacter == null) {
logger.severe("Invalid character " + byCharacterKey);
context.addError(NOT_FOUND_CHARACTER, byCharacterId);
return null;
}
// check its not a NPC
if(byCharacter.getOwner() == null) {
logger.severe("User " + context.getUser().getKey()
+ " tried to get character description as NPC");
context.addError(USER_USURPATION_NPC);
return null;
}
// check the user is its owner
if(!isOwner(context, byCharacter)) {
logger.severe("User " + context.getUser().getKey()
+ " tried to get character description as "
+ byCharacterKey);
context.addError(USER_USURPATION_PC);
return null;
}
// Itself and others players can only see their own.
description.setNotes(
dao.asListOfCharacterNotes(context,
dao.addCharacterNoteFilterOnAuthor(
query, FilterOperator.EQUAL, byCharacterKey), null));
// the character consults itself
if(byCharacterId != null && characterId == byCharacterId) {
description.setSheet(byCharacter.getSheet());
}
}
}
if(master) {
// map the character aliases.
final Map<String, String> aliases = character.getAlias();
if(aliases != null) {
final Map<Long, String> descAliases = new HashMap<Long, String>();
for(Map.Entry<String, String> entry : aliases.entrySet()) {
try {
final Long charId = Long.valueOf(entry.getKey());
descAliases.put(charId, entry.getValue());
} catch(NumberFormatException e) {
logger.severe(entry.getKey()
+ " is not a valid charId for alias in " + character);
}
}
if(descAliases != null) {
description.setAliases(descAliases);
}
}
}
return description;
}
/**
* Update a character description and notes about.
*
* @param context execution context.
* @param campaignId campaign's id.
* @param characterId description's character's id.
* @param byCharacterId id of the character updating the description,
* <code>null</code> for game master.
* @param description description to update, null to not update.
* @param notes map author/note to update.
* @param notes map PC/alias to update, only for GM.
*
* @return the result of {@link #getCharacterDescription(
* Context, long, long, Date)} after the update.
*/
public CharacterDescriptionSDO updateCharacterDescription(
Context context, long campaignId, long characterId, Long byCharacterId,
String description, String sheet, Boolean dead, Boolean locked,
Map<Long, String> notes, Map<Long, String> aliases) {
// Check the campaign and the master
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Campaign not found " + campaignKey);
context.addError(NOT_FOUND_CAMPAIGN, campaignId);
return null;
}
// see if the user is game master
final boolean master = isGameMaster(context, campaign);
// Check the user to update exists
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("character not found " + characterKey);
context.addError(NOT_FOUND_CHARACTER, characterId);
return null;
}
if(Boolean.TRUE.equals(character.getProfile())
&& notes != null && !notes.isEmpty()) {
logger.severe("Character " + characterKey + " is a profile, "
+ context.getUser().getKey() + " cannot make notes");
context.addError(Errors.IMPOSSIBLE_PROFILE);
return null;
}
final boolean npc = character.getOwner() == null;
// Do the updates in a single transaction
final Transaction tx = dao.beginTransaction();
try {
if(master) {
// Update the character's description.
if(description != null) {
if("".equals(description.trim())) {
character.setDescription(null);
} else {
character.setDescription(description);
}
}
// Update the character's sheet.
if(sheet != null) {
if("".equals(sheet.trim())) {
character.setSheet(null);
} else {
character.setSheet(sheet);
}
}
// Update the character's aliases
if(aliases != null) {
for(Map.Entry<Long, String> entry : aliases.entrySet()) {
if(entry.getKey() != null) {
character.setAlias(entry.getKey().toString(), entry.getValue());
}
}
}
// master can update all notes. (but profile can't have notes)
if(notes != null) {
for(Map.Entry<Long, String> entry : notes.entrySet()) {
// author's key is null for GM
final Key authorKey = entry.getKey() == null ? null
: Data.Character.createKey(campaignKey, entry.getKey());
// read the current note from this author
CharacterNote noteDB = dao.getCharacterNoteByAuthor(context, characterKey, authorKey);
if(noteDB == null) {
noteDB = new CharacterNote(character);
noteDB.setAuthor(authorKey);
}
// Notes are never deleted, just set to null (usefull when checking deltas)
noteDB.setContent(entry.getValue());
dao.save(context, noteDB);
}
}
if(dead != null) {
character.setDead(dead);
}
// Only PCs can be locked
if(locked != null && !npc) {
character.setLocked(locked);
}
dao.save(context, character);
} else {
if(notes != null) {
if(byCharacterId == null) {
logger.severe("user " + context.getUser().getKey()
+ " has tried to update GM note on " + characterKey);
context.addError(USER_USURPATION_GM);
return null;
}
// players can only update their own.
final Key authorKey = Data.Character.createKey(campaignKey, byCharacterId);
// check the writer exists
final Data.Character author = dao.readCharacter(context, authorKey);
if(author == null) {
logger.severe("character not found " + characterKey);
context.addError(NOT_FOUND_CHARACTER, characterId);
return null;
}
// check the user is the writer's owner
if(!isOwner(context, author)) {
logger.severe("user " + context.getUser().getKey() + " has tried to update "
+ author.getOwner() + "'s note on " + characterKey);
context.addError(Errors.USER_USURPATION);
return null;
}
// update or create the note if not an NPC
CharacterNote noteDB = dao.getCharacterNoteByAuthor(context, characterKey, authorKey);
if(noteDB == null) {
noteDB = new CharacterNote(character);
noteDB.setAuthor(authorKey);
}
noteDB.setContent(notes.get(byCharacterId));
dao.save(context, noteDB);
}
}
tx.commit();
} finally {
if(tx.isActive()) {
tx.rollback();
}
}
return getCharacterDescription(
context, campaignId, characterId, byCharacterId, null);
}
}