/*
 * Copyright (C) 2003-2007 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */

package org.exoplatform.contact.service.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.version.VersionException;

import org.apache.commons.lang.StringUtils;
import org.exoplatform.contact.service.AddressBook;
import org.exoplatform.contact.service.Contact;
import org.exoplatform.contact.service.ContactAttachment;
import org.exoplatform.contact.service.ContactFilter;
import org.exoplatform.contact.service.ContactPageList;
import org.exoplatform.contact.service.DataPageList;
import org.exoplatform.contact.service.DataStorage;
import org.exoplatform.contact.service.GroupContactData;
import org.exoplatform.contact.service.SharedAddressBook;
import org.exoplatform.contact.service.Tag;
import org.exoplatform.contact.service.Utils;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.access.AccessControlEntry;
import org.exoplatform.services.jcr.access.PermissionType;
import org.exoplatform.services.jcr.access.SystemIdentity;
import org.exoplatform.services.jcr.core.ExtendedNode;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.Group;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.Identity;

/**
 * Created by The eXo Platform SARL
 * Author : Hung Nguyen Quang
 *          hung.nguyen@exoplatform.com
 * Jul 10, 2007  
 */
public class JCRDataStorage implements DataStorage {

  private static final String  PROP_ADDRESSBOOK_REFS  = "exo:categories";

  final private static String  CONTACTS               = "contacts".intern();

  final private static String  PERSONAL_ADDRESS_BOOKS = "contactGroup".intern();

  final private static String  GROUP_ADDRESS_BOOKS    = "GroupAddress".intern();

  final private static String  TAGS                   = "tags".intern();

  final private static String  NT_UNSTRUCTURED        = "nt:unstructured".intern();

  final private static String  CONTACT_APP            = "ContactApplication".intern();

  final private static String  SHARED_HOME            = "Shared".intern();

  final private static String  SHARED_CONTACT         = "SharedContact".intern();

  final private static String  SHARED_ADDRESSBOOK     = "SharedAddressBook".intern();

  final private static String  SHARED_MIXIN           = "exo:contactShared".intern();

  final private static String  SHARED_PROP            = "exo:sharedId".intern();

  private static Log           log                    = ExoLogger.getLogger(JCRDataStorage.class);

  private NodeHierarchyCreator nodeHierarchyCreator_;

  private RepositoryService    repoService_;

  public JCRDataStorage(NodeHierarchyCreator nodeHierarchyCreator, RepositoryService repoService) throws Exception {
    nodeHierarchyCreator_ = nodeHierarchyCreator;
    repoService_ = repoService;
  }

