package net.cloudcodex.server.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import net.cloudcodex.server.Context;
import net.cloudcodex.server.data.Data;
import net.cloudcodex.server.data.Data.Campaign;
import net.cloudcodex.server.data.Data.Message;
import net.cloudcodex.server.data.Data.Scene;
import net.cloudcodex.server.data.Data.User;
import net.cloudcodex.server.data.campaign.msg.SceneSDO;
import net.cloudcodex.server.data.campaign.scene.SceneToCreateSDO;
import net.cloudcodex.shared.Errors;
import net.cloudcodex.shared.MessageAction;
import net.cloudcodex.shared.MessageType;
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 Messages
* @author Thomas
*/
public class MessageService extends AbstractCampaignService {
/**
* @param store Google AppEngine datastore.
*/
public MessageService(DatastoreService store) {
super(store);
}
/**
* Create a scene.
* TODO : utility, replace with a complete scenes organization method.
* @param introduction introduction
* @param characters characters to join.
* @return the newly created scene.
*/
public Scene startScene(Context context, String introduction, Data.Character... chars) {
final List<Data.Character> characters = new ArrayList<Data.Character>(Arrays.asList(chars));
final Key campaignKey = characters.get(0).getKey().getParent();
final Set<Key> oldseqsKeys = new LinkedHashSet<Key>();
final Map<Key, List<Data.Character>> charactersByScene =
new HashMap<Key, List<Data.Character>>();
// check characters validity
for(Data.Character character : characters) {
final Key characterKey = character.getKey();
if(character == null || !characterKey.getParent().equals(campaignKey)) {
logger.severe("startScene() : not of the same campaign : " + characterKey);
return null;
}
if(character.getOwner() != null) { // !NPC
final Key sceneKey = character.getScene();
if(sceneKey != null) {
// keep trace of distinct old scenes
oldseqsKeys.add(sceneKey);
// sort characters by old scene.
List<Data.Character> seqChars =
charactersByScene.get(sceneKey);
if(seqChars == null ){
seqChars = new ArrayList<Data.Character>();
charactersByScene.put(sceneKey, seqChars);
}
seqChars.add(character);
}
}
}
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("startScene() : campaign not found :" + campaignKey);
context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return null;
}
final List<Scene> oldseqs = dao.readScenes(context, oldseqsKeys);
// Start a TX, all entities remains to Campaign
final Transaction tx = dao.getStore().beginTransaction();
try {
if(oldseqs != null) {
for(Scene oldseq : oldseqs) {
// get characters where current scene is oldseq
final List<Data.Character> others = getSceneCharacters(context, oldseq.getKey());
// Remove the characters for the new scene.
dao.removeAll(others, characters);
// Keep only playable characters
final List<Data.Character> otherPCs = removeNPCs(others);
if(otherPCs != null && !otherPCs.isEmpty()) {
// ... create a new alternative scene for "the others" ...
final Scene newseq = new Scene(campaign);
newseq.setDate(new Date());
newseq.setCharacters(dao.toKeysFromData(others)); // includes NPC
// ... link it to the odlseq for each PC ...
for(Data.Character otherPC : otherPCs) {
final String index = String.valueOf(otherPC.getKey().getId());
newseq.setPrevious(index, oldseq.getKey());
}
dao.save(context, newseq);
logger.info(toStringKeys(others) + " go to " + newseq.getKey() + " alternative scene");
// ... and link the oldseq to this new alternative scene
// In 2 steps because newseq.key was not set before
for(Data.Character otherPC : otherPCs) {
final String index = String.valueOf(otherPC.getKey().getId());
oldseq.setNext(index, newseq.getKey());
// save the new alternative scene as current "other characters"'s scene
otherPC.setScene(newseq);
dao.save(context, otherPC);
}
} else {
logger.info("there was no others playable characters");
}
}
}
// create the new scene with characters and link to old scenes.
final Scene newseq = new Scene(campaign);
newseq.setDate(new Date());
newseq.setIntroduction(introduction);
newseq.setCharacters(dao.toKeysFromData(characters)); // includes NPCs
for(Data.Character character : characters) {
if(character.getOwner() != null) { // !NPC
final String index = String.valueOf(character.getKey().getId());
newseq.setPrevious(index, character.getScene());
}
}
dao.save(context, newseq);
// create the forward link from old scenes to the new scene.
if(oldseqs != null) {
for(Scene oldseq : oldseqs) {
final List<Data.Character> oldseqChars =
charactersByScene.get(oldseq.getKey());
if(oldseqChars != null) {
for(Data.Character oldseqChar : oldseqChars) {
// here we iterate only on PC associated to this old scene
final String index = String.valueOf(oldseqChar.getKey().getId());
oldseq.setNext(index, newseq.getKey());
}
}
// useless but ... for the future ?
oldseq.setClosed(true);
dao.save(context, oldseq);
}
}
// save the new scene as current characters's scene
for(Data.Character character : characters) {
if(character.getOwner() != null) { // !NPC
character.setScene(newseq);
dao.save(context, character);
}
}
tx.commit();
logger.info("scene " + newseq.getKey() + " created for " + toStringKeys(characters));
return newseq;
} finally {
if(tx.isActive()) {
tx.rollback();
}
}
}
/**
* Starts a scene for the specified characters.
* TODO : utility, replace with a complete scenes organization method.
* @param introduction Introduction text of the the scene.
* @param charactersKeys characters associated with the new scene.
* @return the new scene.
*/
public Scene startScene(Context context, String introduction, Key... charactersKeys) {
if(charactersKeys == null || charactersKeys.length == 0) {
logger.severe("startScene() : no characters");
context.addError(Errors.REQUIRED, "characters");
return null;
}
final List<Data.Character> characters =
dao.readCharacters(context, Arrays.asList(charactersKeys));
if(characters == null || characters.isEmpty()) {
logger.severe("startScene() : characters not found");
context.addError(Errors.NOT_FOUND_CHARACTER);
return null;
}
return startScene(context, introduction,
characters.toArray(new Data.Character[characters.size()]));
}
public boolean startScenes(Context context, long campaignId, SceneToCreateSDO[] scenes) {
if(scenes == null || scenes.length == 0) {
logger.severe("no scene");
context.addError(Errors.REQUIRED, "scenes");
return false;
}
final Key campaignKey = Campaign.createKey(campaignId);
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Campaign not found " + campaignKey);
context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return false;
}
// Create a list of characters and pcs
final Map<Key, Data.Character> pcs = new HashMap<Key, Data.Character>();
final Map<Key, Data.Character> allCharacters = new HashMap<Key, Data.Character>();
final Map<SceneToCreateSDO, List<Data.Character>> scenesCharacters =
new HashMap<SceneToCreateSDO, List<Data.Character>>();
final Map<SceneToCreateSDO, List<Key>> scenesCharactersKeys =
new HashMap<SceneToCreateSDO, List<Key>>();
for(SceneToCreateSDO scene : scenes) {
final long[] charactersId = scene.getCharacters();
if(charactersId == null || charactersId.length == 0) {
logger.severe("no characters");
context.addError(Errors.REQUIRED, "characters");
return false;
}
// count the PCS
int scenePCs = 0;
final List<Data.Character> sceneCharacters = new ArrayList<Data.Character>();
final List<Key> sceneCharactersKeys = new ArrayList<Key>();
// load and filter the characters
for(long characterId : charactersId) {
final Key characterKey =
Data.Character.createKey(campaignKey, characterId);
// check the character is already found as PC in another scene
if(pcs.containsKey(characterKey)) {
logger.severe("character " + characterKey + " in multiple scenes");
context.addError(Errors.IMPOSSIBLE_UBIQUITY, characterKey);
return false;
}
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
if(character.getOwner() != null) {
pcs.put(character.getKey(), character);
scenePCs++;
}
allCharacters.put(character.getKey(), character);
sceneCharacters.add(character);
sceneCharactersKeys.add(characterKey);
}
// just to get them easily after
scenesCharacters.put(scene, sceneCharacters);
scenesCharactersKeys.put(scene, sceneCharactersKeys);
if(scenePCs == 0) {
logger.severe("try to create a scene with only NPCs");
context.addError(Errors.IMPOSSIBLE_ONLY_NPCS);
return false;
}
}
// a way to remember the PCs last scene
final Map<Key, Scene> lastScenes = new HashMap<Key, Scene>();
// Create a list of PCs's current scenes
final List<Scene> currentScenes = new ArrayList<Scene>();
for(Data.Character pc : pcs.values()) {
final Key sceneKey = pc.getScene();
if(sceneKey != null) {
final Scene currentScene = dao.readScene(context, sceneKey);
if(currentScene == null) {
logger.severe("invalid scene " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return false;
}
lastScenes.put(pc.getKey(), currentScene);
if(!currentScenes.contains(currentScene)) {
currentScenes.add(currentScene);
// check the scene doesn't contains another PC not listed
if(currentScene.getCharacters() != null) {
for(Key characterKey : currentScene.getCharacters()) {
if(!allCharacters.containsKey(characterKey)) {
final Data.Character character =
dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("invalid character " + characterKey);
} else {
if(character.getOwner() != null) {
logger.severe("try to create a scene but "
+ characterKey + " not dispatched");
context.addError(Errors.IMPOSSIBLE_PC_NOT_DISPATCHED,
characterKey);
return false;
}
}
}
}
}
}
}
}
// At this points all PCs of impacted scenes are listed and used
// only one time, we are sure ... so create the new scenes
// Start a TX, all entities remains to Campaign
final Transaction tx = dao.getStore().beginTransaction();
try {
for(SceneToCreateSDO sceneToCreate : scenes) {
final List<Key> charactersKeys = scenesCharactersKeys.get(sceneToCreate);
final List<Data.Character> characters = scenesCharacters.get(sceneToCreate);
final Scene scene = new Scene(campaign);
scene.setDate(new Date());
scene.setCharacters(charactersKeys);
scene.setIntroduction(sceneToCreate.getIntroduction());
final Map<Long, Map<Long, String>> allAliases = sceneToCreate.getAliases();
for(Data.Character character : characters) {
final Key characterKey = character.getKey();
// set the aliases
if(allAliases != null) {
final Map<Long, String> charAliases = allAliases.get(characterKey.getId());
if(charAliases != null) {
for(Map.Entry<Long, String> entry : charAliases.entrySet()) {
final Long charId = entry.getKey();
final String alias = entry.getValue();
if(charId == null) {
// global alias
scene.setAlias(String.valueOf(characterKey.getId()), alias);
} else {
// specific alias
scene.setAlias(String.valueOf(characterKey.getId())
+ "-" + String.valueOf(charId), alias);
}
}
}
}
}
// set the "previous" property of the new scene, for each PC
for(Data.Character character : characters) {
if(character.getOwner() != null) {
final Key characterKey = character.getKey();
final Scene lastScene = lastScenes.get(characterKey);
if(lastScene != null) {
scene.setPrevious(String.valueOf(characterKey.getId()), lastScene.getKey());
}
}
}
// after that we have a scene key
dao.save(context, scene);
// update the PCs and the last scene
final List<Scene> updatedScenes = new ArrayList<Scene>();
for(Data.Character character : characters) {
if(character.getOwner() != null) {
// set the PC current scene
character.setScene(scene.getKey());
dao.save(context, character);
// set the last scene's "next" scene.
final Key characterKey = character.getKey();
final Scene lastScene = lastScenes.get(characterKey);
if(lastScene != null) {
lastScene.setNext(String.valueOf(characterKey.getId()), scene.getKey());
if(!updatedScenes.contains(lastScene)) {
updatedScenes.add(lastScene);
}
}
}
}
// done after to update each scene only one time
for(Scene updatedScene : updatedScenes) {
dao.save(context, updatedScene);
}
}
tx.commit();
} finally {
if(tx.isActive()) {
tx.rollback();
}
}
return true;
}
/**
* Post a Speech Message.
* @param sceneKey scene containing the message.
* @param character character who posts the speech.
* @param text text of the speech.
* @return true if ok.
*/
public boolean postSpeech(Context context, Key sceneKey, Data.Character character, String text) {
if(character.getOwner() != null) { // !NPC
// NPC can post everywhere ... GM is a god !
// But sceneKey must be specified when NPCs post.
sceneKey = character.getScene();
}
if(sceneKey == null) {
return false;
}
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
return false;
}
// FIXME should iterate to deal with concurrent updates of scene
final Message message = new Message(scene);
message.setAuthor(character);
message.setContent(text);
message.setType(MessageType.ACTION.getCode());
message.setAction(MessageAction.SPEECH.getCode());
createMessage(context, scene, message);
return true;
}
/**
* To post an action message by character.
*
* @param context execution context.
* @param campaignId campaign's id.
* @param characterId character's id.
* @param clientSceneId scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param action action.
* @param content message content.
* @return <code>true</code> if ok.
*/
public boolean playerPostAction(Context context,
long campaignId, long characterId, long clientSceneId,
Date clientSceneTimestamp, MessageAction action, String content) {
return playerPostAction(context,
createCharacterKey(campaignId, characterId),
createSceneKey(campaignId, clientSceneId),
clientSceneTimestamp, action, content);
}
/**
* To post an action message by character.
*
* @param context execution context.
* @param characterKey character's key.
* @param clientSceneKey scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param action action.
* @param content message content.
* @return <code>true</code> if ok.
*/
public boolean playerPostAction(Context context,
Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
MessageAction action, String content) {
if(action == null || characterKey == null
|| content == null || clientSceneKey == null
|| clientSceneTimestamp == null) {
logger.severe("missing param");
context.addError(Errors.REQUIRED);
return false;
}
// check the character.
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// check character's lock
if(Boolean.TRUE.equals(character.getLocked())) {
logger.severe("Character " + characterKey + " is locked");
context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
return false;
}
// check character is not dead
if(Boolean.TRUE.equals(character.getDead())) {
logger.severe("Character " + characterKey + " is dead");
context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
return false;
}
// check user rights
if(!isOwner(context, character)) {
logger.severe(context.getUser().getKey()
+ " cannot post for " + characterKey);
context.addError(Errors.USER_USURPATION_PC);
return false;
}
// Get the scene.
final Key sceneKey = character.getScene();
if(sceneKey == null) {
logger.severe(characterKey + " cannot post, it has no current scene");
context.addError(Errors.IMPOSSIBLE);
return false;
}
// Check the client is up-to-date
if(!sceneKey.equals(clientSceneKey)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// check the scene
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("invalid scene " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return false;
}
// check the scene is not closed
if(Boolean.TRUE.equals(scene.getClosed())) {
logger.severe("Scene " + sceneKey + " is closed");
context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
return false;
}
// check the scene is not paused
if(Boolean.TRUE.equals(scene.getPaused())) {
logger.severe("Scene " + sceneKey + " is paused");
context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
return false;
}
// Check the client is up-to-date
if(scene.getTimestamp().after(clientSceneTimestamp)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// FIXME should iterate to deal with concurrent updates of scene
final Message message = new Message(scene);
message.setAuthor(character);
message.setContent(content);
message.setType(MessageType.ACTION.getCode());
message.setAction(action.getCode());
createMessage(context, scene, message);
return true;
}
/**
* To post an action message by character.
*
* @param context execution context.
* @param campaignId campaign's id.
* @param characterId character's id.
* @param clientSceneId scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param dices the dices to roll, a Map<Sides, NumberOfDices>.
* @param content message content.
* @return <code>true</code> if ok.
*/
public boolean playerRollDices(Context context,
long campaignId, long characterId,
long clientSceneId, Date clientSceneTimestamp,
Map<Integer, Integer> dices, String content) {
return playerRollDices(context,
createCharacterKey(campaignId, characterId),
createSceneKey(campaignId, clientSceneId),
clientSceneTimestamp, dices, content);
}
/**
* To post an action message by character.
*
* @param context execution context.
* @param characterKey character's key.
* @param clientSceneKey scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param dices the dices to roll, a Map<Sides, NumberOfDices>.
* @param content message content.
* @return <code>true</code> if ok.
*/
public boolean playerRollDices(Context context,
Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
Map<Integer, Integer> dices, String content) {
if(dices == null || dices.isEmpty()
|| dices.containsKey(null) || dices.containsValue(null)
|| characterKey == null || content == null
|| clientSceneKey == null || clientSceneTimestamp == null) {
logger.severe("missing param");
context.addError(Errors.REQUIRED);
return false;
}
for(Map.Entry<Integer, Integer> entry : dices.entrySet()) {
int sides = entry.getKey().intValue();
if(sides <= 0) {
logger.severe("invalid dice sides : " + sides);
context.addError(Errors.ARGUMENT, "dice sides", sides);
return false;
}
int number = entry.getValue().intValue();
if(number <= 0 || number > 100) {
logger.severe("invalid dice number : " + number);
context.addError(Errors.ARGUMENT, "dice number", number);
return false;
}
}
// check the character.
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// check character's lock
if(Boolean.TRUE.equals(character.getLocked())) {
logger.severe("Character " + characterKey + " is locked");
context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
return false;
}
// check character is not dead
if(Boolean.TRUE.equals(character.getDead())) {
logger.severe("Character " + characterKey + " is dead");
context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
return false;
}
// check user rights
if(!isOwner(context, character)) {
logger.severe(context.getUser().getKey()
+ " cannot post for " + characterKey);
context.addError(Errors.USER_USURPATION_PC);
return false;
}
// Get the scene.
final Key sceneKey = character.getScene();
if(sceneKey == null) {
logger.severe(characterKey + " cannot post, it has no current scene");
context.addError(Errors.IMPOSSIBLE);
return false;
}
// Check the client is up-to-date
if(!sceneKey.equals(clientSceneKey)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// check the scene
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("invalid scene " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return false;
}
// check the scene is not closed
if(Boolean.TRUE.equals(scene.getClosed())) {
logger.severe("Scene " + sceneKey + " is closed");
context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
return false;
}
// check the scene is not paused
if(Boolean.TRUE.equals(scene.getPaused())) {
logger.severe("Scene " + sceneKey + " is paused");
context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
return false;
}
// Check the client is up-to-date
if(scene.getTimestamp().after(clientSceneTimestamp)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// roll the dices.
final Random random = new Random();
final List<String> dicesRolled = new ArrayList<String>();
for(Map.Entry<Integer, Integer> entry : dices.entrySet()) {
int sides = entry.getKey().intValue();
int number = entry.getValue().intValue();
for(int n = 0; n < number; n++) {
int dice = random.nextInt(sides) + 1;
dicesRolled.add(dice + "/" + sides);
}
}
// FIXME should iterate to deal with concurrent updates of scene
final Message message = new Message(scene);
message.setAuthor(character);
message.setContent(content);
message.setType(MessageType.DICEROLL.getCode());
message.setAction(null);
message.setDices(dicesRolled);
createMessage(context, scene, message);
return true;
}
/**
* Post a Speech Message.
* @param sceneKey scene containing the message.
* @param characterKey character who posts the speech.
* @param text text of the speech.
* @return true if ok.
*/
public boolean postSpeech(Context context, Key sceneKey, Key characterKey, String text) {
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
return false;
}
return postSpeech(context, sceneKey, character, text);
}
/**
* Utility method to create a message in a scene.
* @param scene scene receiving the message.
* @param message message to add.
*/
private void createMessage(Context context, Scene scene, Message message) {
message.setDate(new Date());
final Key lastMessageKey = scene.getLastMessage();
message.setPrevious(lastMessageKey);
long index = getNewMessageIndex(null);
Message lastMessage = null;
if(lastMessageKey != null) {
lastMessage = dao.readMessage(context, lastMessageKey);
if(lastMessage != null) {
if(lastMessage.getIndex() != null) {
index = getNewMessageIndex(lastMessage.getIndex());
}
lastMessage.setNext(message.getKey());
}
}
message.setIndex(index);
dao.save(context, message);
if(lastMessage != null) {
// donne here to get the 'message' key after the save operation
dao.save(context, lastMessage);
}
if(scene.getFirstMessage() == null) {
scene.setFirstMessage(message.getKey());
}
scene.setLastMessage(message.getKey());
dao.save(context, scene);
}
private long getNewMessageIndex(Long index) {
return index == null ? 50 : (index + 50);
}
/**
* Method to post an OFF message to a scene.
* PC can only post to their current scene.
* NPC cannot post OFF.
* NPC cannot receive OFF.
* PC can only post OFF to GM (GM see all OFF).
*
* @param sceneKey scene to attach to message.
* @param authorKey author of the message or null if GM.
* @param toKey recipient of the message when GM.
* @param text text of the OFF message.
* @return true if ok.
*/
public boolean postOFF(Context context, Key sceneKey, Key authorKey,
Key toKey, String text) {
Data.Character to = null;
if(toKey != null) {
to = dao.readCharacter(context, toKey);
if(to == null) {
return false;
}
// Cannot post OFF to NPC
if(to.getOwner() == null) {
toKey = null;
}
}
if(authorKey == null) { // GM
if(sceneKey == null) {
if(to == null) {
return false;
}
// use the recipient current scene.
sceneKey = to.getScene();
if(sceneKey == null) {
return false;
}
}
// GM cannot post OFF as a PC
authorKey = null;
} else {
final Data.Character author = dao.readCharacter(context, authorKey);
if(author == null) {
return false;
}
// NPC cannot post OFF messages.
if(author.getOwner() == null) {
return false;
}
// PC can only post on current scene.
sceneKey = author.getScene();
// PC can only post to GM.
toKey = null;
}
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
return false;
}
final Message message = new Message(scene);
message.setAuthor(authorKey);
if(toKey != null) {
message.getNonNullTo().add(toKey);
}
message.setContent(text);
message.setType(MessageType.OFF.getCode());
createMessage(context, scene, message);
return true;
}
/**
* Number of messages accepted for pagination.
* Note that scenes are always complete, it's an approximative max.
*/
public final static int PAGINATION_MESSAGES = 25;
/**
* Gives the last messages of the campaign for a character.
* @param character for wich get the last messages. Must be one of the current user characters.
* @return the last messages of the characters. scenes are limited but complete.
*/
public List<SceneSDO> getMessages(
Context context, long campaignId, long characterId,
Long lastSceneId, Date timestamp
) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return null;
}
// Check user is character's owner.
final User user = context.getUser();
if(!isOwner(context, character)) {
context.addError(Errors.USER_USURPATION);
logger.severe("User " + user.getKey()
+ " cannot read messages of " + characterKey);
return null;
}
// the result
final List<SceneSDO> scenes = new ArrayList<SceneSDO>();
if(timestamp != null) {
final Map<Key, SceneSDO> mapScenes = new HashMap<Key, SceneSDO>();
// Load the scenes wich are newer (or modified after) than timestamp
final List<Scene> scenesDB =
dao.asListOfScenes(context, dao.addSceneFilterOnTimestamp(
dao.queryScene(campaignKey), FilterOperator.GREATER_THAN, timestamp), null);
// register the scenes.
if(scenesDB != null) {
for(Scene sceneDB : scenesDB) {
final SceneSDO sceneSDO = readSceneSDO(context, sceneDB, null, character);
mapScenes.put(sceneDB.getKey(), sceneSDO);
}
}
// search new messages.
final List<Message> messages =
dao.asListOfMessages(context, dao.addMessageFilterOnTimestamp(
dao.queryMessage(campaignKey), FilterOperator.GREATER_THAN, timestamp), null);
// keep only visible messages.
keepOnlyVisibleMessages(messages, character.getKey(), true);
// dispatch messages on scenes
if(messages != null) {
for(Message message : messages) {
final Key sceneKey = message.getKey().getParent();
SceneSDO sceneSDO = mapScenes.get(sceneKey);
if(sceneSDO == null) {
// scene was not already loaded, so ...
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("Invalid scene " + sceneKey);
continue;
}
sceneSDO = readSceneSDO(context, scene, null, character);
mapScenes.put(sceneKey, sceneSDO);
}
// add the message to the scene.
if(sceneSDO.getMessages() == null) {
sceneSDO.setMessages(new ArrayList<Message>());
}
sceneSDO.getMessages().add(message);
}
}
scenes.addAll(mapScenes.values());
} else {
final String characterIndex = String.valueOf(characterId);
final Key sceneKey;
if(lastSceneId == null) {
// use current scene as start
sceneKey = character.getScene();
} else {
// get the "last scene"'s previous scene
final Key lastSceneKey = Scene.createKey(campaignKey, lastSceneId);
final Scene lastScene = dao.readScene(context, lastSceneKey);
if(lastScene == null) {
logger.severe("Invalid scene " + lastSceneKey);
context.addError(Errors.NOT_FOUND_SCENE, lastSceneKey);
return null;
}
if(!lastScene.getNonNullCharacters().contains(characterKey)) {
logger.severe("LastScene " + lastSceneKey
+ " is not associated with " + characterKey);
context.addError(Errors.IMPOSSIBLE);
return null;
}
// get the previous scene "for the current character"
sceneKey = lastScene.getPrevious(characterIndex);
}
if(sceneKey == null) {
logger.severe("cannot find a starting scene for " + characterKey);
context.addError(Errors.IMPOSSIBLE);
return null;
}
// Load the scene.
Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("Invalid scene " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return null;
}
// Iterate over the scenes to reach (if possible), 25 messages
int count = 0;
while(scene != null && count < PAGINATION_MESSAGES) {
// select all the messages of the scene, always, and order them.
final List<Message> messages = order(dao.asListOfMessages(
context, dao.queryMessage(scene.getKey()), null), scene.getFirstMessage());
// keep only visible messages
keepOnlyVisibleMessages(messages, characterKey, false);
// note : even scenes with 0 messages must be returned because off the intro !
final SceneSDO sceneSDO = readSceneSDO(context, scene, messages, character);
scenes.add(0, sceneSDO);
if(messages != null) {
count += messages.size();
}
if(count < PAGINATION_MESSAGES) {
final Key previousSceneKey = scene.getPrevious(characterIndex);
scene = previousSceneKey == null ? null : dao.readScene(context, previousSceneKey);
}
}
}
return scenes.isEmpty() ? null : scenes;
}
/**
* Utility method to not completely load a SceneSDO.
*
* @param context execution context.
* @param scene scene to use to build the SDO.
* @param byCharacter character to view the scene.
* @return the {@link SceneSDO}.
*/
private SceneSDO readSceneSDO(Context context, Scene scene,
List<Message> messages, Data.Character character) {
final SceneSDO sceneSDO = new SceneSDO();
sceneSDO.setScene(scene);
// Note : alias will be used after
sceneSDO.setCharacters(dao.readCharacters(context, scene.getCharacters()));
// keep only visible messages
if(messages != null) {
if(character != null) {
keepOnlyVisibleMessages(messages, character.getKey(), false);
}
sceneSDO.setMessages(messages);
}
return sceneSDO;
}
private void keepOnlyVisibleMessages(List<Message> messages, Key characterKey, boolean keepDeleted) {
if(messages == null) {
return;
}
final Iterator<Message> imessages = messages.iterator();
while(imessages.hasNext()) {
final Message message = imessages.next();
if(message != null) {
if(!keepDeleted && Boolean.TRUE.equals(message.getDeleted())) {
imessages.remove();
} else if(MessageType.ACTION.getCode().equals(message.getType())) {
// all actions of everyone are always seen
} else if (MessageType.OFF.getCode().equals(message.getType())) {
// player cans see OFF of GM (to him or public) and its own
if(!(characterKey.equals(message.getAuthor())
|| isPublicGMOFF(message)
|| message.getNonNullTo().contains(characterKey))) {
imessages.remove();
}
} else if (MessageType.DICEROLL.getCode().equals(message.getType())) {
// character can see its own dices, that's all.
if(!characterKey.equals(message.getAuthor())) {
imessages.remove();
}
}
} else {
imessages.remove();
}
}
}
private boolean isPublicOFF(Message message) {
return message.getTo() == null || message.getTo().isEmpty();
}
private boolean isPublicGMOFF(Message message) {
return isPublicOFF(message) && message.getAuthor() == null;
}
/**
* FIXME use cache
*/
public List<Data.Character> getSceneCharacters(Context context, Key sceneKey) {
// query on campaign
final Query query = dao.queryCharacter(sceneKey.getParent());
// filter on scene
dao.addCharacterFilterOnScene(query, FilterOperator.EQUAL, sceneKey);
// no cursor ...
return dao.asListOfCharacters(context, query, null);
}
/**
* To post an action message by character.
*
* @param context execution context.
* @param campaignId campaign's id.
* @param characterId character's id.
* @param clientSceneId scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param content OFF content.
* @return <code>true</code> if ok.
*/
public boolean playerPostOFF(Context context,
long campaignId, long characterId, long clientSceneId,
Date clientSceneTimestamp, String content) {
return playerPostOFF(context,
createCharacterKey(campaignId, characterId),
createSceneKey(campaignId, clientSceneId),
clientSceneTimestamp, content);
}
/**
* To post an OFF message by character.
*
* @param context execution context.
* @param characterKey character's key.
* @param clientSceneKey scene where post message, used to check client is up to date.
* @param clientSceneTimestamp just to check if the client is up to date.
* @param content message content.
* @return <code>true</code> if ok.
*/
public boolean playerPostOFF(Context context,
Key characterKey, Key clientSceneKey, Date clientSceneTimestamp,
String content) {
if(characterKey == null || content == null
|| clientSceneKey == null || clientSceneTimestamp == null) {
logger.severe("missing param");
context.addError(Errors.REQUIRED);
return false;
}
// check the character.
final Data.Character character = dao.readCharacter(context, characterKey);
if(character == null) {
logger.severe("invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// check character's lock
if(Boolean.TRUE.equals(character.getLocked())) {
logger.severe("Character " + characterKey + " is locked");
context.addError(Errors.IMPOSSIBLE_LOCKED, characterKey);
return false;
}
// check character is not dead
if(Boolean.TRUE.equals(character.getDead())) {
logger.severe("Character " + characterKey + " is dead");
context.addError(Errors.IMPOSSIBLE_DEAD, characterKey);
return false;
}
// check user rights
if(!isOwner(context, character)) {
logger.severe(context.getUser().getKey()
+ " cannot post for " + characterKey);
context.addError(Errors.USER_USURPATION_PC);
return false;
}
// Get the scene.
final Key sceneKey = character.getScene();
if(sceneKey == null) {
logger.severe(characterKey + " cannot post, it has no current scene");
context.addError(Errors.IMPOSSIBLE);
return false;
}
// Check the client is up-to-date
if(!sceneKey.equals(clientSceneKey)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// check the scene
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("invalid scene " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return false;
}
// check the scene is not closed
if(Boolean.TRUE.equals(scene.getClosed())) {
logger.severe("Scene " + sceneKey + " is closed");
context.addError(Errors.IMPOSSIBLE_CLOSED, sceneKey);
return false;
}
// check the scene is not paused
if(Boolean.TRUE.equals(scene.getPaused())) {
logger.severe("Scene " + sceneKey + " is paused");
context.addError(Errors.IMPOSSIBLE_PAUSED, sceneKey);
return false;
}
// Check the client is up-to-date
if(scene.getTimestamp().after(clientSceneTimestamp)) {
logger.severe("client is out-of-date");
context.addError(Errors.OUTOFDATE);
return false;
}
// FIXME should iterate to deal with concurrent updates of scene
final Message message = new Message(scene);
message.setAuthor(character);
message.setContent(content);
message.setType(MessageType.OFF.getCode());
message.setAction(null);
createMessage(context, scene, message);
return true;
}
/**
* Orders a list of messages.
*
* @param messages messages to order
* @param firstKey known first element (see Scene#firstMessage)
* @return messages ordered.
*/
private List<Message> order(Collection<Message> messages, Key firstKey) {
if(messages == null || messages.isEmpty()) {
return null;
}
final Map<Key, Message> map = Data.map(messages);
final List<Message> ordered = new ArrayList<Message>();
Message previous = map.get(firstKey);
while(previous != null) {
ordered.add(previous);
previous = map.get(previous.getNext());
}
return ordered.isEmpty() ? null : ordered;
}
/**
*
* @param campaignId
* @param sceneId
* @param messageId
* @return
*/
public boolean deleteMessage(Context context, long campaignId, long sceneId, long messageId) {
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(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return false;
}
final Key sceneKey = Scene.createKey(campaignKey, sceneId);
final Key messageKey = Message.createKey(sceneKey, messageId);
if(!isGameMaster(context, campaign)) {
logger.severe("User " + context.getUser().getKey()
+ " cannot delete " + messageKey);
context.addError(Errors.USER_USURPATION_GM);
return false;
}
final Message message = dao.readMessage(context, messageKey);
if(message == null) {
logger.severe("Message not found " + messageKey);
context.addError(Errors.NOT_FOUND_MESSAGE, messageKey);
return false;
}
if(Boolean.TRUE.equals(message.getDeleted())) {
logger.warning("Message already deleted " + messageKey);
return true;
}
message.setDeleted(Boolean.TRUE);
dao.save(context, message);
logger.info("message " + messageKey + " deleted");
return true;
}
public boolean justForTestsMakeThemTalkRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
return false;
}
final Key sceneKey = character.getScene();
final Scene scene = dao.readScene(context, sceneKey);
// random a number of messages to write.
final Random random = new Random();
final int countMessages = random.nextInt(4);
logger.info("random " + countMessages + " to write randomly");
for(int n = 0; n < countMessages; n++) {
final List<Key> characters = new ArrayList<Key>(scene.getNonNullCharacters());
// Remove current character.
while(characters.remove(characterKey));
if(characters.isEmpty()) {
return false;
}
final Key speekerKey = characters.get(random.nextInt(characters.size()));
postSpeech(context, sceneKey, speekerKey, "This a randomly generated message from " + speekerKey);
}
return true;
}
public boolean justForTestsMakeThemRollDicesRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
return false;
}
final Key sceneKey = character.getScene();
final Scene scene = dao.readScene(context, sceneKey);
// random a number of messages to write.
final Random random = new Random();
final List<Key> charactersKeys = new ArrayList<Key>(scene.getNonNullCharacters());
// Remove current character.
while(charactersKeys.remove(characterKey));
if(charactersKeys.isEmpty()) {
return false;
}
List<Data.Character> characters = dao.read(context, Data.Character.class, charactersKeys);
characters = removeNPCs(characters);
if(characters.isEmpty()) {
return false;
}
final Data.Character roller = characters.get(random.nextInt(characters.size()));
final User rollerOwner = dao.readUser(context, roller.getOwner());
if(rollerOwner == null) {
logger.severe("Owner not found " + roller.getOwner());
return false;
}
final Map<Integer, Integer> dices = new HashMap<Integer, Integer>();
final int types = random.nextInt(4) + 1;
logger.info("Roll " + types + " types of dices");
for(int n = 0; n < types; n++) {
boolean ok = false;
while(!ok) {
final int sides = random.nextInt(100) + 1;
if(!dices.containsKey(sides)) {
final int number = random.nextInt(4) + 1;
logger.info("Roll " + number + " dices of " + sides + " sides");
dices.put(sides, number);
ok = true;
}
}
}
return playerRollDices(new Context(context, rollerOwner), roller.getKey(),
sceneKey, scene.getTimestamp(),
dices, "A random dice roll !");
}
public boolean justForTestsCreateSequenceRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
// Get all playable characters.
final List<Data.Character> characters = dao.getCampaignCharacters(context, campaignKey);
if(characters == null || characters.isEmpty()) {
return false;
}
// Remove current character.
Data.Character character = null;
final Iterator<Data.Character> icharacters = characters.iterator();
while(icharacters.hasNext()) {
final Data.Character next = icharacters.next();
if(next.getKey().getId() == characterId) {
character = next;
icharacters.remove();
}
}
if(characters.isEmpty() || character == null) {
return false;
}
// Randomly remove guys to create a new scene with the remaining
final Random random = new Random();
final int countToRemove = random.nextInt(characters.size());
for(int n = 0; n < countToRemove; n++) {
characters.remove(random.nextInt(characters.size()));
}
characters.add(character);
// Start the new scene ... and creates automatically the others ...
final String introduction = "This a randomly generated scene, have fun ...";
startScene(context, introduction, characters.toArray(new Data.Character[characters.size()]));
return true;
}
public boolean justForTestsDeleteMessageRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// choose a number of scene to fetch before deleting a message
final Random random = new Random();
final int countScenes = random.nextInt(5);
Key sceneKey = character.getScene();
Scene scene = null;
for(int n = 0; n < countScenes; n++) {
if(sceneKey != null) {
scene = dao.readScene(context, sceneKey);
sceneKey = scene.getPrevious(String.valueOf(characterId));
}
}
if(scene == null) {
logger.severe("was not able to find a scene where delete a message");
context.addError(Errors.IMPOSSIBLE);
return false;
}
sceneKey = scene.getKey();
final List<Message> messages = dao.getSceneMessages(context, sceneKey);
if(messages == null || messages.isEmpty()) {
logger.severe("no messages in scene " + sceneKey);
context.addError(Errors.IMPOSSIBLE);
return false;
}
// choose randomly a message to delete
final long messageId = messages.get(random.nextInt(messages.size())).getKey().getId();
// check the campaign
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Campaign not found " + campaignKey);
context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return false;
}
// may be 100% of test cases
if(!isGameMaster(context, campaign)) {
// then we cheat ...
final Key masterKey = campaign.getMaster();
final User master = dao.readUser(context, masterKey);
if(master == null){
logger.severe("Master not found " + masterKey);
context.addError(Errors.NOT_FOUND_USER, masterKey);
return false;
}
context = new Context(context, master);
}
return deleteMessage(context, campaignId, sceneKey.getId(), messageId);
}
public boolean justForTestsReindexRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// choose a number of scene to fetch before deleting a message
final Random random = new Random();
final int countScenes = random.nextInt(5);
Key sceneKey = character.getScene();
Scene scene = null;
for(int n = 0; n < countScenes; n++) {
if(sceneKey != null) {
scene = dao.readScene(context, sceneKey);
sceneKey = scene.getPrevious(String.valueOf(characterId));
}
}
if(scene == null) {
logger.severe("was not able to find a scene where reindex messages");
context.addError(Errors.IMPOSSIBLE);
return false;
}
sceneKey = scene.getKey();
final List<Message> messages = dao.getSceneMessages(context, sceneKey);
if(messages == null || messages.size() < 2) {
logger.severe("no enough messages in scene " + sceneKey);
context.addError(Errors.IMPOSSIBLE);
return false;
}
// How messages will be reindexed
final int number = random.nextInt(messages.size()) + 1;
final int max = (messages.size() + 2) * 50;
final List<Message> messages2 = new ArrayList<Message>(messages);
for(int n = 0; n < number; n++) {
final Message message = messages2.get(random.nextInt(messages2.size()));
// Random a none used index
long index = random.nextInt(max);
boolean ok = false;
rerandom: while(!ok) {
ok = true;
for(Message m : messages) {
if(m.getIndex() == index) {
ok = false;
index = random.nextInt(max);
continue rerandom;
}
}
}
logger.info("reindex " + message.getKey() + " from " + message.getIndex() + " to " + index);
message.setIndex(index);
dao.save(context, message);
messages2.remove(message);
}
return true;
}
public boolean justForTestsInsertOFFRandomly(Context context, long campaignId, long characterId) {
final Key campaignKey = Data.Campaign.createKey(campaignId);
final Key characterKey = Data.Character.createKey(campaignKey, characterId);
final Data.Character character = dao.readCharacter(context, characterKey);
// Check the character exists.
if(character == null) {
logger.severe("Invalid character " + characterKey);
context.addError(Errors.NOT_FOUND_CHARACTER, characterKey);
return false;
}
// choose a number of scene to fetch before deleting a message
final Random random = new Random();
final int countScenes = random.nextInt(5);
Key sceneKey = character.getScene();
Scene scene = null;
for(int n = 0; n < countScenes; n++) {
if(sceneKey != null) {
scene = dao.readScene(context, sceneKey);
sceneKey = scene.getPrevious(String.valueOf(characterId));
}
}
if(scene == null) {
logger.severe("was not able to find a scene where insert a message");
context.addError(Errors.IMPOSSIBLE);
return false;
}
sceneKey = scene.getKey();
final List<Message> messages = dao.getSceneMessages(context, sceneKey);
if(messages == null || messages.isEmpty()) {
logger.severe("no messages in scene " + sceneKey);
context.addError(Errors.IMPOSSIBLE);
return false;
}
// choose randomly after which insert a new message
final long messageId = messages.get(random.nextInt(messages.size())).getKey().getId();
// check the campaign
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Campaign not found " + campaignKey);
context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return false;
}
// may be 100% of test cases
if(!isGameMaster(context, campaign)) {
// then we cheat ...
final Key masterKey = campaign.getMaster();
final User master = dao.readUser(context, masterKey);
if(master == null){
logger.severe("Master not found " + masterKey);
context.addError(Errors.NOT_FOUND_USER, masterKey);
return false;
}
context = new Context(context, master);
}
return insertMessage(context, campaignId, sceneKey.getId(), messageId,
"This is a generated randomly inserted message");
}
/**
* Temporary method to insert a public OFF.
*/
public boolean insertMessage(
Context context, long campaignId, long sceneId,
long messageBeforeId, String content) {
if(content == null) {
context.addError(Errors.REQUIRED, "content");
return false;
}
// check the campaign
final Key campaignKey = Campaign.createKey(campaignId);
final Campaign campaign = dao.readCampaign(context, campaignKey);
if(campaign == null) {
logger.severe("Campaign not found " + campaignKey);
context.addError(Errors.NOT_FOUND_CAMPAIGN, campaignKey);
return false;
}
// check user rights
if(!isGameMaster(context, campaign)) {
logger.severe("User " + context.getUser().getKey()
+ " cannot insert messages in " + campaignKey);
context.addError(Errors.USER_USURPATION);
return false;
}
// find the message before
final Key sceneKey = Scene.createKey(campaignKey, sceneId);
final Scene scene = dao.readScene(context, sceneKey);
if(scene == null) {
logger.severe("Scene not found " + sceneKey);
context.addError(Errors.NOT_FOUND_SCENE, sceneKey);
return false;
}
final Key messageBeforeKey = Message.createKey(sceneKey, messageBeforeId);
final Message messageBefore = dao.readMessage(context, messageBeforeKey);
if(messageBefore == null) {
logger.severe("Message not found " + messageBeforeKey);
context.addError(Errors.NOT_FOUND_MESSAGE, messageBeforeKey);
return false;
}
final Transaction tx = dao.beginTransaction();
try {
final long newIndex;
// find the message after
final Key messageAfterKey = messageBefore.getNext();
final Message messageAfter;
if(messageAfterKey != null) {
messageAfter = dao.readMessage(context, messageAfterKey);
if(messageAfter == null) {
logger.severe("Message not found " + messageAfterKey);
context.addError(Errors.NOT_FOUND_MESSAGE, messageAfterKey);
return false;
}
newIndex = (messageBefore.getIndex() + messageAfter.getIndex()) / 2;
if(newIndex == messageBefore.getIndex()
|| newIndex == messageAfter.getIndex()) {
logger.severe("No more indexes to insert between "
+ messageBeforeKey + " and " + messageAfterKey);
context.addError(Errors.IMPOSSIBLE, "index");
return false;
}
} else {
newIndex = getNewMessageIndex(messageBefore.getIndex());
messageAfter = null;
}
final Message message = new Message(scene);
message.setDate(new Date());
message.setType(MessageType.OFF.getCode());
message.setPrevious(messageBeforeKey);
message.setContent(content);
message.setIndex(newIndex);
if(messageAfter != null) {
message.setNext(messageAfterKey);
}
dao.save(context, message);
messageBefore.setNext(message);
dao.save(context, messageBefore);
if(messageAfter != null) {
messageAfter.setPrevious(message);
dao.save(context, messageAfter);
} else {
scene.setLastMessage(message);
dao.save(context, scene);
}
tx.commit();
logger.info("Message" + message.getKey() + " inserted after "
+ messageBeforeKey + " and before " + messageAfterKey
+ " with index " + newIndex);
return true;
} finally {
if(tx.isActive()) {
tx.rollback();
}
}
}
}