Package net.cloudcodex.server.service

Source Code of net.cloudcodex.server.service.MessageService

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();
      }
    }
   
  }
 
}
TOP

Related Classes of net.cloudcodex.server.service.MessageService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.