/**
   * Demonstrates how to use the cursor to be able to pass over an complete set of data
   *
   * @param pm Persistence manager instance to use - let open at the end to allow possible object
   *     updates later
   * @param cursorString Representation of a cursor, to be used to get the next set of data to
   *     process
   * @param range Number of data to process at this particular stage
   * @param defaultSource Value to apply to any Demand instance without a default value
   * @return New representation of the cursor, ready for a next process call
   */
  @SuppressWarnings("unchecked")
  public static String updateSource(
      PersistenceManager pm, String cursorString, int range, Source defaultSource) {

    Query query = null;
    try {
      query = pm.newQuery(Demand.class);
      if (cursorString != null) {
        Map<String, Object> extensionMap = new HashMap<String, Object>();
        extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, Cursor.fromWebSafeString(cursorString));
        query.setExtensions(extensionMap);
      }
      query.setRange(0, range);
      List<Demand> results = (List<Demand>) query.execute();
      if (results.iterator().hasNext()) {
        for (Demand demand : results) {
          // Initialize the new field if necessary. By checking first that the field is null, we
          // allow this migration to be safely run multiple times.
          if (demand.getSource() == null) {
            demand.setSource(defaultSource);
            pm.makePersistent(demand);
          }
        }
        cursorString = JDOCursorHelper.getCursor(results).toWebSafeString();
      } else {
        // no results
        cursorString = null;
      }
    } finally {
      query.closeAll();
    }
    return cursorString;
  }
  /**
   * Utility method update a proposal with the given parameters and triggering the associated
   * workflow steps
   *
   * @param pm Persistence manager instance to use - let open at the end to allow possible object
   *     updates later
   * @param rawCommand Reference of the command which initiated the process, is <code>null</code> if
   *     initiated by a REST API call
   * @param proposalKey Resource identifier
   * @param parameters Parameters produced by the Command line parser or transmitted via the REST
   *     API
   * @param ownerKey Key of the sale associate who owns the proposal to be updated
   * @param saConsumerRecordKey Key of the consumer record attached to the sale associate
   * @param isUserAdmin
   * @return Just updated proposal
   * @throws DataSourceException if the retrieval of the last created proposal or of the location
   *     information fail
   * @throws InvalidIdentifierException if there's an issue with the Proposal identifier is invalid
   * @throws InvalidStateException if the Proposal is not update-able
   * @throws CommunicationException if the communication of the update confirmation fails
   */
  public static Proposal updateProposal(
      PersistenceManager pm,
      RawCommand rawCommand,
      Long proposalKey,
      JsonObject parameters,
      Long ownerKey,
      Long saConsumerRecordKey,
      boolean isUserAdmin)
      throws DataSourceException, InvalidIdentifierException, InvalidStateException,
          CommunicationException {

    SaleAssociate owner = getSaleAssociateOperations().getSaleAssociate(pm, ownerKey);
    Consumer saConsumerRecord = getConsumerOperations().getConsumer(pm, saConsumerRecordKey);
    Proposal proposal =
        getProposalOperations()
            .getProposal(
                pm,
                proposalKey,
                isUserAdmin ? null : ownerKey,
                isUserAdmin ? null : owner.getStoreKey());
    State currentState = proposal.getState();

    // Workflow state change
    if (parameters.size() == 1 && parameters.containsKey(Command.STATE)) {
      String proposedState = parameters.getString(Command.STATE);

      // Close
      if (State.confirmed.equals(currentState) && State.closed.toString().equals(proposedState)) {
        // Update the user's statistics
        owner.setClosedProposalNb(
            owner.getClosedProposalNb() == null ? 1 : owner.getClosedProposalNb() + 1);
        owner = BaseSteps.getSaleAssociateOperations().updateSaleAssociate(pm, owner);

        // Update the store's statistics
        Store store = getStoreOperations().getStore(pm, proposal.getStoreKey());
        store.setClosedProposalNb(
            store.getClosedProposalNb() == null ? 1 : store.getClosedProposalNb() + 1);
        store = BaseSteps.getStoreOperations().updateStore(pm, store);

        // Get the associated demand
        Demand demand = getDemandOperations().getDemand(pm, proposal.getDemandKey(), null);

        if (rawCommand != null && !Source.robot.equals(rawCommand.getSource())) {
          Locale locale = saConsumerRecord.getLocale();

          MessageGenerator msgGen =
              new MessageGenerator(rawCommand.getSource(), demand.getHashTags(), locale);
          msgGen
              .put("proposal>owner>name", saConsumerRecord.getName())
              .fetch(demand)
              .fetch(proposal)
              .put("message>footer", msgGen.getAlternateMessage(MessageId.messageFooter));

          String subject = null;
          if (Source.mail.equals(msgGen.getCommunicationChannel())) {
            subject = rawCommand.getSubject();
          }
          if (subject == null) {
            subject = msgGen.getAlternateMessage(MessageId.messageSubject, msgGen.getParameters());
          }
          subject = MailConnector.prepareSubjectAsResponse(subject, locale);

          communicateToConsumer(
              msgGen.getCommunicationChannel(),
              subject,
              saConsumerRecord,
              new String[] {msgGen.getMessage(MessageId.PROPOSAL_CLOSING_OK_TO_ASSOCIATE)});
        }

        if (!State.closed.equals(demand.getState())) {
          // Get demand owner
          Consumer demandOwner = getConsumerOperations().getConsumer(pm, demand.getOwnerKey());
          Location location = getLocationOperations().getLocation(pm, store.getLocationKey());
          Registrar registrar = getRegistrarOperations().getRegistrar(pm, store.getRegistrarKey());

          // Inform Proposal owner about the closing
          Locale locale = demandOwner.getLocale();
          MessageGenerator msgGen =
              new MessageGenerator(
                  demandOwner.getPreferredConnection(), demand.getHashTags(), locale);
          msgGen
              .put("demand>owner>name", demandOwner.getName())
              .fetch(demand)
              .fetch(proposal)
              .fetch(store)
              .fetch(location, "store")
              .fetch(registrar)
              .put("message>footer", msgGen.getAlternateMessage(MessageId.messageFooter))
              .put(
                  "command>footer",
                  LabelExtractor.get(ResourceFileId.fourth, "command_message_footer", locale));

          String closeDemand =
              LabelExtractor.get(
                  ResourceFileId.fourth,
                  "command_message_body_demand_close",
                  msgGen.getParameters(),
                  locale);
          String subject = null;
          if (Source.mail.equals(msgGen.getCommunicationChannel())
              && Source.mail.equals(demand.getSource())) {
            subject =
                BaseSteps.getRawCommandOperations()
                    .getRawCommand(pm, demand.getRawCommandId())
                    .getSubject();
          }
          if (subject == null) {
            subject = msgGen.getAlternateMessage(MessageId.messageSubject, msgGen.getParameters());
          }
          subject = MailConnector.prepareSubjectAsResponse(subject, locale);

          msgGen
              .put(
                  "command>threadSubject",
                  BaseConnector.prepareMailToSubject(
                      MailConnector.prepareSubjectAsResponse(subject, locale)))
              .put("command>closeDemand", BaseConnector.prepareMailToBody(closeDemand));

          try {
            communicateToConsumer(
                msgGen.getCommunicationChannel(),
                subject,
                demandOwner,
                new String[] {msgGen.getMessage(MessageId.PROPOSAL_CLOSING_OK_TO_CONSUMER)});
          } catch (CommunicationException e) {
            // Not a critical error, should not block the rest of the process
            getLogger().warning("Cannot inform " + demand.getOwnerKey());
          }
        }

        // No need to bother CC-ed
      }
      // Cancel
      else if (!State.closed.equals(currentState)
          && State.cancelled.toString().equals(proposedState)) {
        proposal.setCancelerKey(saConsumerRecord.getKey());

        // Get the associated demand
        Demand demand = getDemandOperations().getDemand(pm, proposal.getDemandKey(), null);
        demand.removeProposalKey(proposalKey);

        // Confirm the proposal canceling to the owner
        if (rawCommand != null) {
          Locale locale = saConsumerRecord.getLocale();

          MessageGenerator msgGen =
              new MessageGenerator(rawCommand.getSource(), demand.getHashTags(), locale);
          msgGen
              .put("proposal>owner>name", saConsumerRecord.getName())
              .fetch(demand)
              .fetch(proposal)
              .put("message>footer", msgGen.getAlternateMessage(MessageId.messageFooter));

          String subject = null;
          if (Source.mail.equals(msgGen.getCommunicationChannel())) {
            subject = rawCommand.getSubject();
          }
          if (subject == null) {
            subject = msgGen.getAlternateMessage(MessageId.messageSubject, msgGen.getParameters());
          }
          subject = MailConnector.prepareSubjectAsResponse(subject, locale);

          communicateToConsumer(
              msgGen.getCommunicationChannel(),
              subject,
              saConsumerRecord,
              new String[] {msgGen.getMessage(MessageId.PROPOSAL_CANCELLATION_OK_TO_ASSOCIATE)});
        }

        if (State.confirmed.equals(currentState)) {
          Consumer demandOwner = getConsumerOperations().getConsumer(pm, demand.getOwnerKey());
          Location location = getLocationOperations().getLocation(pm, demand.getLocationKey());

          // FIXME: Place the associated demand in the published state again if not expired
          demand.setState(State.published);
          demand.updateModificationDate(); // To be sure it's picked-up by the next cron job
          // 'processPublishedDemands'

          // Notify Consumer about the confirmed Proposal cancellation
          Locale locale = demandOwner.getLocale();
          MessageGenerator msgGen =
              new MessageGenerator(
                  demandOwner.getPreferredConnection(), demand.getHashTags(), locale);
          msgGen
              .put("demand>owner>name", demandOwner.getName())
              .fetch(demand)
              .fetch(location, "demand")
              .fetch(proposal)
              .put("message>footer", msgGen.getAlternateMessage(MessageId.messageFooter))
              .put(
                  "command>footer",
                  LabelExtractor.get(ResourceFileId.fourth, "command_message_footer", locale));

          String cancelDemand =
              LabelExtractor.get(
                  ResourceFileId.fourth,
                  "command_message_body_demand_cancel",
                  msgGen.getParameters(),
                  locale);
          String updateDemand =
              LabelExtractor.get(
                  ResourceFileId.fourth,
                  "command_message_body_demand_update",
                  msgGen.getParameters(),
                  locale);
          String subject = null;
          if (Source.mail.equals(msgGen.getCommunicationChannel())
              && Source.mail.equals(demand.getSource())) {
            subject =
                getRawCommandOperations().getRawCommand(pm, demand.getRawCommandId()).getSubject();
          }
          if (subject == null) {
            subject = msgGen.getAlternateMessage(MessageId.messageSubject, msgGen.getParameters());
          }
          subject = MailConnector.prepareSubjectAsResponse(subject, locale);

          msgGen
              .put("command>threadSubject", BaseConnector.prepareMailToSubject(subject))
              .put("command>cancelDemand", BaseConnector.prepareMailToBody(cancelDemand))
              .put("command>updateDemand", BaseConnector.prepareMailToBody(updateDemand));

          try {
            communicateToConsumer(
                msgGen.getCommunicationChannel(),
                subject,
                demandOwner,
                new String[] {
                  msgGen.getMessage(MessageId.PROPOSAL_CONFIRMED_CANCELLATION_OK_TO_CONSUMER)
                });
          } catch (CommunicationException e) {
            // Not a critical error, should not block the rest of the process
            getLogger().warning("Cannot inform " + demand.getOwnerKey());
          }
        }

        getDemandOperations().updateDemand(pm, demand);
      } else {
        throw new InvalidStateException(
            "Invalid state change attempt to: " + proposedState,
            currentState.toString(),
            proposedState);
      }

      proposal.setState(proposedState);
      proposal = getProposalOperations().updateProposal(pm, proposal);
    }
    // Normal attribute update
    else if (State.opened.equals(currentState)
        || State.published.equals(currentState)
        || State.invalid.equals(currentState)) {
      // Integrate updates
      proposal.fromJson(parameters, isUserAdmin, false);

      // Prepare as a new Demand
      proposal.setState(State.opened); // Will force the re-validation of the entire demand

      // Persist updates
      proposal = getProposalOperations().updateProposal(pm, proposal);

      // Detach the proposal from the associated demand (just in case it's now invalid)
      Demand demand = getDemandOperations().getDemand(pm, proposal.getDemandKey(), null);
      demand.removeProposalKey(proposalKey);
      getDemandOperations().updateDemand(pm, demand);

      // Related workflow step
      MaelzelServlet.triggerValidationTask(proposal);
    } else {
      throw new InvalidStateException(
          "Entity not in modifiable state", currentState.toString(), null);
    }

    return proposal;
  }