  /**
   * {@inheritDoc}
   */
  public Node getSharedContact(String userId) throws Exception {
    Node contactHome = getContactUserDataHome(userId);
    Node sharedHome;
    try {
      sharedHome = contactHome.getNode(SHARED_HOME);
    } catch (PathNotFoundException ex) {
      sharedHome = contactHome.addNode(SHARED_HOME, NT_UNSTRUCTURED);
      contactHome.save();
    }
    try {
      return sharedHome.getNode(SHARED_CONTACT);
    } catch (PathNotFoundException ex) {
      Node sharedContact = sharedHome.addNode(SHARED_CONTACT, NT_UNSTRUCTURED);
      if (sharedContact.canAddMixin("mix:referenceable")) {
        sharedContact.addMixin("mix:referenceable");
      }
      sharedHome.save();
      return sharedContact;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Node getSharedAddressBooksHome(String userId) throws Exception {
    Node contactHome = getContactUserDataHome(userId);
    Node sharedHome;
    try {
      sharedHome = contactHome.getNode(SHARED_HOME);
    } catch (PathNotFoundException ex) {
      sharedHome = contactHome.addNode(SHARED_HOME, NT_UNSTRUCTURED);
      contactHome.save();
    }
    try {
      return sharedHome.getNode(SHARED_ADDRESSBOOK);
    } catch (PathNotFoundException ex) {
      Node sharedAddressBooksHome = sharedHome.addNode(SHARED_ADDRESSBOOK, NT_UNSTRUCTURED);
      if (sharedAddressBooksHome.canAddMixin("mix:referenceable")) {
        sharedAddressBooksHome.addMixin("mix:referenceable");
      }
      sharedHome.save();
      return sharedAddressBooksHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Contact loadPublicContactByUser(String userId) throws Exception {
    try {
      Node contactHomeNode = getPersonalContactsHome(userId);
      return Utils.getContact(contactHomeNode.getNode(userId), PUBLIC);
    } catch (PathNotFoundException e) {
      log.error("Public contact " + userId + " not found");
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Contact> findAllContactsByOwner(String username) throws Exception {
    Node contactHomeNode = getPersonalContactsHome(username);
    List<Contact> contacts = new ArrayList<Contact>();
    NodeIterator iter = contactHomeNode.getNodes();
    Contact contact;
    while (iter.hasNext()) {
      Node contactNode = iter.nextNode();
      contact = Utils.getContact(contactNode, PERSONAL);
      contacts.add(contact);
    }
    return contacts;
  }

  /**
   * {@inheritDoc}
   */
  public ContactPageList findContactsByFilter(String username, ContactFilter filter, String type) throws Exception {
    boolean canfind = false;

    if (type.equals(PERSONAL)) {
      // look in user home
      Node contactHomeNode = getPersonalContactsHome(username);
      filter.setAccountPath(contactHomeNode.getPath());
      canfind = true;
      // qm = getSession(createSystemProvider()).getWorkspace().getQueryManager();
    } else if (type.equals(PUBLIC)) {
      // look in all users
      Node publicContactHomeNode = getPublicContactsHome();
      String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
      filter.setAccountPath(usersPath);
      canfind = true;
      // qm = publicContactHomeNode.getSession().getWorkspace().getQueryManager();
    } else if (type.equals(SHARED)) {
      // look in contacts shared to username
      Node sharedAddressBookHolder = getSharedAddressBooksHome(username);
      PropertyIterator iter = sharedAddressBookHolder.getReferences();
      Node addressBook;
      while (iter.hasNext()) {
        addressBook = iter.nextProperty().getParent();
        if (addressBook.getName().equals(filter.getCategories()[0])) {
          Node contacts = addressBook.getParent().getParent().getNode(CONTACTS);
          filter.setAccountPath(contacts.getPath());
          canfind = true;
          // qm = contacts.getSession().getWorkspace().getQueryManager();
          break;
        }
      }
    }
    if (canfind) {
      return new ContactPageList(username, 10, filter.getStatement(), type);
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public Contact loadPersonalContact(String ownerUserId, String contactId) throws Exception {
    Node contactHomeNode = getPersonalContactsHome(ownerUserId);
    try {
      return Utils.getContact(contactHomeNode.getNode(contactId), PERSONAL);
    } catch (PathNotFoundException ex) {
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  public ContactPageList findPersonalContactsByAddressBook(String owner, String addressBookId) throws Exception {
    Node userContactsHome = getPersonalContactsHome(owner);
    String queryString = new StringBuffer("/jcr:root" + userContactsHome.getPath() + "//element(*,exo:contact)[@exo:categories='").append(addressBookId).append("']").append("order by @exo:fullName,@exo:id ascending").toString();
    ContactPageList pageList = new ContactPageList(owner, 10, queryString, PERSONAL);
    return pageList;
  }

  /**
   * {@inheritDoc}
   */
  public List<String> findEmailsByAddressBook(String username, String addressBookId) throws AddressBookNotFoundException, Exception {
    AddressBookType type = getAddressBookType(username, addressBookId);

    if (type == AddressBookType.Personal) {
      return findEmailsInPersonalAddressBook(username, addressBookId);
    } else if (type == AddressBookType.Shared) {
      return findEmailsInPersonalAddressBook(username, addressBookId);
    } else if (type == AddressBookType.Public) {
      return findEmailsInPublicAddressBook(username, addressBookId);
    }
    throw new AddressBookNotFoundException(addressBookId);
  }

  /**
   * {@inheritDoc}
   */
  public AddressBookType getAddressBookType(String username, String addressBookId) throws Exception {
    AddressBook shared = getSharedAddressBookById(username, addressBookId);
    if (shared != null) {
      return AddressBookType.Shared;
    }

    AddressBook personal = findPersonalAddressBookById(username, addressBookId);
    if (personal != null) {
      return AddressBookType.Personal;
    }
    return null;
  }

  public AddressBook findPersonalAddressBookById(String username, String addressBookId) {
    throw new UnsupportedOperationException();
  }

  /**
   * {@inheritDoc}
   */
  public List<String> findEmailsInPersonalAddressBook(String username, String addressBookId) throws Exception {
    Node contactHome = getPersonalContactsHome(username);
    QueryManager qm = contactHome.getSession().getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:categories='").append(addressBookId).append("']");
    NodeIterator it = qm.createQuery(queryString.toString(), Query.XPATH).execute().getNodes();
    return getEmails(it);
  }

  private List<String> getEmails(NodeIterator it) throws Exception {
    List<String> address = new ArrayList<String>();
    while (it.hasNext()) {
      Node contact = it.nextNode();
      try {
        String emails = Utils.valuesToString(contact.getProperty("exo:emailAddress").getValues());
        if (!Utils.isEmpty(emails))
          address.add(emails.split(",")[0].split(";")[0]);
      } catch (PathNotFoundException e) {
      }
    }
    return address;
  }

  /**
   * {@inheritDoc}
   */
  public List<String> findEmailsInPublicAddressBook(String username, String groupId) throws Exception {
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    QueryManager qm = getSession(createSystemProvider()).getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:categories='").append(groupId).append("']");
    NodeIterator it = qm.createQuery(queryString.toString(), Query.XPATH).execute().getNodes();
    return getEmails(it);
  }

  /**
   * {@inheritDoc}
   */
  public List<String> getAllEmailBySharedGroup(String username, String addressBookId) throws Exception {
    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookMock.getReferences();
    Node addressBook;
    QueryManager qm = sharedAddressBookMock.getSession().getWorkspace().getQueryManager();
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      if (addressBook.getName().equals(addressBookId)) {
        StringBuffer queryString = new StringBuffer("/jcr:root" + addressBook.getParent().getParent().getNode(CONTACTS).getPath() + "//element(*,exo:contact)[(@exo:categories='").append(addressBookId).append("')]");
        NodeIterator it = qm.createQuery(queryString.toString(), Query.XPATH).execute().getNodes();
        return getEmails(it);
      }
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public AddressBook toAddressBook(Node contactGroupNode) throws Exception {
    AddressBook contactGroup = new AddressBook();
    if (contactGroupNode.hasProperty("exo:id"))
      contactGroup.setId(Utils.decodeGroupId(contactGroupNode.getProperty("exo:id").getString()));
    if (contactGroupNode.hasProperty("exo:name"))
      contactGroup.setName(contactGroupNode.getProperty("exo:name").getString());
    if (contactGroupNode.hasProperty("exo:description"))
      contactGroup.setDescription(contactGroupNode.getProperty("exo:description").getString());
    if (contactGroupNode.hasProperty("exo:viewPermissionUsers"))
      contactGroup.setViewPermissionUsers(Utils.valuesToStringArray(contactGroupNode.getProperty("exo:viewPermissionUsers").getValues()));
    if (contactGroupNode.hasProperty("exo:editPermissionUsers"))
      contactGroup.setEditPermissionUsers(Utils.valuesToStringArray(contactGroupNode.getProperty("exo:editPermissionUsers").getValues()));

    if (contactGroupNode.hasProperty("exo:viewPermissionGroups"))
      contactGroup.setViewPermissionGroups(Utils.valuesToStringArray(contactGroupNode.getProperty("exo:viewPermissionGroups").getValues()));
    if (contactGroupNode.hasProperty("exo:editPermissionGroups"))
      contactGroup.setEditPermissionGroups(Utils.valuesToStringArray(contactGroupNode.getProperty("exo:editPermissionGroups").getValues()));
    return contactGroup;
  }

  /**
   * {@inheritDoc}
   */
  public AddressBook loadPersonalAddressBook(String username, String groupId) throws Exception {
    if (groupId == null)
      return null;
    Node contactGroupHomeNode = getPersonalAddressBooksHome(username);
    if (contactGroupHomeNode.hasNode(groupId))
      return toAddressBook(contactGroupHomeNode.getNode(groupId));
    return null;
  }

  public AddressBook loadPublicAddressBook(String username, String groupId) throws Exception {
    if (groupId == null)
      return null;
    Node contactGroupHomeNode = getPublicAddressHome();
    if (contactGroupHomeNode.hasNode(Utils.encodeGroupId(groupId)))
      return toAddressBook(contactGroupHomeNode.getNode(Utils.encodeGroupId(groupId)));
    return null;
  }

  public AddressBook getSharedAddressBookById(String username, String addressBookId) throws Exception {
    Node sharedAddressBookNode = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookNode.getReferences();
    Node addressBook;
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      if (addressBook.getName().equals(addressBookId)) {
        return toAddressBook(addressBook);
      }
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public List<AddressBook> findPersonalAddressBooksByOwner(String username) throws Exception {
    Node addressBooksHome = getPersonalAddressBooksHome(username);
    List<AddressBook> addressBooks = new ArrayList<AddressBook>();
    NodeIterator iter = addressBooksHome.getNodes();
    while (iter.hasNext()) {
      Node addressBook = iter.nextNode();
      addressBooks.add(toAddressBook(addressBook));
    }
    return addressBooks;
  }

  public List<String> getPublicAddresses(String username) throws Exception {
    List<String> groupIds;
    if (username == null) {
      Identity identity = ConversationState.getCurrent().getIdentity();
      groupIds = new ArrayList<String>(identity.getGroups());
    } else {
      OrganizationService organizationService = (OrganizationService) ExoContainerContext.getCurrentContainer().getComponentInstanceOfType(OrganizationService.class);
      Object[] objGroupIds = organizationService.getGroupHandler().findGroupsOfUser(username).toArray();
      groupIds = new ArrayList<String>();
      for (Object object : objGroupIds) {
        groupIds.add(((Group) object).getId());
      }
    }
    Node addressBooksHome = getPublicAddressHome();
    List<String> addressBooks = new ArrayList<String>();
    NodeIterator iter = addressBooksHome.getNodes();
    while (iter.hasNext()) {
      Node addressBook = iter.nextNode();
      String id = Utils.decodeGroupId(addressBook.getProperty("exo:id").getString());
      if (addressBook.hasProperty("exo:viewPermissionUsers")) {
        String[] viewUsers = Utils.valuesToStringArray(addressBook.getProperty("exo:viewPermissionUsers").getValues());
        if (viewUsers.length > 0 && Arrays.asList(viewUsers).contains(username)) {
          if (!addressBooks.contains(id))
            addressBooks.add(id);
        }
      }
      if (addressBook.hasProperty("exo:viewPermissionGroups")) {
        String[] viewGroups = Utils.valuesToStringArray(addressBook.getProperty("exo:viewPermissionGroups").getValues());
        for (String viewGroup : viewGroups) {
          if (viewGroup.contains(Utils.COLON))
            viewGroup = viewGroup.split(Utils.COLON)[0];
          if (groupIds.contains(viewGroup)) {
            if (!addressBooks.contains(id))
              addressBooks.add(id);
            break;
          }
        }
      }
    }
    return addressBooks;
  }

  /**
   * {@inheritDoc}
   */
  public List<Contact> removeContacts(String username, List<String> contactIds) throws Exception {

    Node contactHomeNode = getPersonalContactsHome(username);
    List<Contact> contacts = new ArrayList<Contact>();
    for (String contactId : contactIds) {
      if (contactHomeNode.hasNode(contactId)) {
        Contact contact = loadPersonalContact(username, contactId);
        contactHomeNode.getNode(contactId).remove();
        contactHomeNode.getSession().save();
        contacts.add(contact);
      }
    }
    return contacts;
  }

  /**
   * {@inheritDoc}
   */
  public void moveContacts(String username, List<Contact> contacts, String addressType) throws Exception {
    Node publicContactHome = getPersonalContactsHome(username);
    for (Contact contact : contacts) {
      if (addressType.equals(PERSONAL)) {
        saveContactUser(username, contact, false);
      } else if (addressType.equals(SHARED)) {
        // CS-2389
        if (!haveEditPermissionOnAddressBook(username, contact.getAddressBookIds()[0]) || (contact.getContactType().equals(SHARED) && !haveEditPermissionOnContact(username, contact))) {
          log.info("\n Do not have edit permission. \n");
          throw new Exception();
        }
        saveContactToSharedAddressBook(username, contact.getAddressBookIds()[0], contact, true);
        if (publicContactHome.hasNode(contact.getId()))
          publicContactHome.getNode(contact.getId()).remove();
      }
    }
    if (publicContactHome.getSession().hasPendingChanges())
      publicContactHome.getSession().save();
  }

  /**
   * {@inheritDoc}
   */
  public AddressBook removePersonalAddressBook(String username, String addressBookId) throws Exception {
    Node addressBooksHomeNode = getPersonalAddressBooksHome(username);
    if (addressBooksHomeNode.hasNode(addressBookId)) {
      // load before removing
      AddressBook contactGroup = loadPersonalAddressBook(username, addressBookId);
      // remove the address book
      addressBooksHomeNode.getNode(addressBookId).remove();
      addressBooksHomeNode.save();
      addressBooksHomeNode.getSession().save();
      // Can not call removeContacts(...) here!!!
      return contactGroup;
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public void clearAddressBook(String username, String addressBookId) throws Exception {
    List<String> contactIds = getUserContactNodesByGroup(username, addressBookId);
    removeContacts(username, contactIds);
  }

  /**
   * {@inheritDoc}
   */
  public void saveContact(String username, Contact contact, boolean isNew) throws Exception {
    Node contactHomeNode = getPersonalContactsHome(username);
    contactToNode(contactHomeNode, contact, isNew);
    contactHomeNode.getSession().save();
  }

  private void saveContactUser(String username, Contact contact, boolean isNew) throws Exception {
    Node contactHomeNode = getPersonalContactsHome(username);
    contactToNode(contactHomeNode, contact, isNew);
    contactHomeNode.getSession().save();
  }

  /**
   * {@inheritDoc}
   */
  public void savePersonalOrSharedAddressBook(String username, AddressBook addressbook, boolean isNew) throws Exception {
    Node groupNode = null;
    String id = addressbook.getId();
    if (isNew) {
      groupNode = getPersonalAddressBooksHome(username).addNode(id, "exo:contactGroup");
      groupNode.setProperty("exo:id", id);
    } else {
      try {
        groupNode = getPersonalAddressBooksHome(username).getNode(id);
      } catch (PathNotFoundException e) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter = sharedAddressBookMock.getReferences();
        Node addressBook;
        while (iter.hasNext()) {
          addressBook = iter.nextProperty().getParent();
          if (addressBook.getName().equals(id)) {

            // CS-2389
            if (!haveEditPermissionOnAddressBook(username, addressbook.getId())) {
              log.info("\n Do not have edit permission. \n");
              throw new Exception();
            }
            groupNode = addressBook;
            break;
          }
        }
      }
    }
    // TODO : should throw a business exception instead of PathNotFoundException
    if (groupNode == null && !isNew)
      throw new PathNotFoundException("No personal or shared address book for user " + username + " was found with ID " + addressbook);
    groupNode.setProperty("exo:name", addressbook.getName());
    groupNode.setProperty("exo:description", addressbook.getDescription());
    groupNode.setProperty("exo:editPermissionUsers", addressbook.getEditPermissionUsers());
    groupNode.setProperty("exo:viewPermissionUsers", addressbook.getViewPermissionUsers());
    groupNode.setProperty("exo:editPermissionGroups", addressbook.getEditPermissionGroups());
    groupNode.setProperty("exo:viewPermissionGroups", addressbook.getViewPermissionGroups());
    if (isNew)
      groupNode.getSession().save();
    else
      groupNode.save();
  }

  public void savePublicAddressBook(AddressBook addressbook, boolean isNew) throws Exception {
    Node groupNode = null;
    String id = Utils.encodeGroupId(addressbook.getId());
    if (isNew) {
      groupNode = getPublicAddressHome().addNode(id, "exo:contactGroup");
      groupNode.setProperty("exo:id", id);
    } else {
      try {
        groupNode = getPublicAddressHome().getNode(id);
        if (groupNode == null)
          throw new PathNotFoundException("No address book was found with ID " + addressbook);
      } catch (PathNotFoundException e) {
        log.error(e.getMessage());
      }
    }
    groupNode.setProperty("exo:name", addressbook.getName());
    groupNode.setProperty("exo:description", addressbook.getDescription());
    groupNode.setProperty("exo:editPermissionUsers", addressbook.getEditPermissionUsers());
    groupNode.setProperty("exo:viewPermissionUsers", addressbook.getViewPermissionUsers());
    groupNode.setProperty("exo:editPermissionGroups", addressbook.getEditPermissionGroups());
    groupNode.setProperty("exo:viewPermissionGroups", addressbook.getViewPermissionGroups());
    if (isNew)
      groupNode.getSession().save();
    else
      groupNode.save();
  }

  public void removeUserShareContact(String username, String contactId, String removedUser) throws Exception {
    Node contactNode;
    String split = "/";
    // shared contacts
    if (username.split(split).length > 1) {
      String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
      String temp = username.split(usersPath)[1];
      String userId = temp.split(split)[1];
      contactNode = getPersonalContactsHome(userId).getNode(contactId);
    } else {
      contactNode = getPersonalContactsHome(username).getNode(contactId);
    }
    List<String> values = new ArrayList<String>(Arrays.asList(Utils.valuesToStringArray(contactNode.getProperty(SHARED_PROP).getValues())));
    List<String> newValues = new ArrayList<String>(values);
    Node sharedContact = getSharedContact(removedUser);
    for (String value : values) {
      Node refNode = sharedContact.getSession().getNodeByUUID(value);
      if (refNode.getPath().equals(sharedContact.getPath())) {
        newValues.remove(value);
      }
    }
    try {
      String[] viewPer = Utils.valuesToStringArray(contactNode.getProperty("exo:viewPermissionUsers").getValues());
      if (viewPer != null) {
        List<String> newViewPer = new ArrayList<String>();
        newViewPer.addAll(Arrays.asList(viewPer));
        newViewPer.remove(removedUser + HYPHEN);
        contactNode.setProperty("exo:viewPermissionUsers", newViewPer.toArray(new String[] {}));
        String[] editPer = Utils.valuesToStringArray(contactNode.getProperty("exo:editPermissionUsers").getValues());
        if (editPer != null) {
          List<String> newEditPer = new ArrayList<String>();
          newEditPer.addAll(Arrays.asList(editPer));
          newEditPer.remove(removedUser + HYPHEN);
          contactNode.setProperty("exo:editPermissionUsers", newEditPer.toArray(new String[] {}));
        }
      }
    } catch (PathNotFoundException e) {
    }
    contactNode.setProperty(SHARED_PROP, newValues.toArray(new String[] {}));
    contactNode.save();
    contactNode.getSession().save();
  }

  /**
   * {@inheritDoc}
   */
  public void unshareAddressBook(String username, String addressBookId, String removedUser) throws Exception {

    Node addressBookNode = getPersonalAddressBooksHome(username).getNode(addressBookId);
    List<String> values = new ArrayList<String>(Arrays.asList(Utils.valuesToStringArray(addressBookNode.getProperty(SHARED_PROP).getValues())));
    List<String> newValues = new ArrayList<String>(values);

    Node sharedAddress = getSharedAddressBooksHome(removedUser);
    for (String value : values) {
      Node refNode = sharedAddress.getSession().getNodeByUUID(value);
      if (refNode.getPath().equals(sharedAddress.getPath())) {
        newValues.remove(value);
      }
    }

    String[] viewPer = null;
    try {
      viewPer = Utils.valuesToStringArray(addressBookNode.getProperty("exo:viewPermissionUsers").getValues());
    } catch (PathNotFoundException e) {
    }
    if (viewPer != null) {
      List<String> newViewPer = new ArrayList<String>();
      newViewPer.addAll(Arrays.asList(viewPer));
      newViewPer.remove(removedUser + HYPHEN);
      addressBookNode.setProperty("exo:viewPermissionUsers", newViewPer.toArray(new String[] {}));
      String[] editPer = null;
      try {
        editPer = Utils.valuesToStringArray(addressBookNode.getProperty("exo:editPermissionUsers").getValues());
      } catch (PathNotFoundException e) {
      }
      if (editPer != null) {
        List<String> newEditPer = new ArrayList<String>();
        newEditPer.addAll(Arrays.asList(editPer));
        newEditPer.remove(removedUser + HYPHEN);
        addressBookNode.setProperty("exo:editPermissionUsers", newEditPer.toArray(new String[] {}));
      }
    }
    addressBookNode.setProperty(SHARED_PROP, newValues.toArray(new String[] {}));
    addressBookNode.save();
    addressBookNode.getSession().save();
  }

  /**
   * {@inheritDoc}
   */
  public void shareAddressBook(String username, String addressBookId, List<String> receiveUsers) throws Exception {
    Node addressBookNode = getPersonalAddressBooksHome(username).getNode(addressBookId);
    Value[] values = {};
    if (addressBookNode.isNodeType(SHARED_MIXIN)) {
      values = addressBookNode.getProperty(SHARED_PROP).getValues();
    } else {
      addressBookNode.addMixin(SHARED_MIXIN);
      addressBookNode.setProperty("exo:sharedUserId", username);
    }

    List<Value> valueList = new ArrayList<Value>();
    for (String userId : receiveUsers) {
      Node sharedAddress = getSharedAddressBooksHome(userId.replaceFirst(DataStorage.HYPHEN, ""));
      boolean isExist = false;
      for (int i = 0; i < values.length; i++) {
        Value value = values[i];
        String uuid = value.getString();
        Node refNode = sharedAddress.getSession().getNodeByUUID(uuid);
        if (refNode.getPath().equals(sharedAddress.getPath())) {
          isExist = true;
          break;
        }
        valueList.add(value);
      }
      if (!isExist) {
        Value value2add = addressBookNode.getSession().getValueFactory().createValue(sharedAddress);
        valueList.add(value2add);
      }
    }

    if (valueList.size() > 0) {
      Map<String, Value> newValue = new LinkedHashMap<String, Value>();
      for (Value value : values)
        newValue.put(value.getString(), value);
      for (Value value : valueList)
        newValue.put(value.getString(), value);
      addressBookNode.setProperty(SHARED_PROP, newValue.values().toArray(new Value[newValue.size()]));
    } else {
      try {
        addressBookNode.getProperty(SHARED_PROP);
      } catch (PathNotFoundException e) {
        addressBookNode.setProperty(SHARED_PROP, new Value[] {}); // add to
        // fix bug
        // cs-1449
      }
    }
    addressBookNode.save();
    addressBookNode.getSession().save();
  }

  /**
   * {@inheritDoc}
   */
  public void shareContact(String username, String[] contactIds, List<String> receiveUsers) throws Exception {
    for (String contactId : contactIds) {
      Node contactNode = getPersonalContactsHome(username).getNode(contactId);
      Value[] values = {};
      if (contactNode.isNodeType(SHARED_MIXIN)) {
        values = contactNode.getProperty(SHARED_PROP).getValues();
      } else {
        contactNode.addMixin(SHARED_MIXIN);
        contactNode.setProperty("exo:sharedUserId", username);
      }
      List<Value> valueList = new ArrayList<Value>();
      for (String user : receiveUsers) {
        Node sharedContact = getSharedContact(user.replaceFirst(DataStorage.HYPHEN, ""));
        boolean isExist = false;
        for (int i = 0; i < values.length; i++) {
          Value value = values[i];
          String uuid = value.getString();
          Node refNode = sharedContact.getSession().getNodeByUUID(uuid);
          if (refNode.getPath().equals(sharedContact.getPath())) {
            isExist = true;
            break;
          }
          valueList.add(value);
        }
        if (!isExist) {
          Value value2add = contactNode.getSession().getValueFactory().createValue(sharedContact);
          valueList.add(value2add);
        }
      }
      if (valueList.size() > 0) {
        Map<String, Value> newValue = new LinkedHashMap<String, Value>();
        for (Value value : values)
          newValue.put(value.getString(), value);
        for (Value value : valueList)
          newValue.put(value.getString(), value);

        contactNode.setProperty(SHARED_PROP, newValue.values().toArray(new Value[newValue.size()]));
        contactNode.save();
        contactNode.getSession().save();
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<SharedAddressBook> findSharedAddressBooksByUser(String username) throws Exception {
    List<SharedAddressBook> addressBooks = new ArrayList<SharedAddressBook>();
    Node sharedAddress = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddress.getReferences();
    while (iter.hasNext()) {
      try {
        Node addressNode = iter.nextProperty().getParent();
        SharedAddressBook sharedAddressBook = new SharedAddressBook(addressNode.getProperty("exo:name").getString(), addressNode.getName(), addressNode.getProperty("exo:sharedUserId").getString());
        if (addressNode.hasProperty("exo:editPermissionUsers"))
          sharedAddressBook.setEditPermissionUsers(Utils.valuesToStringArray(addressNode.getProperty("exo:editPermissionUsers").getValues()));
        if (addressNode.hasProperty("exo:editPermissionGroups"))
          sharedAddressBook.setEditPermissionGroups(Utils.valuesToStringArray(addressNode.getProperty("exo:editPermissionGroups").getValues()));
        addressBooks.add(sharedAddressBook);
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Exception in method findSharedAddressBooksByUser", e);
        }
      }
    }
    return addressBooks;
  }

  /**
   * {@inheritDoc}
   */
  public void removeSharedContact(String username, String addressBookId, String contactId) throws Exception {
    // CS-2389
    if (!haveEditPermissionOnAddressBook(username, addressBookId)) {
      log.info("\n Do not have edit permission. \n");
      throw new Exception();
    }
    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter2 = sharedAddressBookMock.getReferences();
    Node addressBook;
    while (iter2.hasNext()) {
      addressBook = iter2.nextProperty().getParent();

      // need improved
      if (addressBook.getName().equals(addressBookId)) {
        Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
        try {
          contactHomeNode.getNode(contactId).remove();
          contactHomeNode.getSession().save();
          break;
        } catch (PathNotFoundException e) {
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public Contact getSharedContact(String username, String contactId) throws Exception {
    Node sharedContactMock = getSharedContact(username);
    PropertyIterator iter = sharedContactMock.getReferences();
    while (iter.hasNext()) {
      try {
        Node contactNode = iter.nextProperty().getParent();
        if (contactNode.getName().equals(contactId)) {
          Contact contact = Utils.getContact(contactNode, DataStorage.SHARED);
          if (contactNode.hasProperty("exo:sharedUserId")) {
            contact.setAuthor(contactNode.getProperty("exo:sharedUserId").getString());
          }
          return contact;
        }
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Exception in method getSharedContact", e);
        }
      }
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public DataPageList getSharedContacts(String username) throws Exception {
    List<Contact> sharedContacts = new ArrayList<Contact>();
    Node sharedContact = getSharedContact(username);
    PropertyIterator iter = sharedContact.getReferences();
    while (iter.hasNext()) {
      try {
        Node contactNode = iter.nextProperty().getParent();
        Contact contact = Utils.getContact(contactNode, SHARED);
        if (contactNode.hasProperty("exo:sharedUserId")) {
          contact.setAuthor(contactNode.getProperty("exo:sharedUserId").getString());
        }        
        sharedContacts.add(contact);
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Exception in method getSharedContacts", e);
        }
      }
    }
    return new DataPageList(sharedContacts, 10, null, false);
  }

  /**
   * {@inheritDoc}
   */
  public void saveSharedContact(String username, Contact contact) throws Exception {
    Node sharedContactMock = getSharedContact(username);
    PropertyIterator iter = sharedContactMock.getReferences();
    boolean isEdit = false;
    while (iter.hasNext()) {
      try {
        Node contactNode = iter.nextProperty().getParent();
        if (contactNode.getName().equals(contact.getId())) {
          // CS-2389
          if (!haveEditPermissionOnAddressBook(username, contact.getAddressBookIds()[0]) && !haveEditPermissionOnContact(username, contact)) {
            log.info("\n Do not have edit permission. \n");
            throw new Exception();
          }

          isEdit = true;
          contactToNode(contactNode.getParent(), contact, false);
          contactNode.getParent().getSession().save();
          return;
        }
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.debug("Exception in method saveSharedContact", e);
        }
      }
    }
    if (!isEdit)
      throw new PathNotFoundException();
  }

  /**
   * {@inheritDoc}
   */
  public void saveContactToSharedAddressBook(String username, String addressBookId, Contact contact, boolean isNew) throws Exception {
    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookMock.getReferences();
    Node addressBook;
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      if (addressBook.getName().equals(addressBookId)) {
        // CS-2389
        if (!haveEditPermissionOnAddressBook(username, addressBookId) && !haveEditPermissionOnContact(username, contact)) {
          log.info("\n Do not have edit permission. \n");
          throw new Exception();
        }

        Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
        contact.setOwner(false);
        contactToNode(contactHomeNode, contact, isNew);
        contactHomeNode.getSession().save();
        return;
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public Contact getSharedContactAddressBook(String username, String contactId) throws Exception {
    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookMock.getReferences();
    Node addressBook;
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
      try {
        Node contactNode = contactHomeNode.getNode(contactId);
        if (Arrays.asList(Utils.valuesToStringArray(contactNode.getProperty(PROP_ADDRESSBOOK_REFS).getValues())).contains(addressBook.getProperty("exo:id").getString()))
          return Utils.getContact(contactNode, DataStorage.SHARED);
      } catch (PathNotFoundException e) {
      }
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public ContactPageList getSharedContactsByAddressBook(String username, SharedAddressBook addressBook) throws Exception {
    if (addressBook == null)
      return null;
    Node contactHome = getPersonalContactsHome(addressBook.getSharedUserId());
    StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[(@exo:categories='").append(addressBook.getId()).append("')]").append(" order by @exo:fullName,@exo:id ascending");
    return new ContactPageList(username, 10, queryString.toString(), SHARED);
  }

  /**
   * {@inheritDoc}
   */
  public ContactPageList getPublicContactsByAddressBook(String groupId) throws Exception {
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    try {
      StringBuffer queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:categories='").append(groupId).append("' and @exo:isOwner='true'] ").append("order by @exo:fullName,@exo:id ascending");
      return new ContactPageList(null, 10, queryString.toString(), PUBLIC);
    } finally {
      // sysProvider.close();
    }
  }

  /**
   * {@inheritDoc}
   */
  public void addUserContactInAddressBook(String userId, String addressBookId) throws Exception {
    Node contactHome = getPersonalContactsHome(userId);
    Node contactNode = contactHome.getNode(userId);
    Value[] values = contactNode.getProperty(PROP_ADDRESSBOOK_REFS).getValues();
    List<String> ls = new ArrayList<String>();
    for (Value vl : values) {
      if (vl.getString().equals(addressBookId))
        return;
      ls.add(vl.getString());
    }
    ls.add(addressBookId);
    contactNode.setProperty(PROP_ADDRESSBOOK_REFS, ls.toArray(new String[] {}));
    contactNode.save();
  }

  /**
   * {@inheritDoc}
   */
  public void contactToNode(Node contactsHome, Contact contact, boolean isNew) throws Exception {
    Node contactNode;
    if (isNew) {
      contactNode = contactsHome.addNode(contact.getId(), "exo:contact");
      contactNode.setProperty("exo:id", contact.getId());
      if (contact.isOwner()) {
        contactNode.setProperty("exo:isOwner", true);
        contactNode.setProperty("exo:ownerId", contact.getOwnerId());
      }
    } else {
      contactNode = contactsHome.getNode(contact.getId());
    }
    setContactNodeProperties(contactNode, contact);
  }

  private Node setContactNodeProperties(Node contactNode, Contact contact) throws Exception {
    contactNode.setProperty("exo:fullName", contact.getFullName());
    contactNode.setProperty("exo:firstName", contact.getFirstName());
    contactNode.setProperty("exo:lastName", contact.getLastName());
    contactNode.setProperty("exo:nickName", contact.getNickName());
    contactNode.setProperty("exo:gender", contact.getGender());
    GregorianCalendar dateTime = new GregorianCalendar();
    Date birthday = contact.getBirthday();
    if (birthday != null) {
      dateTime.setTime(birthday);
      contactNode.setProperty("exo:birthday", dateTime);
    } else
      try {
        contactNode.getProperty("exo:birthday").remove();
      } catch (PathNotFoundException e) {
      }

    contactNode.setProperty("exo:jobTitle", contact.getJobTitle());

    if (contact.getEmailAddresses() == null || contact.getEmailAddress().length() == 0)
      contactNode.setProperty("exo:emailAddress", new String[] { "" });
    else
      contactNode.setProperty("exo:emailAddress", contact.getEmailAddresses().toArray(new String[] {}));

    contactNode.setProperty("exo:exoId", contact.getExoId());
    contactNode.setProperty("exo:googleId", contact.getGoogleId());
    contactNode.setProperty("exo:msnId", contact.getMsnId());
    contactNode.setProperty("exo:aolId", contact.getAolId());
    contactNode.setProperty("exo:yahooId", contact.getYahooId());
    contactNode.setProperty("exo:icrId", contact.getIcrId());
    contactNode.setProperty("exo:skypeId", contact.getSkypeId());
    contactNode.setProperty("exo:icqId", contact.getIcqId());

    contactNode.setProperty("exo:homeAddress", contact.getHomeAddress());
    contactNode.setProperty("exo:homeCity", contact.getHomeCity());
    contactNode.setProperty("exo:homeState_province", contact.getHomeState_province());
    contactNode.setProperty("exo:homePostalCode", contact.getHomePostalCode());
    contactNode.setProperty("exo:homeCountry", contact.getHomeCountry());
    contactNode.setProperty("exo:homePhone1", contact.getHomePhone1());
    contactNode.setProperty("exo:homePhone2", contact.getHomePhone2());
    contactNode.setProperty("exo:homeFax", contact.getHomeFax());
    contactNode.setProperty("exo:personalSite", contact.getPersonalSite());

    contactNode.setProperty("exo:workAddress", contact.getWorkAddress());
    contactNode.setProperty("exo:workCity", contact.getWorkCity());
    contactNode.setProperty("exo:workState_province", contact.getWorkStateProvince());
    contactNode.setProperty("exo:workPostalCode", contact.getWorkPostalCode());
    contactNode.setProperty("exo:workCountry", contact.getWorkCountry());
    contactNode.setProperty("exo:workPhone1", contact.getWorkPhone1());
    contactNode.setProperty("exo:workPhone2", contact.getWorkPhone2());
    contactNode.setProperty("exo:workFax", contact.getWorkFax());
    contactNode.setProperty("exo:mobilePhone", contact.getMobilePhone());
    contactNode.setProperty("exo:webPage", contact.getWebPage());

    contactNode.setProperty("exo:note", contact.getNote());
    contactNode.setProperty(PROP_ADDRESSBOOK_REFS, contact.getAddressBookIds());
    contactNode.setProperty("exo:tags", contact.getTags());
    contactNode.setProperty("exo:editPermissionUsers", contact.getEditPermissionUsers());
    contactNode.setProperty("exo:viewPermissionUsers", contact.getViewPermissionUsers());
    contactNode.setProperty("exo:editPermissionGroups", contact.getEditPermissionGroups());
    contactNode.setProperty("exo:viewPermissionGroups", contact.getViewPermissionGroups());

    if (contact.getLastUpdated() != null) {
      dateTime.setTime(contact.getLastUpdated());
      contactNode.setProperty("exo:lastUpdated", dateTime);
    }
    // save image to contact
    ContactAttachment attachment = contact.getAttachment();
    if (attachment != null) {
      // fix load image on IE6 UI
      ExtendedNode extNode = (ExtendedNode) contactNode;
      if (extNode.canAddMixin("exo:privilegeable"))
        extNode.addMixin("exo:privilegeable");
      String[] arrayPers = { PermissionType.READ, PermissionType.ADD_NODE, PermissionType.SET_PROPERTY, PermissionType.REMOVE };
      extNode.setPermission(SystemIdentity.ANY, arrayPers);
      List<AccessControlEntry> permsList = extNode.getACL().getPermissionEntries();
      for (AccessControlEntry accessControlEntry : permsList) {
        extNode.setPermission(accessControlEntry.getIdentity(), arrayPers);
      }

      if (attachment.getFileName() != null) {
        Node nodeFile = null;
        try {
          nodeFile = contactNode.getNode("image");
        } catch (PathNotFoundException ex) {
          nodeFile = contactNode.addNode("image", "nt:file");
        }
        Node nodeContent = null;
        try {
          nodeContent = nodeFile.getNode("jcr:content");
        } catch (PathNotFoundException ex) {
          nodeContent = nodeFile.addNode("jcr:content", "nt:resource");
        }
        nodeContent.setProperty("jcr:mimeType", attachment.getMimeType());
        nodeContent.setProperty("jcr:data", attachment.getInputStream());
        nodeContent.setProperty("jcr:lastModified", Calendar.getInstance().getTimeInMillis());
      }
    } else {
      if (contactNode.hasNode("image"))
        contactNode.getNode("image").remove();
    }
    return contactNode;
  }

  /**
   * {@inheritDoc}
   */
  public Tag getTag(Node tagNode) throws Exception {
    Tag tag = new Tag();
    if (tagNode.hasProperty("exo:id"))
      tag.setId(tagNode.getProperty("exo:id").getString());
    if (tagNode.hasProperty("exo:name"))
      tag.setName(tagNode.getProperty("exo:name").getString());
    if (tagNode.hasProperty("exo:description"))
      tag.setDescription(tagNode.getProperty("exo:description").getString());
    if (tagNode.hasProperty("exo:color"))
      tag.setColor(tagNode.getProperty("exo:color").getString());
    return tag;
  }

  /**
   * {@inheritDoc}
   */
  public void updateTag(String username, Tag tag) throws Exception {
    Node tagHome = getTagsHome(username);
    Node tagNode = tagHome.getNode(tag.getId());
    tagNode.setProperty("exo:name", tag.getName());
    tagNode.setProperty("exo:description", tag.getDescription());
    tagNode.setProperty("exo:color", tag.getColor());
    tagHome.save();
  }

  /**
   * {@inheritDoc}
   */
  public Tag getTag(String username, String tagId) throws Exception {
    Node tagHomeNode = getTagsHome(username);
    if (tagHomeNode.hasNode(tagId))
      return getTag(tagHomeNode.getNode(tagId));
    return null;
  }

  /**
   * {@inheritDoc}
   */
  public List<Tag> getTags(String username) throws Exception {
    Node tagHomeNode = getTagsHome(username);
    List<Tag> tags = new ArrayList<Tag>();
    NodeIterator iter = tagHomeNode.getNodes();
    while (iter.hasNext()) {
      Node tagNode = iter.nextNode();
      tags.add(getTag(tagNode));
    }
    return tags;
  }

  /**
   * {@inheritDoc}
   */
  public DataPageList getContactPageListByTag(String username, String tagId) throws Exception {
    Map<String, Contact> contacts = new LinkedHashMap<String, Contact>();
    QueryResult result = null;
    NodeIterator it = null;
    Query query = null;
    QueryManager qm = null;
    StringBuffer queryString = null;

    // query on public contacts
    Node contactHome = getPersonalContactsHome(username);
    qm = contactHome.getSession().getWorkspace().getQueryManager();
    queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("']");
    query = qm.createQuery(queryString.toString(), Query.XPATH);
    result = query.execute();
    it = result.getNodes();

    while (it.hasNext()) {
      Contact contact = Utils.getContact(it.nextNode(), PERSONAL);
      contacts.put(contact.getId(), contact);
    }

    // query on public contacts
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    Node publicContactHome = getPublicContactsHome();
    qm = publicContactHome.getSession().getWorkspace().getQueryManager();
    queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("' and @exo:isOwner='true'] ");
    query = qm.createQuery(queryString.toString(), Query.XPATH);
    result = query.execute();
    it = result.getNodes();
    while (it.hasNext()) {
      Contact contact = Utils.getContact(it.nextNode(), PUBLIC);
      if (contact.getId().equals(username))
        contact.setContactType(PERSONAL);
      contacts.put(contact.getId(), contact);
    }

    // query on shared contacts
    try {
      Node sharedContact = getSharedContact(username);
      PropertyIterator iter = sharedContact.getReferences();
      while (iter.hasNext()) {
        try {
          Node contactNode = iter.nextProperty().getParent();
          if (contactNode.hasProperty("exo:tags") && Arrays.asList(Utils.valuesToStringArray(contactNode.getProperty("exo:tags").getValues())).contains(tagId)) {
            Contact contact = Utils.getContact(contactNode, SHARED);
            contacts.put(contact.getId(), contact);
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Exception in method saveSharedContact", e);
          }
        }
      }
    } catch (PathNotFoundException e) {
    }
    // Query shared contacts
    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookMock.getReferences();
    Node addressBook;
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
      queryString = new StringBuffer("/jcr:root" + contactHomeNode.getPath() + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("']");

      // CS-3644
      // qm = contactHomeNode.getSession().getWorkspace().getQueryManager();
      qm = sharedAddressBookMock.getSession().getWorkspace().getQueryManager();
      query = qm.createQuery(queryString.toString(), Query.XPATH);
      result = query.execute();
      it = result.getNodes();
      while (it.hasNext()) {
        Contact contact = Utils.getContact(it.nextNode(), SHARED);
        if (Arrays.asList(contact.getAddressBookIds()).contains(addressBook.getProperty("exo:id").getString()))
          contacts.put(contact.getId(), contact);
      }
    }
    return new DataPageList(Arrays.asList(contacts.values().toArray(new Contact[] {})), 10, null, false);
  }

  private Node getContactNode(String username, String contact) throws Exception {
    String contactId = contact.split(SPLIT)[0];
    String contactType = contact.split(SPLIT)[1];
    if (contactType.equals(PERSONAL)) {
      return getPersonalContactsHome(username).getNode(contactId);
    } else if (contactType.equals(PUBLIC)) {
      return getPersonalContactsHome(contactId).getNode(contactId);
    } else {
      Node contactNode = null;
      Node sharedContactMock = getSharedContact(username);
      PropertyIterator iter = sharedContactMock.getReferences();
      while (iter.hasNext()) {
        try {
          Node node = iter.nextProperty().getParent();
          if (node.getName().equals(contactId)) {
            contactNode = node;
            break;
          }
        } catch (Exception exx) {
          if (log.isDebugEnabled()) {
            log.debug("Exception in method getContactNode", exx);
          }
        }
      }
      if (contactNode == null) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter1 = sharedAddressBookMock.getReferences();
        Node addressBook;
        while (iter1.hasNext()) {
          addressBook = iter1.nextProperty().getParent();
          Node contacts = addressBook.getParent().getParent().getNode(CONTACTS);
          if (contacts.hasNode(contactId)) {
            contactNode = contacts.getNode(contactId);
            if (Arrays.asList(Utils.valuesToStringArray(contactNode.getProperty(PROP_ADDRESSBOOK_REFS).getValues())).contains(addressBook.getProperty("exo:id").getString()))
              break;
            else {
              contactNode = null;
            }
          }
        }
      }
      return contactNode;
    }
  }

  /**
   * {@inheritDoc}
   */
  public void addTag(String username, List<String> contactIds, String tagId) throws Exception {
    Map<String, String> tagMap = new HashMap<String, String>();
    tagMap.put(tagId, tagId);
    for (String contact : contactIds) {
      Node contactNode = getContactNode(username, contact);
      if (contactNode == null) {
        throw new PathNotFoundException();
      } else {
        Map<String, String> thisTagMap = new HashMap<String, String>();
        thisTagMap = tagMap;
        Value[] values = null;
        if (contactNode.hasProperty("exo:tags")) {
          values = contactNode.getProperty("exo:tags").getValues();
          for (Value value : values)
            thisTagMap.put(value.getString(), value.getString());
        }
        contactNode.setProperty("exo:tags", thisTagMap.values().toArray(new String[] {}));
        contactNode.save();
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void addTag(String username, List<String> contactIds, List<Tag> tags) throws Exception {
    Node tagHomeNode = getTagsHome(username);
    Map<String, String> tagMap = new HashMap<String, String>();
    String newTag = null;
    for (Tag tag : tags) {
      if (!tagHomeNode.hasNode(tag.getId())) {
        newTag = tag.getId();
        Node tagNode = tagHomeNode.addNode(tag.getId(), "exo:contactTag");
        tagNode.setProperty("exo:id", tag.getId());
        tagNode.setProperty("exo:name", tag.getName());
        tagNode.setProperty("exo:description", tag.getDescription());
        tagNode.setProperty("exo:color", tag.getColor());
        tagHomeNode.getSession().save();
      }
      tagMap.put(tag.getId(), tag.getId());
    }
    if (contactIds == null || contactIds.size() == 0)
      return;
    for (String contact : contactIds) {
      Node contactNode = getContactNode(username, contact);
      if (contactNode == null) {
        if (contactIds.get(0).equals(contact) && (newTag != null)) {
          tagHomeNode.getNode(newTag).remove();
          tagHomeNode.getSession().save();
        }
        throw new PathNotFoundException();
      } else {
        Map<String, String> thisTagMap = new HashMap<String, String>();
        thisTagMap.putAll(tagMap);
        Value[] values = null;
        if (contactNode.hasProperty("exo:tags")) {
          values = contactNode.getProperty("exo:tags").getValues();
          for (Value value : values)
            thisTagMap.put(value.getString(), value.getString());
        }
        contactNode.setProperty("exo:tags", thisTagMap.values().toArray(new String[] {}));
        contactNode.save();
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void removeTagInContacts(NodeIterator it, String tagId) throws Exception {
    while (it.hasNext()) {
      Node contactNode = it.nextNode();
      if (contactNode.hasProperty("exo:tags")) {
        String[] tagIds = Utils.valuesToStringArray(contactNode.getProperty("exo:tags").getValues());
        List<String> newTagIds = new ArrayList<String>();
        for (String id : tagIds)
          if (!id.equals(tagId))
            newTagIds.add(id);
        contactNode.setProperty("exo:tags", newTagIds.toArray(new String[] {}));
        contactNode.save();
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public Tag removeTag(String username, String tagId) throws Exception {
    Node tagHomeNode = getTagsHome(username);
    Node tagNode = tagHomeNode.getNode(tagId);
    Tag tag = getTag(tagNode);
    tagNode.remove();
    tagHomeNode.save();

    // remove tagId in contact property
    // query on public contacts
    Node contactHome = getPersonalContactsHome(username);
    QueryManager qm = contactHome.getSession().getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("']");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    removeTagInContacts(query.execute().getNodes(), tagId);

    // query on public contacts
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    try {
      Node publicContactHome = getPublicContactsHome();
      qm = publicContactHome.getSession().getWorkspace().getQueryManager();
      queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("' and @exo:isOwner='true'] ");
      query = qm.createQuery(queryString.toString(), Query.XPATH);
      removeTagInContacts(query.execute().getNodes(), tagId);
    } finally {
      // sysProvider.close();
    }

    // query on shared contacts
    try {
      Node sharedContact = getSharedContact(username);
      PropertyIterator iter = sharedContact.getReferences();
      while (iter.hasNext()) {
        try {
          Node contactNode = iter.nextProperty().getParent();
          if (contactNode.hasProperty("exo:tags")) {
            String[] tagIds = Utils.valuesToStringArray(contactNode.getProperty("exo:tags").getValues());
            List<String> newTagIds = new ArrayList<String>();
            for (String id : tagIds)
              if (!id.equals(tagId))
                newTagIds.add(id);
            contactNode.setProperty("exo:tags", newTagIds.toArray(new String[] {}));
            contactNode.save();
          }
        } catch (Exception e) {
          if (log.isDebugEnabled()) {
            log.debug("Exception in method removeTag", e);
          }
        }
      }
    } catch (PathNotFoundException e) {
    }

    Node sharedAddressBookMock = getSharedAddressBooksHome(username);
    PropertyIterator iter = sharedAddressBookMock.getReferences();
    qm = sharedAddressBookMock.getSession().getWorkspace().getQueryManager();
    Node addressBook;
    while (iter.hasNext()) {
      addressBook = iter.nextProperty().getParent();
      Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
      queryString = new StringBuffer("/jcr:root" + contactHomeNode.getPath() + "//element(*,exo:contact)[@exo:tags='").append(tagId).append("']");
      query = qm.createQuery(queryString.toString(), Query.XPATH);
      removeTagInContacts(query.execute().getNodes(), tagId);
    }
    return tag;
  }

  /**
   * {@inheritDoc}
   */
  public void removeContactTag(String username, List<String> contactIds, List<String> tags) throws Exception {
    for (String contact : contactIds) {
      Node contactNode = getContactNode(username, contact);
      if (contactNode == null) {
        throw new PathNotFoundException();
      } else {
        if (contactNode.hasProperty("exo:tags")) {
          Value[] values = contactNode.getProperty("exo:tags").getValues();
          List<String> tagList = new ArrayList<String>();
          for (Value value : values) {
            tagList.add(value.getString());
          }
          for (String tag : tags) {
            if (tagList.contains(tag))
              tagList.remove(tag);
          }
          contactNode.setProperty("exo:tags", tagList.toArray(new String[] {}));
          contactNode.save();
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public DataPageList searchContact(String username, ContactFilter filter) throws Exception {
    Map<String, Contact> contacts = new LinkedHashMap<String, Contact>();
    filter.setUsername(username);
    QueryManager qm = getSession(createSystemProvider()).getWorkspace().getQueryManager();
    Query query = null;
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    // public contacts
    if (filter.getType() == null || filter.getType().equals(PUBLIC)) {
      // Node publicContactHome = getPublicContactsHome(sysProvider) ;
      usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
      filter.setAccountPath(usersPath);
      // minus shared contacts
      filter.setOwner("true");
      query = qm.createQuery(filter.getStatement(), Query.XPATH);
      NodeIterator itpublic = query.execute().getNodes();
      while (itpublic.hasNext()) {
        Contact contact = Utils.getContact(itpublic.nextNode(), PUBLIC);
        contacts.put(contact.getId(), contact);
      }
    }
    filter.setOwner(null);
    if (filter.getType() == null || filter.getType().equals(PERSONAL)) {
      // public contacts
      if (username != null && username.length() > 0) {
        Node contactHome = getPersonalContactsHome(username);
        filter.setAccountPath(contactHome.getPath());
        qm = contactHome.getSession().getWorkspace().getQueryManager();
        query = qm.createQuery(filter.getStatement(), Query.XPATH);
        NodeIterator it = query.execute().getNodes();
        while (it.hasNext()) {
          Contact contact = Utils.getContact(it.nextNode(), PERSONAL);
          contacts.put(contact.getId(), contact);
        }
      }
    }

    if (filter.getType() == null || filter.getType().equals(SHARED)) {
      // share contacts
      try {
        Node sharedContact = getSharedContact(username);
        PropertyIterator iter = sharedContact.getReferences();
        List<Contact> sharedContacts = getSharedContacts(username).getAll();
        List<String> sharedContactIds = new ArrayList<String>();
        for (Contact contact : sharedContacts)
          sharedContactIds.add(contact.getId());
        while (iter.hasNext()) {
          try {
            Node sharedContactHomeNode = iter.nextProperty().getParent().getParent();
            filter.setAccountPath(sharedContactHomeNode.getPath());

            // add
            String split = "/";
            String temp = sharedContactHomeNode.getPath().split(usersPath)[1];
            String userId = temp.split(split)[1];
            filter.setUsername(userId);
            // qm = getSession(sysProvider).getWorkspace().getQueryManager() ;
            query = qm.createQuery(filter.getStatement(), Query.XPATH);
            NodeIterator it = query.execute().getNodes();
            while (it.hasNext()) {
              Node contactNode = it.nextNode();
              if (sharedContactIds.contains(contactNode.getProperty("exo:id").getString())) {
                Contact contact = Utils.getContact(contactNode, SHARED);

                // add
                if (!contacts.containsKey(contact.getId())) {
                  contacts.put(contact.getId(), contact);
                } else {
                  contacts.get(contact.getId()).setContactType(DataStorage.SHARED);
                }
                sharedContactIds.remove(contact.getId());
              }
            }
          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Exception in method searchContact", e);
            }
          }
        }
      } catch (PathNotFoundException e) {
      }

      if (filter.isSearchSharedContacts() == false) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter = sharedAddressBookMock.getReferences();
        Node addressBook;
        boolean searchByAddress = false;
        if (filter.getCategories() != null && filter.getCategories().length > 0)
          searchByAddress = true;
        while (iter.hasNext()) {
          addressBook = iter.nextProperty().getParent();
          Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
          filter.setAccountPath(contactHomeNode.getPath());

          // avoid getAllContacts from contactHomeNode of another user.
          if (!searchByAddress)
            filter.setCategories(new String[] { addressBook.getName() });
          filter.setUsername(addressBook.getProperty("exo:sharedUserId").getString());
          // qm = getSession(sysProvider).getWorkspace().getQueryManager();
          qm = contactHomeNode.getSession().getWorkspace().getQueryManager();
          query = qm.createQuery(filter.getStatement(), Query.XPATH);
          NodeIterator it = query.execute().getNodes();
          while (it.hasNext()) {
            Contact contact = Utils.getContact(it.nextNode(), SHARED);
            contacts.put(contact.getId(), contact);
          }
        }
      }
    }

    List<Contact> contactList = new ArrayList<Contact>();
    String tagId = "";

    // get contact by tag
    if (filter.getTag() != null) {
      if (filter.getTag().length > 0) {
        tagId = filter.getTag()[0];
      }
      if (!Utils.isEmpty(tagId)) {
        DataPageList dpl = getContactPageListByTag(username, tagId);
        contactList = dpl.getAll();
      }
    }

    contactList.addAll(contacts.values());

    return new DataPageList(contactList, 10, null, false);
  }

  /**
   * {@inheritDoc}
   */
  public Map<String, String> findEmailsByFilter(String username, ContactFilter filter) throws Exception {
    Map<String, String> emails = new LinkedHashMap<String, String>();
    filter.setUsername(username);
    filter.setHasEmails(true);
    QueryManager qm = getContactUserDataHome(username).getSession().getWorkspace().getQueryManager();
    Query query;
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    // query public contacts
    if (filter.getType() == null || filter.getType().equals(PUBLIC)) {
      filter.setAccountPath(usersPath);
      // minus shared contacts
      filter.setOwner("true");

      query = qm.createQuery(filter.getStatement(), Query.XPATH);
      NodeIterator itpublic = query.execute().getNodes();
      while (itpublic.hasNext()) {
        Node contactNode = itpublic.nextNode();
        if (filter.getLimit() > 0 && filter.getLimit() <= emails.size())
          break;
        feedEmailResult(emails, contactNode);
      }
      filter.setOwner(null);
    }
    // query personal contacts
    if (filter.getType() == null || filter.getType().equals(PERSONAL)) {
      if (username != null && username.length() > 0) {
        Node contactHome = getPersonalContactsHome(username);
        filter.setAccountPath(contactHome.getPath());
        qm = contactHome.getSession().getWorkspace().getQueryManager();
        query = qm.createQuery(filter.getStatement(), Query.XPATH);
        NodeIterator it = query.execute().getNodes();
        while (it.hasNext()) {
          Node contactNode = it.nextNode();
          if (filter.getLimit() > 0 && filter.getLimit() <= emails.size())
            break;
          feedEmailResult(emails, contactNode);
        }
      }
    }

    // query shared contacts
    if (filter.getType() == null || filter.getType().equals(SHARED)) {
      try {
        Node sharedContact = getSharedContact(username);
        PropertyIterator iter = sharedContact.getReferences();
        while (iter.hasNext()) {
          try {
            Node sharedContactHomeNode = iter.nextProperty().getParent().getParent();
            filter.setAccountPath(sharedContactHomeNode.getPath());

            String split = "/";
            String temp = sharedContactHomeNode.getPath().split(usersPath)[1];
            String userId = temp.split(split)[1];
            filter.setUsername(userId);

            qm = sharedContactHomeNode.getSession().getWorkspace().getQueryManager();
            query = qm.createQuery(filter.getStatement(), Query.XPATH);
            NodeIterator it = query.execute().getNodes();
            while (it.hasNext()) {
              Node contactNode = it.nextNode();
              if (filter.getLimit() > 0 && filter.getLimit() <= emails.size())
                break;
              feedEmailResult(emails, contactNode);
            }
          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Exception in method findEmailsByFilter", e);
            }
          }
        }
      } catch (PathNotFoundException e) {
      }

      if (!filter.isSearchSharedContacts()) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter = sharedAddressBookMock.getReferences();
        Node addressBook;

        // TODO add if to fix bug 1407
        boolean hasGroup = (filter.getCategories() != null && filter.getCategories().length > 0);
        while (iter.hasNext()) {
          addressBook = iter.nextProperty().getParent();
          Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
          filter.setAccountPath(contactHomeNode.getPath());
          if (!hasGroup)
            filter.setCategories(new String[] { addressBook.getName() });
          filter.setUsername(addressBook.getProperty("exo:sharedUserId").getString());
          // qm = getSession(sysProvider).getWorkspace().getQueryManager();
          qm = contactHomeNode.getSession().getWorkspace().getQueryManager();
          query = qm.createQuery(filter.getStatement(), Query.XPATH);
          NodeIterator it = query.execute().getNodes();
          while (it.hasNext()) {
            Node contactNode = it.nextNode();
            if (filter.getLimit() > 0 && filter.getLimit() <= emails.size())
              break;
            feedEmailResult(emails, contactNode);
          }
        }
      }
    }
    return emails;
  }

  /**
   * {@inheritDoc}
   */
  public void feedEmailResult(Map<String, String> emails, Node contactNode) throws Exception {
    String id = contactNode.getProperty("exo:id").getString();
    String fullName = contactNode.getProperty("exo:fullName").getString();
    String emailAddresses = Utils.valuesToString(contactNode.getProperty("exo:emailAddress").getValues());
    emails.put(id, fullName + Utils.SPLIT + emailAddresses);
  }

  // created by Duy Tu <tu.duy@exoplatform.com>
  public List<String> searchEmailsByFilter(String username, ContactFilter filter) throws Exception {
    Map<String, String> emails = new LinkedHashMap<String, String>();
    filter.setUsername(username);
    filter.setHasEmails(true);
    QueryManager qm = getContactUserDataHome(username).getSession().getWorkspace().getQueryManager();
    Query query;
    String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
    // query public contacts
    if (filter.getType() == null || filter.getType().equals(PUBLIC)) {
      filter.setAccountPath(usersPath);
      // minus shared contacts
      filter.setOwner("true");

      query = qm.createQuery(filter.getStatement(), Query.XPATH);
      NodeIterator itpublic = query.execute().getNodes();
      while (itpublic.hasNext()) {
        Node contactNode = itpublic.nextNode();
        if (filter.getLimit() > 0 && filter.getLimit() <= emails.size()) {
          return new ArrayList<String>(emails.values());
        }
        calculateEmailResult(contactNode, filter, emails);
      }
      filter.setOwner(null);
    }

    // query personal contacts
    if (filter.getType() == null || filter.getType().equals(PERSONAL)) {
      if (username != null && username.length() > 0) {
        Node contactHome = getPersonalContactsHome(username);
        filter.setAccountPath(contactHome.getPath());
        qm = contactHome.getSession().getWorkspace().getQueryManager();
        query = qm.createQuery(filter.getStatement(), Query.XPATH);
        NodeIterator it = query.execute().getNodes();
        while (it.hasNext()) {
          Node contactNode = it.nextNode();
          if (filter.getLimit() > 0 && filter.getLimit() <= emails.size()) {
            return new ArrayList<String>(emails.values());
          }
          calculateEmailResult(contactNode, filter, emails);
        }
      }
    }
    // query shared contacts
    if (filter.getType() == null || filter.getType().equals(SHARED)) {
      try {
        Node sharedContact = getSharedContact(username);
        PropertyIterator iter = sharedContact.getReferences();
        while (iter.hasNext()) {
          try {
            Node sharedContactHomeNode = iter.nextProperty().getParent().getParent();
            filter.setAccountPath(sharedContactHomeNode.getPath());

            String split = "/";
            String temp = sharedContactHomeNode.getPath().split(usersPath)[1];
            String userId = temp.split(split)[1];
            filter.setUsername(userId);

            qm = sharedContactHomeNode.getSession().getWorkspace().getQueryManager();
            query = qm.createQuery(filter.getStatement(), Query.XPATH);
            NodeIterator it = query.execute().getNodes();
            while (it.hasNext()) {
              Node contactNode = it.nextNode();
              if (filter.getLimit() > 0 && filter.getLimit() <= emails.size()) {
                return new ArrayList<String>(emails.values());
              }
              calculateEmailResult(contactNode, filter, emails);
            }
          } catch (Exception e) {
            if (log.isDebugEnabled()) {
              log.debug("Exception in method searchEmailsByFilter", e);
            }
          }
        }
      } catch (PathNotFoundException e) {
      }

      if (!filter.isSearchSharedContacts()) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter = sharedAddressBookMock.getReferences();
        Node addressBook;

        // add if to fix bug 1407
        boolean hasGroup = (filter.getCategories() != null && filter.getCategories().length > 0);
        while (iter.hasNext()) {
          addressBook = iter.nextProperty().getParent();
          Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
          filter.setAccountPath(contactHomeNode.getPath());
          if (!hasGroup)
            filter.setCategories(new String[] { addressBook.getName() });
          filter.setUsername(addressBook.getProperty("exo:sharedUserId").getString());
          qm = contactHomeNode.getSession().getWorkspace().getQueryManager();
          query = qm.createQuery(filter.getStatement(), Query.XPATH);
          NodeIterator it = query.execute().getNodes();
          while (it.hasNext()) {
            Node contactNode = it.nextNode();
            if (filter.getLimit() > 0 && filter.getLimit() <= emails.size()) {
              return new ArrayList<String>(emails.values());
            }
            calculateEmailResult(contactNode, filter, emails);
          }
        }
      }
    }
    return new ArrayList<String>(emails.values());
  }

  // created by Duy Tu
  private void calculateEmailResult(Node contactNode, ContactFilter filter, Map<String, String> emails) throws Exception {
    String emailAddresses = "";
    String fullName = contactNode.getProperty("exo:fullName").getString();
    try {
      Value[] values = contactNode.getProperty("exo:emailAddress").getValues();
      int i = 0;
      String email, classCss;
      for (Value value : values) {
        email = value.getString().trim();
        if (email.length() > 0) {
          if (emails.containsKey(fullName + email))
            continue;
          if (i > 2 || filter.getLimit() <= emails.size())
            break;
          classCss = (emails.isEmpty()) ? "<div class='AutoCompleteItem AutoCompleteOver'>" : "<div class='AutoCompleteItem'>";
          emailAddresses = classCss + StringUtils.replace(fullName, filter.getNickName(), "<b>" + filter.getNickName() + "</b>") + "&lt;" + StringUtils.replace(email, filter.getNickName(), "<b>" + filter.getNickName() + "</b>") + "&gt;" + "</div>";
          emails.put(fullName + email, emailAddresses);
          ++i;
        }
      }
    } catch (Exception e) {
    }
  }

  /**
   * {@inheritDoc}
   */
  public void copyNodes(String username, Node srcHomeNode, NodeIterator iter, String destAddress, String destType) throws Exception {
    if (destType.equals(PERSONAL)) {
      Node contactHomeNode = getPersonalContactsHome(username);
      while (iter.hasNext()) {
        Node oldNode = iter.nextNode();
        String newId = "Contact" + IdGenerator.generate();
        try {
          contactHomeNode.getSession().getWorkspace().copy(oldNode.getPath(), contactHomeNode.getPath() + "/" + newId);
        } catch (AccessDeniedException ex) {
          Node userContactHome = getPersonalContactsHome(oldNode.getProperty("exo:id").getString());
          userContactHome.getSession().getWorkspace().copy(oldNode.getPath(), contactHomeNode.getPath() + "/" + newId);
        }
        ExtendedNode extNode;
        try {
          extNode = (ExtendedNode) contactHomeNode.getNode(newId);
        } catch (Exception e) {
          extNode = (ExtendedNode) getPersonalContactsHome(username).getNode(newId);
        }
        if (extNode.canAddMixin("exo:privilegeable"))
          extNode.addMixin("exo:privilegeable");
        String[] arrayPers = { PermissionType.READ, PermissionType.ADD_NODE, PermissionType.SET_PROPERTY, PermissionType.REMOVE };
        extNode.setPermission(username, arrayPers);
        extNode.save();

        createContactNote(destAddress, contactHomeNode, newId);
      }
      contactHomeNode.getSession().save();
    } else if (destType.equals(SHARED)) {
      Node sharedAddressBookMock = getSharedAddressBooksHome(username);
      PropertyIterator proIter = sharedAddressBookMock.getReferences();
      Node addressBook;
      while (proIter.hasNext()) {
        addressBook = proIter.nextProperty().getParent();
        if (addressBook.getName().equals(destAddress)) {
          Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
          while (iter.hasNext()) {
            String newId = "Contact" + IdGenerator.generate();
            Node oldNode = iter.nextNode();
            contactHomeNode.getSession().getWorkspace().copy(srcHomeNode.getPath() + "/" + oldNode.getProperty("exo:id").getString(), contactHomeNode.getPath() + "/" + newId);
            createContactNote(destAddress, contactHomeNode, newId);
          }
          contactHomeNode.getSession().save();
          break;
        }
      }
    }
  }

  private void createContactNote(String destAddress, Node contactHomeNode, String newId) throws Exception {
    Node newNode = contactHomeNode.getNode(newId);
    newNode.setProperty(PROP_ADDRESSBOOK_REFS, new String[] { destAddress });
    newNode.setProperty("exo:id", newId);
    newNode.setProperty("exo:isOwner", false);

    newNode.setProperty("exo:editPermissionUsers", new String[] {});
    newNode.setProperty("exo:viewPermissionUsers", new String[] {});
    newNode.setProperty("exo:editPermissionGroups", new String[] {});
    newNode.setProperty("exo:viewPermissionGroups", new String[] {});
  }

  /**
   * {@inheritDoc} 
   */
  public void pasteAddressBook(String username, String srcAddress, String srcType, String destAddress, String destType) throws Exception {
    // CS-2389
    if (destType.equals(SHARED) && !haveEditPermissionOnAddressBook(username, destAddress)) {
      throw new AccessDeniedException();
    }
    if (srcType.equals(PERSONAL)) {
      Node contactHome = getPersonalContactsHome(username);
      QueryManager qm = contactHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:categories='").append(srcAddress).append("']");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      copyNodes(username, contactHome, iter, destAddress, destType);
    } else if (srcType.equals(SHARED)) {
      Node sharedAddressBookMock = getSharedAddressBooksHome(username);
      PropertyIterator proIter = sharedAddressBookMock.getReferences();
      QueryManager qm = sharedAddressBookMock.getSession().getWorkspace().getQueryManager();
      Node addressBook;
      while (proIter.hasNext()) {
        addressBook = proIter.nextProperty().getParent();
        if (addressBook.getName().equals(srcAddress)) {
          Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
          StringBuffer queryString = new StringBuffer("/jcr:root" + contactHomeNode.getPath() + "//element(*,exo:contact)[@exo:categories='").append(srcAddress).append("']");
          Query query = qm.createQuery(queryString.toString(), Query.XPATH);
          QueryResult result = query.execute();
          NodeIterator iter = result.getNodes();
          copyNodes(username, contactHomeNode, iter, destAddress, destType);
          break;
        }
      }
    } else {
      String usersPath = nodeHierarchyCreator_.getJcrPath(USERS_PATH);
      Node publicContactHome = getPublicContactsHome();
      QueryManager qm = publicContactHome.getSession().getWorkspace().getQueryManager();
      StringBuffer queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:categories='").append(srcAddress).append("']");
      Query query = qm.createQuery(queryString.toString(), Query.XPATH);
      QueryResult result = query.execute();
      NodeIterator iter = result.getNodes();
      copyNodes(username, publicContactHome, iter, destAddress, destType);
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Contact> pasteContacts(String username, String destAddress, String destType, Map<String, String> contactsMap) throws Exception {
    // CS-2389
    if (destType.equals(SHARED) && !haveEditPermissionOnAddressBook(username, destAddress)) {
      throw new AccessDeniedException();
    }
    List<Contact> contacts = new ArrayList<Contact>();
    List<Contact> pastedContacts = new ArrayList<Contact>();
    for (String contactId : contactsMap.keySet()) {
      String type = contactsMap.get(contactId);
      Contact contact = null;
      if (type.equals(PERSONAL)) {
        contact = loadPersonalContact(username, contactId);
      } else if (type.equals(PUBLIC)) {
        contact = loadPublicContactByUser(contactId);
      } else { // test here
        contact = getSharedContact(username, contactId);
        if (contact == null)
          contact = getSharedContactAddressBook(username, contactId);
        // CS-2389
        if (contact != null && !haveEditPermissionOnContact(username, contact) && !haveEditPermissionOnAddressBook(username, contact.getAddressBookIds()[0])) {
          log.error("do not have permission");
          throw new AccessDeniedException();
        }
      }
      if (contact != null)
        contacts.add(contact);
    }

    for (Contact contact : contacts) {
      if (destType.equals(PERSONAL)) {
        Node contactHomeNode = getPersonalContactsHome(username);
        pastedContacts.add(Utils.getContact(saveCopyContact(contactHomeNode, contact, destAddress, destType), destType));
      } else if (destType.equals(SHARED)) {
        Node sharedAddressBookMock = getSharedAddressBooksHome(username);
        PropertyIterator iter = sharedAddressBookMock.getReferences();
        Node addressBook;
        while (iter.hasNext()) {
          addressBook = iter.nextProperty().getParent();
          if (addressBook.getName().equals(destAddress)) {
            Node contactHomeNode = addressBook.getParent().getParent().getNode(CONTACTS);
            pastedContacts.add(Utils.getContact(saveCopyContact(contactHomeNode, contact, destAddress, destType), destType));
            break;
          }
        }
      }
    }
    return pastedContacts;
  }

  /**
   * {@inheritDoc}
   */
  public Node saveCopyContact(Node contactHomeNode, Contact contact, String destAddress, String destType) throws Exception {
    String newId = "Contact" + IdGenerator.generate();
    Node contactNode = contactHomeNode.addNode(newId, "exo:contact");
    contactNode.setProperty("exo:id", newId);
    contact.setAddressBookIds(new String[] { destAddress });
    contact.setViewPermissionUsers(null);
    contact.setViewPermissionGroups(null);
    contact.setEditPermissionUsers(null);
    contact.setEditPermissionGroups(null);
    contactNode = setContactNodeProperties(contactNode, contact);
    contactHomeNode.getSession().save();
    return contactNode;
  }

  /**
   * {@inheritDoc}
   */
  public void registerNewUser(User user, boolean isNew) throws Exception {
    Contact contact = null;
    if (isNew)
      contact = new Contact();
    else
      contact = loadPublicContactByUser(user.getUserName());
    if (contact != null) {
      contact.setFullName(user.getFirstName() + " " + user.getLastName());
      contact.setFirstName(user.getFirstName());
      contact.setLastName(user.getLastName());
      contact.setEmailAddress(user.getEmail());
      Calendar cal = new GregorianCalendar();
      contact.setLastUpdated(cal.getTime());
    }
    if (isNew) {
      Node addressHome = getPersonalAddressBooksHome(user.getUserName());

      AddressBook addressbook = new AddressBook();
      addressbook.setId(NewUserListener.DEFAULTGROUP + user.getUserName());
      addressbook.setName(NewUserListener.DEFAULTGROUPNAME);
      addressbook.setDescription(NewUserListener.DEFAULTGROUPDES);
      Node groupNode = addressHome.addNode(addressbook.getId(), "exo:contactGroup");
      groupNode.setProperty("exo:id", addressbook.getId());
      groupNode.setProperty("exo:name", addressbook.getName());
      groupNode.setProperty("exo:description", addressbook.getDescription());
      groupNode.setProperty("exo:editPermissionUsers", addressbook.getEditPermissionUsers());
      groupNode.setProperty("exo:viewPermissionUsers", addressbook.getViewPermissionUsers());
      groupNode.setProperty("exo:editPermissionGroups", addressbook.getEditPermissionGroups());
      groupNode.setProperty("exo:viewPermissionGroups", addressbook.getViewPermissionGroups());

      // TODO cs-1141
      Node addressesGroup = addressHome.addNode(NewUserListener.ADDRESSESGROUP + user.getUserName(), "exo:contactGroup");
      addressesGroup.setProperty("exo:id", NewUserListener.ADDRESSESGROUP + user.getUserName());
      addressesGroup.setProperty("exo:name", NewUserListener.ADDRESSESGROUPNAME);
      addressHome.getSession().save();

      // save contact
      contact.setId(user.getUserName());
      Set<String> groupIds = new HashSet<String>();
      groupIds.add(addressbook.getId());
      ExoContainer container = ExoContainerContext.getCurrentContainer();
      OrganizationService organizationService = (OrganizationService) container.getComponentInstanceOfType(OrganizationService.class);
      Object[] groupsOfUser = organizationService.getGroupHandler().findGroupsOfUser(user.getUserName()).toArray();
      for (Object object : groupsOfUser) {
        String id = ((Group) object).getId();
        groupIds.add(id);
      }

      contact.setAddressBookIds(groupIds.toArray(new String[] {}));
      contact.setOwner(true);
      contact.setOwnerId(user.getUserName());
      saveContactUser(user.getUserName(), contact, true);
      QueryManager qm = getSession(createSystemProvider()).getWorkspace().getQueryManager();
      String usersPath = nodeHierarchyCreator_.getJcrPath(DataStorage.USERS_PATH);
      List<String> recievedUser = new ArrayList<String>();
      recievedUser.add(user.getUserName());
      for (Object object : groupsOfUser) {
        String groupId = ((Group) object).getId();
        // get all address books that current user can see thank to his groups
        StringBuffer queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contactGroup)[@exo:viewPermissionGroups='").append(groupId + "']");
        Query query = qm.createQuery(queryString.toString(), Query.XPATH);
        QueryResult result = query.execute();
        NodeIterator nodes = result.getNodes();
        while (nodes.hasNext()) {
          Node addressBook = nodes.nextNode();
          // share between adressbook owner and current user
          shareAddressBook(addressBook.getProperty("exo:sharedUserId").getString(), addressBook.getProperty("exo:id").getString(), recievedUser);
        }

        // lookup shared contacts that user can see thank to his groups
        queryString = new StringBuffer("/jcr:root" + usersPath + "//element(*,exo:contact)[@exo:viewPermissionGroups='").append(groupId + "']");
        query = qm.createQuery(queryString.toString(), Query.XPATH);
        result = query.execute();
        nodes = result.getNodes();
        while (nodes.hasNext()) {
          Node contactNode = nodes.nextNode();
          String split = "/";
          String temp = contactNode.getPath().split(usersPath)[1];
          String userId = temp.split(split)[1];
          shareContact(userId, new String[] { contactNode.getProperty("exo:id").getString() }, recievedUser);
        }
      }
      Node userApp = nodeHierarchyCreator_.getUserApplicationNode(createSystemProvider(), user.getUserName());
      userApp.getSession().save();
    } else {
      if (contact != null) {
        saveContactUser(user.getUserName(), contact, false);
      }
    }
  }

  /**`
   * {@inheritDoc}
   */
  public SessionProvider createSystemProvider() {
    ExoContainer container = ExoContainerContext.getCurrentContainer();
    SessionProviderService service = (SessionProviderService) container.getComponentInstanceOfType(SessionProviderService.class);
    return service.getSystemSessionProvider(null);
  }

  /**
   * {@inheritDoc}
   */
  public void saveAddress(String username, String emailAddress) throws Exception {
    ContactFilter filter = new ContactFilter();
    for (String email : Utils.parseEmails(emailAddress)) {
      filter.setEmailAddress(email);
      if (searchContact(username, filter).getAll().size() == 0) {
        Contact contact = new Contact();
        String name = email.split("@")[0];
        contact.setLastName(name);
        contact.setFullName(name);
        contact.setEmailAddress(email);
        contact.setAddressBookIds(new String[] { NewUserListener.ADDRESSESGROUP + username });
        saveContactUser(username, contact, true);
      }
    }
  }

  public Node getNodeByPath(String nodePath, SessionProvider sessionProvider) throws Exception {
    return (Node) getSession(sessionProvider).getItem(nodePath);
  }

  /**
   * {@inheritDoc}
   */
  public boolean haveEditPermissionOnAddressBook(String username, String addressBookId) throws Exception {
    AddressBook addressbook = getSharedAddressBookById(username, addressBookId);
    if (addressbook == null)
      return false;
    if (addressbook.getEditPermissionUsers() != null && Arrays.asList(addressbook.getEditPermissionUsers()).contains(username + DataStorage.HYPHEN)) {
      return true;
    }
    String[] editPerGroups = addressbook.getEditPermissionGroups();
    if (editPerGroups != null) {
      List<String> groupIds = new ArrayList<String>();
      ExoContainer container = ExoContainerContext.getCurrentContainer();
      OrganizationService organizationService = (OrganizationService) container.getComponentInstanceOfType(OrganizationService.class);
      Object[] groupsOfUser = organizationService.getGroupHandler().findGroupsOfUser(username).toArray();
      for (Object object : groupsOfUser) {
        String groupId = ((Group) object).getId();
        groupIds.add(groupId);
      }
      for (String editPer : editPerGroups)
        if (groupIds.contains(editPer))
          return true;
    }
    return false;
  }

  /**
   * {@inheritDoc}
   */
  public boolean haveEditPermissionOnContact(String username, Contact contact) throws Exception {
    if (contact.getEditPermissionUsers() != null && Arrays.asList(contact.getEditPermissionUsers()).contains(username + DataStorage.HYPHEN)) {
      return true;
    }
    List<String> groupIds = new ArrayList<String>();
    ExoContainer container = ExoContainerContext.getCurrentContainer();
    OrganizationService organizationService = (OrganizationService) container.getComponentInstanceOfType(OrganizationService.class);
    Object[] groupsOfUser = organizationService.getGroupHandler().findGroupsOfUser(username).toArray();
    for (Object object : groupsOfUser) {
      String groupId = ((Group) object).getId();
      groupIds.add(groupId);
    }
    String[] editPerGroups = contact.getEditPermissionGroups();
    if (editPerGroups != null)
      for (String editPer : editPerGroups)
        if (groupIds.contains(editPer)) {
          return true;
        }
    return false;
  }

  /**
   * {@inheritDoc}
   */
  public Session getSession(SessionProvider sprovider) throws Exception {
    ManageableRepository currentRepo = repoService_.getCurrentRepository();
    return sprovider.getSession(currentRepo.getConfiguration().getDefaultWorkspaceName(), currentRepo);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getContactApplicationDataHome() throws Exception {
    SessionProvider sProvider = createSystemProvider();
    Node applicationDataHome = getNodeByPath(nodeHierarchyCreator_.getPublicApplicationNode(sProvider).getPath(), sProvider);
    try {
      return applicationDataHome.getNode(CONTACT_APP);
    } catch (PathNotFoundException ex) {
      Node contactApplicationDataHome = applicationDataHome.addNode(CONTACT_APP, NT_UNSTRUCTURED);
      applicationDataHome.save();
      return contactApplicationDataHome;
    }
  }

  @Override
  public Node getContactUserDataHome(String username) throws Exception {
    SessionProvider sessionProvider = createSystemProvider();
    Node userDataHome = getNodeByPath(nodeHierarchyCreator_.getUserApplicationNode(sessionProvider, username).getPath(), sessionProvider);
    try {
      return userDataHome.getNode(CONTACT_APP);
    } catch (PathNotFoundException ex) {
      Node contactUserDataHome = userDataHome.addNode(CONTACT_APP, NT_UNSTRUCTURED);
      userDataHome.getSession().save();
      return contactUserDataHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getPersonalAddressBooksHome(String username) throws Exception {
    Node userDataHome = getContactUserDataHome(username);
    try {
      return userDataHome.getNode(PERSONAL_ADDRESS_BOOKS);
    } catch (PathNotFoundException ex) {
      Node padHome = userDataHome.addNode(PERSONAL_ADDRESS_BOOKS, NT_UNSTRUCTURED);
      userDataHome.save();
      return padHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getPersonalContactsHome(String username) throws Exception {
    Node userDataHome = getContactUserDataHome(username);
    try {
      return userDataHome.getNode(CONTACTS);
    } catch (PathNotFoundException ex) {
      Node personalContactsHome = userDataHome.addNode(CONTACTS, NT_UNSTRUCTURED);
      userDataHome.save();
      return personalContactsHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List<GroupContactData> getPublicContacts(String[] groupIds) throws Exception {
    List<GroupContactData> contactByGroup = new ArrayList<GroupContactData>();
    List<Contact> contacts;
    for (String groupId : groupIds) {
      contacts = getPublicContactsByAddressBook(groupId).getAll();
      if (contacts.size() > 0)
        contactByGroup.add(new GroupContactData(groupId, contacts));
    }
    return contactByGroup;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getPublicContactsHome() throws Exception {
    Node contactServiceHome = getContactApplicationDataHome();
    try {
      return contactServiceHome.getNode(CONTACTS);
    } catch (PathNotFoundException ex) {
      Node publicHome = contactServiceHome.addNode(CONTACTS, NT_UNSTRUCTURED);
      contactServiceHome.save();
      return publicHome;
    }
  }

  public Node getPublicAddressHome() throws Exception {
    Node contactServiceHome = getContactApplicationDataHome();
    try {
      return contactServiceHome.getNode(GROUP_ADDRESS_BOOKS);
    } catch (PathNotFoundException ex) {
      Node publicHome = contactServiceHome.addNode(GROUP_ADDRESS_BOOKS, NT_UNSTRUCTURED);
      contactServiceHome.save();
      return publicHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getTagsHome(String username) throws Exception {
    Node contactServiceHome = getContactUserDataHome(username);
    try {
      return contactServiceHome.getNode(TAGS);
    } catch (PathNotFoundException ex) {
      Node tagHome = contactServiceHome.addNode(TAGS, NT_UNSTRUCTURED);
      contactServiceHome.save();
      return tagHome;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List<String> getUserContactNodesByGroup(String username, String groupId) throws Exception {

    Node contactHome = getPersonalContactsHome(username);
    QueryManager qm = contactHome.getSession().getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:categories='").append(groupId).append("']");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator it = result.getNodes();
    List<String> contactIds = new ArrayList<String>();
    while (it.hasNext())
      contactIds.add(it.nextNode().getProperty("exo:id").getString());
    return contactIds;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean hasContacts(String groupId) throws Exception {
    Node contactHome = getPublicContactsHome();
    QueryManager qm = contactHome.getSession().getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer("/jcr:root" + contactHome.getPath() + "//element(*,exo:contact)[@exo:categories='").append(groupId).append("']");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    if (result.getNodes().getSize() > 0)
      return true;
    return false;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Node getSharedContactsHome(String user) throws Exception {
    Node contactHome = getContactUserDataHome(user);
    Node sharedHome;
    try {
      sharedHome = contactHome.getNode(SHARED_HOME);
    } catch (PathNotFoundException ex) {
      sharedHome = contactHome.addNode(SHARED_HOME, NT_UNSTRUCTURED);
      contactHome.save();
    }
    return sharedHome;
  }
}
