@Override
  public void save(NotificationInfo message) throws Exception {
    boolean created = NotificationSessionManager.createSystemProvider();
    SessionProvider sProvider = NotificationSessionManager.getSessionProvider();
    final ReentrantLock localLock = lock;
    try {
      localLock.lock();
      Node messageHomeNode =
          getOrCreateMessageParent(sProvider, workspace, message.getKey().getId());
      Node messageNode = messageHomeNode.addNode(message.getId(), NTF_MESSAGE);
      messageNode.setProperty(NTF_FROM, message.getFrom());
      messageNode.setProperty(NTF_ORDER, message.getOrder());
      messageNode.setProperty(NTF_PROVIDER_TYPE, message.getKey().getId());
      messageNode.setProperty(NTF_OWNER_PARAMETER, message.getArrayOwnerParameter());
      messageNode.setProperty(NTF_SEND_TO_DAILY, message.getSendToDaily());
      messageNode.setProperty(NTF_SEND_TO_WEEKLY, message.getSendToWeekly());
      messageHomeNode.getSession().save();

      // record statistics insert entity
      if (NotificationContextFactory.getInstance().getStatistics().isStatisticsEnabled()) {
        NotificationContextFactory.getInstance().getStatisticsCollector().insertEntity(NTF_MESSAGE);
      }

    } catch (Exception e) {
      LOG.error("Failed to save the NotificationMessage", e);
    } finally {
      NotificationSessionManager.closeSessionProvider(created);
      localLock.unlock();
    }
  }
  private void removeProperty(Session session, String path, String property, String value) {
    final boolean stats =
        NotificationContextFactory.getInstance().getStatistics().isStatisticsEnabled();
    try {
      Node node = (Node) session.getItem(path);
      List<String> values = NotificationUtils.valuesToList(node.getProperty(property).getValues());
      if (values.contains(value)) {
        values.remove(value);
        if (values.isEmpty()) {
          values.add("");
        }
        node.setProperty(property, values.toArray(new String[values.size()]));
        node.save();

        // record entity update here
        if (stats) {
          NotificationContextFactory.getInstance()
              .getStatisticsCollector()
              .updateEntity(NTF_MESSAGE);
        }
      }
    } catch (Exception e) {
      LOG.warn(String.format("Failed to remove property %s of value %s on node ", property, value));
    }
  }
  @Override
  public void removeMessageAfterSent() throws Exception {
    final boolean stats =
        NotificationContextFactory.getInstance().getStatistics().isStatisticsEnabled();
    boolean created = NotificationSessionManager.createSystemProvider();
    SessionProvider sProvider = NotificationSessionManager.getSessionProvider();

    try {
      Node notificationHome = getNotificationHomeNode(sProvider, workspace);
      Session session = notificationHome.getSession();
      // remove all
      Set<String> listPaths = removeByCallBack.get(REMOVE_ALL);
      removeByCallBack.remove(REMOVE_ALL);
      if (listPaths != null && listPaths.size() > 0) {
        for (String nodePath : listPaths) {
          try {
            session.getItem(nodePath).remove();
            // record entity delete here
            if (stats) {
              NotificationContextFactory.getInstance()
                  .getStatisticsCollector()
                  .deleteEntity(NTF_MESSAGE);
            }

            LOG.debug("Remove NotificationMessage " + nodePath);
          } catch (Exception e) {
            LOG.warn(
                "Failed to remove node of NotificationMessage " + nodePath + "\n" + e.getMessage());
            LOG.debug("Remove NotificationMessage " + nodePath, e);
          }
        }
        session.save();
      }

      listPaths = removeByCallBack.get(REMOVE_DAILY);
      if (listPaths != null && listPaths.size() > 0) {
        for (String nodePath : listPaths) {
          try {
            Item item = session.getItem(nodePath);
            if (item.isNode()) {
              Node node = (Node) item;
              node.setProperty(NTF_SEND_TO_DAILY, new String[] {""});
            }
            LOG.debug("Remove SendToDaily property " + nodePath);
          } catch (Exception e) {
            LOG.warn(
                "Failed to remove SendToDaily property of " + nodePath + "\n" + e.getMessage());
            LOG.debug("Remove SendToDaily property " + nodePath, e);
          }
        }
        session.save();
      }
    } catch (Exception e) {
      LOG.warn("Failed to remove message after sent email notification", e);
    } finally {
      NotificationSessionManager.closeSessionProvider(created);
    }
  }
  private NodeIterator getDailyNodes(Node pluginDayNode, String userId) throws Exception {
    final boolean stats =
        NotificationContextFactory.getInstance().getStatistics().isStatisticsEnabled();
    long startTime = 0;
    if (stats) startTime = System.currentTimeMillis();
    //
    userId = userId.replace("'", "''");

    StringBuilder strQuery =
        new StringBuilder("SELECT * FROM ").append(NTF_MESSAGE).append(" WHERE ");
    strQuery
        .append(" (jcr:path LIKE '")
        .append(pluginDayNode.getPath())
        .append("/%'")
        .append(" AND NOT jcr:path LIKE '")
        .append(pluginDayNode.getPath())
        .append("/%/%')");
    strQuery.append(" AND (").append(NTF_SEND_TO_DAILY).append("='").append(userId).append("'");
    strQuery
        .append(" OR ")
        .append(NTF_SEND_TO_DAILY)
        .append("='")
        .append(NotificationInfo.FOR_ALL_USER)
        .append("') AND ")
        .append(NTF_FROM)
        .append("<>'")
        .append(userId)
        .append("'");
    strQuery
        .append(" order by ")
        .append(NTF_ORDER)
        .append(ASCENDING)
        .append(", exo:dateCreated")
        .append(DESCENDING);

    QueryManager qm = pluginDayNode.getSession().getWorkspace().getQueryManager();
    Query query = qm.createQuery(strQuery.toString(), Query.SQL);
    NodeIterator it = query.execute().getNodes();

    if (stats) {
      NotificationContextFactory.getInstance()
          .getStatisticsCollector()
          .queryExecuted(strQuery.toString(), it.getSize(), System.currentTimeMillis() - startTime);
    }
    return it;
  }