/*
 * Copyright (C) 2003-2011 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.exoplatform.social.core.storage.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;

import org.chromattic.api.query.Ordering;
import org.chromattic.api.query.QueryBuilder;
import org.chromattic.api.query.QueryResult;
import org.chromattic.core.query.QueryImpl;

import org.exoplatform.commons.notification.impl.AbstractService;
import org.exoplatform.commons.utils.CommonsUtils;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.social.core.chromattic.entity.DisabledEntity;
import org.exoplatform.social.core.chromattic.entity.IdentityEntity;
import org.exoplatform.social.core.chromattic.entity.ProfileEntity;
import org.exoplatform.social.core.chromattic.entity.RelationshipEntity;
import org.exoplatform.social.core.chromattic.entity.RelationshipListEntity;
import org.exoplatform.social.core.identity.model.Identity;
import org.exoplatform.social.core.identity.model.Profile;
import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
import org.exoplatform.social.core.manager.RelationshipManager;
import org.exoplatform.social.core.profile.ProfileFilter;
import org.exoplatform.social.core.profile.ProfileLoader;
import org.exoplatform.social.core.relationship.model.Relationship;
import org.exoplatform.social.core.storage.IdentityStorageException;
import org.exoplatform.social.core.storage.RelationshipStorageException;
import org.exoplatform.social.core.storage.api.ActivityStorage;
import org.exoplatform.social.core.storage.api.IdentityStorage;
import org.exoplatform.social.core.storage.api.RelationshipStorage;
import org.exoplatform.social.core.storage.cache.CachedActivityStorage;
import org.exoplatform.social.core.storage.exception.NodeNotFoundException;
import org.exoplatform.social.core.storage.query.JCRProperties;
import org.exoplatform.social.core.storage.query.WhereExpression;
import org.exoplatform.social.core.storage.streams.StreamInvocationHelper;

/**
 * @author <a href="mailto:alain.defrance@exoplatform.com">Alain Defrance</a>
 * @version $Revision$
 */
public class RelationshipStorageImpl extends AbstractStorage implements RelationshipStorage {

  /** Logger */
  private static final Log LOG = ExoLogger.getLogger(RelationshipStorage.class);

  private final IdentityStorage identityStorage;
  private RelationshipManager relationshipManager;
  private RelationshipStorage relationshipStorage;
  private ActivityStorage activityStorage;

  public RelationshipStorageImpl(IdentityStorage identityStorage) {
   this.identityStorage = identityStorage;
  }

  private enum Origin { FROM, TO }

  private RelationshipManager getRelationshipManager() {
    
    if (relationshipManager == null) {
      PortalContainer container = PortalContainer.getInstance();
      this.relationshipManager  = (RelationshipManager) container.getComponentInstanceOfType(RelationshipManager.class);
    }
    return relationshipManager;
  }

  private ActivityStorage getCachedActivityStorage() {
    if (activityStorage == null) {
      activityStorage = CommonsUtils.getService(ActivityStorage.class);
    }
    return activityStorage;
  }
  
  private void clearActivityStorageCache() {
    if (getCachedActivityStorage() instanceof CachedActivityStorage) {
      ((CachedActivityStorage) getCachedActivityStorage()).clearCache();
    }
  }

  private void putRelationshipToList(List<Relationship> relationships, RelationshipListEntity list) {
    if (list != null) {
      for (Map.Entry<String, RelationshipEntity> entry : list.getRelationships().entrySet()) {
        Relationship relationship = new Relationship(entry.getValue().getId());

        RelationshipEntity relationshipEntity = entry.getValue();
        IdentityEntity senderEntity = relationshipEntity.getFrom();
        IdentityEntity receiverEntity = relationshipEntity.getTo();
        //
        if (_getMixin(senderEntity, DisabledEntity.class, false) != null ||
            _getMixin(receiverEntity, DisabledEntity.class, false) != null) {
          continue;
        }
        Identity sender = new Identity(senderEntity.getId());
        sender.setRemoteId(senderEntity.getRemoteId());
        sender.setProviderId(senderEntity.getProviderId());
        ProfileEntity senderProfileEntity = senderEntity.getProfile();

        if (senderProfileEntity != null) {
          loadProfile(sender);
        }

        Identity receiver = new Identity(receiverEntity.getId());
        receiver.setRemoteId(receiverEntity.getRemoteId());
        receiver.setProviderId(receiverEntity.getProviderId());
        ProfileEntity receiverProfileEntity = receiverEntity.getProfile();

        if (receiverProfileEntity != null) {
          loadProfile(receiver);
        }

        relationship.setSender(sender);
        relationship.setReceiver(receiver);

        if (SENDER.equals(entry.getValue().getParent().getName()) ||
            RECEIVER.equals(entry.getValue().getParent().getName())) {
          relationship.setStatus(Relationship.Type.PENDING);
        }
        else {
          relationship.setStatus(Relationship.Type.CONFIRMED);
        }

        // TODO : IGNORED

        relationships.add(relationship);
      }
    }
  }

  private void putReceiverRelationshipToList(List<Relationship> relationships, RelationshipListEntity list, Identity receiver) {
    if (list != null) {
      for (Map.Entry<String, RelationshipEntity> entry : list.getRelationships().entrySet()) {
        Relationship relationship = new Relationship(entry.getValue().getId());

        RelationshipEntity relationshipEntity = entry.getValue();
        IdentityEntity senderEntity = relationshipEntity.getFrom();
        if (senderEntity.getId().equals(receiver.getId())) {
          senderEntity = relationshipEntity.getTo();
        }

        Identity sender = new Identity(senderEntity.getId());
        sender.setRemoteId(senderEntity.getRemoteId());
        sender.setProviderId(senderEntity.getProviderId());
        ProfileEntity senderProfileEntity = senderEntity.getProfile();

        if (senderProfileEntity != null) {
          loadProfile(sender);
        }

        if (receiver.getProfile() != null) {
          loadProfile(receiver);
        }

        relationship.setSender(sender);
        relationship.setReceiver(receiver);
        relationship.setStatus(Relationship.Type.PENDING);

       relationships.add(relationship);
      }
    }
  }
  
  private void loadProfile(final Identity identity) {
    ProfileLoader loader = new ProfileLoader() {
      public Profile load() throws IdentityStorageException {
        Profile profile = new Profile(identity);
        return identityStorage.loadProfile(profile);
      }
    };
    identity.setProfileLoader(loader);
  }

  private List<Identity> getIdentitiesFromRelationship(Iterator<RelationshipEntity> it, Origin origin, long offset, long limit) {

    //
    Set<Identity> identities = new LinkedHashSet<Identity>();
    int i = 0;

    _skip(it, offset);

    Identity identity = null;
    while (it.hasNext()) {

      RelationshipEntity relationshipEntity = it.next();
      IdentityEntity identityEntity;

      switch (origin) {

        case FROM:
          identityEntity = relationshipEntity.getFrom();
          identity = createIdentityFromEntity(identityEntity);
          
          if (identity.isEnable()) {
            identities.add(identity);
          }
          break;

        case TO:
          identityEntity = relationshipEntity.getTo();
          identity = createIdentityFromEntity(identityEntity);

          if (identity.isEnable()) {
            identities.add(identity);
          }
          break;
      }

      if (limit != -1 && limit > 0 && ++i >= limit) {
        break;
      }

    }

    return new ArrayList<Identity>(identities);
  }

  private List<Identity> getIdentitiesFromRelationship(Iterator<RelationshipEntity> it, Identity current, long offset, long limit) {
    //
    Set<Identity> identities = new LinkedHashSet<Identity>();
    int i = 0;

    _skip(it, offset);

    Identity identity = null;
    while (it.hasNext()) {
      RelationshipEntity relationshipEntity = it.next();
      
      IdentityEntity entity = relationshipEntity.getFrom();
      if (entity.getId().equals(current.getId())) {
        entity = relationshipEntity.getTo();
      }
      
      identity = createIdentityFromEntity(entity);
      if (identity.isEnable()) {
        identities.add(identity);
        if (limit != -1 && limit > 0 && ++i >= limit) {
          break;
        }
      }

    }

    return new ArrayList<Identity>(identities);
  }
  
  private Identity createIdentityFromEntity(IdentityEntity entity) {

    Identity identity = identityStorage.findIdentityById(entity.getId());
    loadProfile(identity);

    return identity;

  }

  private List<Identity> getIdentitiesRelationsByFilter(final List<Identity> relations, final ProfileFilter filter,
                                                        final long offset, final long limit) {
    
    if (relations.isEmpty()) return new ArrayList<Identity>();
    
    //
    List<Identity> found = new ArrayList<Identity>();
    if(relations.isEmpty()) return found ;
    QueryBuilder<ProfileEntity> builder = getSession().createQueryBuilder(ProfileEntity.class);
    WhereExpression whereExpression = new WhereExpression();
    StorageUtils.applyWhereFromIdentity(whereExpression, relations);

    //
    StorageUtils.applyFilter(whereExpression, filter);

    //
    builder.where(whereExpression.toString()).orderBy(ProfileEntity.fullName.getName(), Ordering.ASC);
    
    QueryImpl<ProfileEntity> queryImpl = (QueryImpl<ProfileEntity>) builder.get();
    ((org.exoplatform.services.jcr.impl.core.query.QueryImpl) queryImpl.getNativeQuery()).setCaseInsensitiveOrder(true);
    
    QueryResult<ProfileEntity> result = queryImpl.objects(offset, limit);
    
    while(result.hasNext()) {
      IdentityEntity current = result.next().getIdentity();
      if (_getMixin(current, DisabledEntity.class, false) != null) {
        continue;
      }
      Identity i = new Identity(current.getProviderId(), current.getRemoteId());
      i.setId(current.getId());
      found.add(i);
    }

    //
    return found;

  }

  private int getIdentitiesRelationsByFilterCount(final List<Identity> relations, final ProfileFilter filter) {

    if (relations.size() == 0) {
      return 0;
    }

    //
    QueryBuilder<ProfileEntity> builder = getSession().createQueryBuilder(ProfileEntity.class);

    //
    WhereExpression whereExpression = new WhereExpression();
    StorageUtils.applyWhereFromIdentity(whereExpression, relations);

    //
    StorageUtils.applyFilter(whereExpression, filter);
    //
    QueryResult<ProfileEntity> result = builder.where(whereExpression.toString()).get().objects();
    int number = 0;
    while (result.hasNext()) {
      IdentityEntity current = result.next().getIdentity();
      if (_getMixin(current, DisabledEntity.class, false) == null) {
        ++number;
      }
    }
    //
    return number;
  }

  private RelationshipStorage getStorage() {
    return (relationshipStorage != null ? relationshipStorage : this);
  }

  /*
   * Internal
   */

  protected RelationshipEntity _createRelationship(final Relationship relationship) throws NodeNotFoundException {
    String identityId1 = relationship.getSender().getId();
    String identityId2 = relationship.getReceiver().getId();

    IdentityEntity identity1 = _findById(IdentityEntity.class, identityId1);
    IdentityEntity identity2 = _findById(IdentityEntity.class, identityId2);

    RelationshipEntity createdRelationship = identity1.createRelationship();
    RelationshipEntity symmetricalRelationship = identity2.createRelationship();
    
    switch (relationship.getStatus()) {

      case PENDING:
        identity1.getSender().getRelationships().put(identity2.getRemoteId(), createdRelationship);
        identity2.getReceiver().getRelationships().put(identity1.getRemoteId(), symmetricalRelationship);
        
        createdRelationship.setFrom(identity1);
        createdRelationship.setTo(identity2);
        
        symmetricalRelationship.setFrom(identity1);
        symmetricalRelationship.setTo(identity2);
        
        break;

      case CONFIRMED:
        identity1.getRelationship().getRelationships().put(identity2.getRemoteId(), createdRelationship);
        identity2.getRelationship().getRelationships().put(identity1.getRemoteId(), symmetricalRelationship);
        
        createdRelationship.setFrom(identity1);
        createdRelationship.setTo(identity2);
        
        symmetricalRelationship.setFrom(identity2);
        symmetricalRelationship.setTo(identity1);
        
        break;

      case IGNORED:
        identity1.getIgnore().getRelationships().put(identity2.getRemoteId(), createdRelationship);
        identity2.getIgnored().getRelationships().put(identity1.getRemoteId(), symmetricalRelationship);
        
        createdRelationship.setFrom(identity1);
        createdRelationship.setTo(identity2);
        
        symmetricalRelationship.setFrom(identity1);
        symmetricalRelationship.setTo(identity2);
        
        break;

    }

    long createdTimeStamp = System.currentTimeMillis();
    createdRelationship.setReciprocal(symmetricalRelationship);
    createdRelationship.setStatus(relationship.getStatus().toString());
    createdRelationship.setCreatedTime(createdTimeStamp);
    
    symmetricalRelationship.setReciprocal(createdRelationship);
    symmetricalRelationship.setStatus(relationship.getStatus().toString());
    symmetricalRelationship.setCreatedTime(createdTimeStamp);

    relationship.setId(createdRelationship.getId());

    getSession().save();

    //
    LOG.debug(String.format(
        "Relationship from %s:%s to %s:%s created (%s)",
        createdRelationship.getFrom().getProviderId(),
        createdRelationship.getFrom().getRemoteId(),
        createdRelationship.getTo().getProviderId(),
        createdRelationship.getTo().getRemoteId(),
        createdRelationship.getPath()
    ));

    //
    LOG.debug(String.format(
        "Symmetrical relationship from %s:%s to %s:%s created (%s)",
        symmetricalRelationship.getFrom().getProviderId(),
        symmetricalRelationship.getFrom().getRemoteId(),
        symmetricalRelationship.getTo().getProviderId(),
        symmetricalRelationship.getTo().getRemoteId(),
        symmetricalRelationship.getPath()
    ));

    return createdRelationship;
  }

  protected RelationshipEntity _saveRelationship(final Relationship relationship) throws NodeNotFoundException {

    RelationshipEntity savedRelationship = _findById(RelationshipEntity.class, relationship.getId());
    RelationshipEntity symmetricalRelationship = savedRelationship.getReciprocal();
    
    IdentityEntity sender = _findById(IdentityEntity.class, relationship.getSender().getId());
    IdentityEntity receiver = _findById(IdentityEntity.class, relationship.getReceiver().getId());

    savedRelationship.setStatus(relationship.getStatus().toString());
    symmetricalRelationship.setStatus(relationship.getStatus().toString());

    switch (relationship.getStatus()) {
      case PENDING:

        // Move to sender / receiver
        savedRelationship.getParent().getParent().getSender().getRelationships()
            .put(savedRelationship.getName(), savedRelationship);

        symmetricalRelationship.getParent().getParent().getReceiver().getRelationships()
            .put(symmetricalRelationship.getName(), symmetricalRelationship);
        
        break;
      case CONFIRMED:
        
        //measure the relationship is two ways when relationship is confirmed
        savedRelationship.setFrom(sender);
        savedRelationship.setTo(receiver);
        
        symmetricalRelationship.setFrom(receiver);
        symmetricalRelationship.setTo(sender);

        // Move to relationship
        savedRelationship.getParent().getParent().getRelationship().getRelationships()
            .put(savedRelationship.getName(), savedRelationship);

        symmetricalRelationship.getParent().getParent().getRelationship().getRelationships()
            .put(symmetricalRelationship.getName(), symmetricalRelationship);
        
        updateRelationshipStatistic(sender, true);
        updateRelationshipStatistic(receiver, true);
        
        StreamInvocationHelper.connect(relationship.getSender(), relationship.getReceiver());
        
        break;
      
      // TODO : IGNORED
    }

    //getSession().save();

    //
    LOG.debug(String.format(
        "Relationship from %s:%s to %s:%s saved (%s)",
        savedRelationship.getFrom().getProviderId(),
        savedRelationship.getFrom().getRemoteId(),
        savedRelationship.getTo().getProviderId(),
        savedRelationship.getTo().getRemoteId(),
        savedRelationship.getPath()
    ));

    //
    LOG.debug(String.format(
        "Symmetrical relationship from %s:%s to %s:%s saved (%s)",
        symmetricalRelationship.getFrom().getProviderId(),
        symmetricalRelationship.getFrom().getRemoteId(),
        symmetricalRelationship.getTo().getProviderId(),
        symmetricalRelationship.getTo().getRemoteId(),
        symmetricalRelationship.getPath()
    ));

    return savedRelationship;
  }
  
  /**
   * Updates the relationship statistic for the given user. 
   * 
   * @param identityEntity the identity
   * @param isIncreaseCount determines the increase or decrease
   * @throws NodeNotFoundException
   */
  private void updateRelationshipStatistic(IdentityEntity identityEntity, boolean isIncreaseCount) {
    int newValue = 0;
    if (identityEntity.hasProperty(IdentityEntity.RELATIONSHIP_NUMBER_PARAM)) {
      String value = identityEntity.getProperties().get(IdentityEntity.RELATIONSHIP_NUMBER_PARAM);
      newValue = Integer.valueOf(value);
      if (isIncreaseCount) {
        newValue++;
      } else {
        newValue--;
      }
    } else {
      if (isIncreaseCount) {
        newValue = 1;
      }
    }
    
    identityEntity.setProperty(IdentityEntity.RELATIONSHIP_NUMBER_PARAM, String.valueOf(newValue));

    
  }

  protected List<Relationship> _getSenderRelationships(
      final Identity sender, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws NodeNotFoundException {

    // TODO : listCheckIdentity ?

    List<Relationship> relationships = new ArrayList<Relationship>();

    //
    IdentityEntity senderEntity = _findById(IdentityEntity.class, sender.getId());

    if (type == null) {
      putRelationshipToList(relationships, senderEntity.getRelationship());
      putRelationshipToList(relationships, senderEntity.getSender());
    }
    else {
      switch (type) {

        case CONFIRMED:
          putRelationshipToList(relationships, senderEntity.getRelationship());
          break;

        case PENDING:
          putRelationshipToList(relationships, senderEntity.getSender());
          break;

        // TODO : IGNORED

      }
    }

    return relationships;
  }

  protected List<Relationship> _getReceiverRelationships(
      final Identity receiver, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws NodeNotFoundException {

    List<Relationship> relationships = new ArrayList<Relationship>();

    //
    IdentityEntity receiverEntity = _findById(IdentityEntity.class, receiver.getId());

    if (type == null) {
      putRelationshipToList(relationships, receiverEntity.getRelationship());
      putRelationshipToList(relationships, receiverEntity.getReceiver());
    }
    else {
      switch (type) {

        case CONFIRMED:
          putRelationshipToList(relationships, receiverEntity.getRelationship());
          break;

        case PENDING:
          //SOC-4283 : to work around the problem of wrong data with receiver relationship (sender and receiver value are exchanged)
          //so we need a specific method to treat the problem
          putReceiverRelationshipToList(relationships, receiverEntity.getReceiver(), receiver);
          break;

        // TODO : IGNORED

      }
    }

    return relationships;
  }

  protected Relationship _getRelationship(String uuid) throws NodeNotFoundException {

    RelationshipEntity relationshipEntity = _findById(RelationshipEntity.class, uuid);

    IdentityEntity receiverEntity = relationshipEntity.getTo();
    IdentityEntity senderEntity = relationshipEntity.getFrom();

    Identity receiver = identityStorage.findIdentityById(receiverEntity.getId());
    Identity sender = identityStorage.findIdentityById(senderEntity.getId());

    Relationship relationship = new Relationship(uuid);
    if (relationshipEntity.isReceiver()) {
      relationship.setSender(receiver);
      relationship.setReceiver(sender);
    }
    else {
      relationship.setSender(sender);
      relationship.setReceiver(receiver);
    }

    if (SENDER.equals(relationshipEntity.getParent().getName()) ||
        RECEIVER.equals(relationshipEntity.getParent().getName())) {
      relationship.setStatus(Relationship.Type.PENDING);
    }
    else {
      relationship.setStatus(Relationship.Type.CONFIRMED);
    }

    // TODO : IGNORED

    return relationship;
  }

  protected Relationship _getRelationship(final Identity identity1, final Identity identity2)
      throws RelationshipStorageException, NodeNotFoundException {
    IdentityEntity identityEntity1 = _findById(IdentityEntity.class, identity1.getId());
    IdentityEntity identityEntity2 = _findById(IdentityEntity.class, identity2.getId());

    // CONFIRMED
    RelationshipEntity got = identityEntity1.getRelationship().getRelationships().get(identityEntity2.getName());    

    // PENDING
    if (got == null) {
      got = identityEntity1.getSender().getRelationships().get(identity2.getRemoteId());
    }
    if (got == null) {
      got = identityEntity2.getSender().getRelationships().get(identity1.getRemoteId());
    }

    // IGNORED
    if (got == null) {
      got = identityEntity1.getIgnore().getRelationships().get(identity2.getRemoteId());
    }
    if (got == null) {
      got = identityEntity2.getIgnore().getRelationships().get(identity1.getRemoteId());
    }
    
    // NOT FOUND
    if (got == null) {
      throw new NodeNotFoundException();
    }

    Relationship relationship = new Relationship(got.getId());

    //
    IdentityEntity senderEntity = got.getFrom();
    IdentityEntity receiverEntity = got.getTo();

    Identity sender = new Identity(senderEntity.getId());
    sender.setRemoteId(senderEntity.getRemoteId());
    sender.setProviderId(senderEntity.getProviderId());

    Identity receiver = new Identity(receiverEntity.getId());
    receiver.setRemoteId(receiverEntity.getRemoteId());
    receiver.setProviderId(receiverEntity.getProviderId());

    relationship.setSender(sender);
    relationship.setReceiver(receiver);

    relationship.setStatus(Relationship.Type.valueOf(got.getStatus()));

    return relationship;
  }

  /*
   * Public
   */

  /**
   * {@inheritDoc}
   */
  public Relationship saveRelationship(final Relationship relationship) throws RelationshipStorageException {
    try {
      if (relationship.getId() == null) {
        _createRelationship(relationship);
      }
      else {
        _saveRelationship(relationship);
        //
        StorageUtils.persist();
      }
    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
          RelationshipStorageException.Type.ILLEGAL_ARGUMENTS,
          new String[] { Relationship.class.getSimpleName() });
    }
    //
    clearActivityStorageCache();

    return relationship;
  }

  /**
   * {@inheritDoc}
   */
  public void removeRelationship(Relationship relationship) throws RelationshipStorageException {

    try {
      RelationshipEntity toDeleteRelationship = _findById(RelationshipEntity.class, relationship.getId());
      RelationshipEntity symmetricalRelationship = toDeleteRelationship.getReciprocal();

      IdentityEntity from = toDeleteRelationship.getFrom();
      IdentityEntity to = toDeleteRelationship.getTo();
      
      if(Relationship.Type.CONFIRMED.equals(relationship.getStatus())) {
        updateRelationshipStatistic(from, false);
        updateRelationshipStatistic(to, false);
      }

      _removeById(RelationshipEntity.class, symmetricalRelationship.getId());
      _removeById(RelationshipEntity.class, relationship.getId());
      
      StorageUtils.persist();
      
      StreamInvocationHelper.deleteConnect(relationship.getSender(), relationship.getReceiver());
      
      clearActivityStorageCache();

      //
      LOG.debug(String.format(
          "Symmetrical relationship from %s:%s to %s:%s removed",
          to.getProviderId(),
          to.getRemoteId(),
          from.getProviderId(),
          from.getRemoteId()
      ));

      //
      LOG.debug(String.format(
          "Relationship from %s:%s to %s:%s removed",
          from.getProviderId(),
          from.getRemoteId(),
          to.getProviderId(),
          to.getRemoteId()
      ));
    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP);
    }
  }

  /**
   * {@inheritDoc}
   */
  public Relationship getRelationship(String uuid) throws RelationshipStorageException {

    try {
      return _getRelationship(uuid);
    }
    catch (NodeNotFoundException e) {
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Relationship> getSenderRelationships(
      final Identity sender, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws RelationshipStorageException {

    try {
      return _getSenderRelationships(sender, type, listCheckIdentity);
    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
          RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP, null, e, sender.getId(), type.toString());
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Relationship> getSenderRelationships(
      final String senderId, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws RelationshipStorageException {

    return getSenderRelationships(new Identity(senderId), type, listCheckIdentity);

  }

  /**
   * {@inheritDoc}
   */
  public List<Relationship> getReceiverRelationships(
      final Identity receiver, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws RelationshipStorageException {

    try {
      return _getReceiverRelationships(receiver, type, listCheckIdentity);
    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
          RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP, null, e, receiver.getId(), type.toString());
    }
  }

  /**
   * {@inheritDoc}
   */
  public Relationship getRelationship(final Identity identity1, final Identity identity2)
      throws RelationshipStorageException {

    try {
      return _getRelationship(identity1, identity2);
    }
    catch (NodeNotFoundException e) {
      return null;
    }
  }
  
  @Override
  public boolean hasRelationship(Identity identity1, Identity identity2, String relationshipPath) throws RelationshipStorageException {
    //it implemented on CachedRelationshipStorage
    throw new RelationshipStorageException(RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP_OF_THEM, "hasRelationship() unsupported!"); 
  }

  /**
   * {@inheritDoc}
   */
  public List<Relationship> getRelationships(
      final Identity identity, final Relationship.Type type, final List<Identity> listCheckIdentity)
      throws RelationshipStorageException {
    
    try {
      List<Relationship> relationships = new ArrayList<Relationship>();

      //
      IdentityEntity receiverEntity = _findById(IdentityEntity.class, identity.getId());

      if (type == null) {
        putRelationshipToList(relationships, receiverEntity.getRelationship());
        putRelationshipToList(relationships, receiverEntity.getReceiver());
        putRelationshipToList(relationships, receiverEntity.getSender());
      }
      else {
        switch (type) {

          case CONFIRMED:
            putRelationshipToList(relationships, receiverEntity.getRelationship());
            break;

          case PENDING:
            putRelationshipToList(relationships, receiverEntity.getReceiver());
            putRelationshipToList(relationships, receiverEntity.getSender());
            break;

          case IGNORED:
            putRelationshipToList(relationships, receiverEntity.getIgnored());
            break;

        }
      }
      
      return relationships;
    }
    catch (NodeNotFoundException e) {
      return new ArrayList<Relationship>();
    }
  }
  
  /**
   * {@inheritDoc}
   */
  public List<Identity> getLastConnections(Identity identity, int limit) throws RelationshipStorageException {
    //check the limit parameter
    if (limit <= 0) {
      return new ArrayList<Identity>();
    }
    //
    List<Identity> identities = new ArrayList<Identity>();
    try {
      IdentityEntity identityEntity = _findById(IdentityEntity.class, identity.getId());
      String relationshipNodePath = identityEntity.getPath() + StorageUtils.SLASH_STR + StorageUtils.SOC_RELATIONSHIP;
      Node node = (Node) getSession().getJCRSession().getItem(relationshipNodePath);
      NodeIterator iterator = AbstractService.getNodeIteratorOrderDESC(node);
      while (iterator.hasNext() && limit > 0) {
        Node relNode = iterator.nextNode();
        if (relNode.getName().contains(StorageUtils.COLON_STR)) {
          String remoteId = relNode.getName().split(StorageUtils.COLON_STR)[1];
          Identity newIdentity = identityStorage.findIdentity(OrganizationIdentityProvider.NAME, remoteId);
          if (newIdentity != null) {//SOC-4865 : avoid the case that user has been deleted but the relationship is kept
            identities.add(newIdentity);
            limit--;
          }
        }
      }
    }
    catch (Exception e) {
      throw new RelationshipStorageException(
           RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
           e.getMessage());
    }

    return identities;
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getRelationships(final Identity identity, long offset, long limit)
      throws RelationshipStorageException {

    List<Identity> identities = new ArrayList<Identity>();

    try {

      IdentityEntity identityEntity = _findById(IdentityEntity.class, identity.getId());

      QueryBuilder<RelationshipEntity> builder = getSession().createQueryBuilder(RelationshipEntity.class);

      WhereExpression whereExpression = new WhereExpression();
      whereExpression.like(JCRProperties.path, identityEntity.getPath() + StorageUtils.SLASH_STR + StorageUtils.PERCENT_STR);

      builder.where(whereExpression.toString());
      builder.orderBy(RelationshipEntity.createdTime.getName(), Ordering.DESC);

      QueryResult<RelationshipEntity> results = builder.get().objects(offset, limit);

      while (results.hasNext()) {

        RelationshipEntity currentRelationshipEntity = results.next();
        IdentityEntity gotIdentityEntity;
        if (currentRelationshipEntity.isReceiver()) {
          gotIdentityEntity = currentRelationshipEntity.getFrom();
        }
        else {
          gotIdentityEntity = currentRelationshipEntity.getTo();
        }
        //
        if (_getMixin(gotIdentityEntity, DisabledEntity.class, false) != null) {
          continue;
        }

        Identity newIdentity = new Identity(gotIdentityEntity.getId());
        newIdentity.setProviderId(gotIdentityEntity.getProviderId());
        newIdentity.setRemoteId(gotIdentityEntity.getRemoteId());

        identities.add(newIdentity);
      }

    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
           RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
           e.getMessage());
    }

    return identities;
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getIncomingRelationships(Identity receiver,
                                                 long offset, long limit) throws RelationshipStorageException {

    try {
      
      IdentityEntity receiverEntity = _findById(IdentityEntity.class, receiver.getId());

      Iterator<RelationshipEntity> it = receiverEntity.getReceiver().getRelationships().values().iterator();
      //SOC-4283 : to work around the problem of wrong data with receiver relationship (sender and receiver value are exchanged)
      //so we need a specific method to treat the problem
      return getIdentitiesFromRelationship(it, receiver, offset, limit);

    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
           RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
           e.getMessage());
    }

  }

  /**
   * {@inheritDoc}
   */
  public int getIncomingRelationshipsCount(Identity receiver) throws RelationshipStorageException {
    //
    return getIncomingRelationships(receiver, 0, -1).size();
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getOutgoingRelationships(Identity sender,
                                                 long offset, long limit) throws RelationshipStorageException {

    try {

      IdentityEntity senderEntity = _findById(IdentityEntity.class, sender.getId());

      Iterator<RelationshipEntity> it = senderEntity.getSender().getRelationships().values().iterator();
      return getIdentitiesFromRelationship(it, Origin.TO, offset, limit);

    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(
           RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
           e.getMessage());
    }

  }

  /**
   * {@inheritDoc}
   */
  public int getOutgoingRelationshipsCount(Identity sender) throws RelationshipStorageException {
    return getOutgoingRelationships(sender, 0, -1).size();
  }

  /**
   * {@inheritDoc}
   */
   public int getRelationshipsCount(Identity identity) throws RelationshipStorageException {

     int nb = 0;

     //
     try {

       IdentityEntity identityEntity = _findById(IdentityEntity.class, identity.getId());
       nb += identityEntity.getRelationship().getRelationships().size();
       nb += identityEntity.getSender().getRelationships().size();
       nb += identityEntity.getReceiver().getRelationships().size();
       nb += identityEntity.getIgnore().getRelationships().size();

       return nb;
       
     }
     catch (NodeNotFoundException e) {

       throw new RelationshipStorageException(
           RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
           e.getMessage());

     }
   }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getConnections(Identity identity, long offset, long limit) throws RelationshipStorageException {

    try {
      IdentityEntity identityEntity = _findById(IdentityEntity.class, identity.getId());

      Iterator<RelationshipEntity> it = identityEntity.getRelationship().getRelationships().values().iterator();
      return getIdentitiesFromRelationship(it, Origin.TO, offset, limit);

    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.ILLEGAL_ARGUMENTS);
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getConnections(Identity identity) throws RelationshipStorageException {

    return getConnections(identity, 0, -1);

  }

  /**
   * {@inheritDoc}
   */
  public int getConnectionsCount(Identity identity) throws RelationshipStorageException {
    try {
      // TODO : use property to improve the perfs
      IdentityEntity identityEntity = _findById(IdentityEntity.class, identity.getId());
      if (identityEntity.hasProperty(IdentityEntity.RELATIONSHIP_NUMBER_PARAM)) {
        String value = identityEntity.getProperties().get(IdentityEntity.RELATIONSHIP_NUMBER_PARAM);
        return Integer.valueOf(value);
      } else {
        //
        int totalSize = identityEntity.getRelationship().getRelationships().size();
        identityEntity.setProperty(IdentityEntity.RELATIONSHIP_NUMBER_PARAM, String.valueOf(totalSize));
        getSession().save();
        return totalSize;
      }
    }
    catch (NodeNotFoundException e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.ILLEGAL_ARGUMENTS);
    }
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getConnectionsByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter, final long offset, final long limit)
      throws RelationshipStorageException {

    List<Identity> identities = getStorage().getConnections(existingIdentity);
    return getIdentitiesRelationsByFilter(identities, profileFilter, offset, limit);
    
  }

  /**
   * {@inheritDoc}
   */
  public List<Identity> getIncomingByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter, final long offset, final long limit)
      throws RelationshipStorageException {
    //
    if (profileFilter.isEmpty()) {
      return StorageUtils.sortIdentitiesByFullName(getIncomingRelationships(existingIdentity, offset, limit), true);
    }

    List<Identity> identities = getStorage().getIncomingRelationships(existingIdentity, 0, -1);
    return getIdentitiesRelationsByFilter(identities, profileFilter, offset, limit);

  }
  
  /**
   * {@inheritDoc}
   */
  public List<Identity> getOutgoingByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter, final long offset, final long limit)
      throws RelationshipStorageException {

    if (profileFilter.isEmpty()) {
      return StorageUtils.sortIdentitiesByFullName(getOutgoingRelationships(existingIdentity, offset, limit), true);
    }
    
    List<Identity> identities = getStorage().getOutgoingRelationships(existingIdentity, 0, -1);
    return getIdentitiesRelationsByFilter(identities, profileFilter, offset, limit);

  }
  /**
   * {@inheritDoc}
   */
  public int getIncomingCountByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter) throws RelationshipStorageException {
    
    if (profileFilter.isEmpty()) {
      return getIncomingRelationshipsCount(existingIdentity);
    }

    List<Identity> identities = getStorage().getIncomingRelationships(existingIdentity, 0, -1);
    return getIdentitiesRelationsByFilterCount(identities, profileFilter);

  }
  
  /**
   * {@inheritDoc}
   */
  public int getConnectionsCountByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter) throws RelationshipStorageException {

    List<Identity> identities = getStorage().getConnections(existingIdentity);
    return getIdentitiesRelationsByFilterCount(identities, profileFilter);

  }
  
  /**
   * {@inheritDoc}
   */
  public int getOutgoingCountByFilter(
      final Identity existingIdentity, final ProfileFilter profileFilter) throws RelationshipStorageException {
    
    if (profileFilter.isEmpty()) {
      return getOutgoingRelationshipsCount(existingIdentity);
    }

    List<Identity> identities = getStorage().getOutgoingRelationships(existingIdentity, 0, -1);
    return getIdentitiesRelationsByFilterCount(identities, profileFilter);

  }

  /**
   * {@inheritDoc}
   */
  public Map<Identity, Integer> getSuggestions(Identity currentIdentity, int maxConnections, 
                                                int maxConnectionsToLoad, 
                                                int maxSuggestions) throws RelationshipStorageException {
    try {
      return _getSuggestions(currentIdentity, maxConnections, maxConnectionsToLoad, maxSuggestions);
    } catch (Exception e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.FAILED_TO_GET_SUGGESTION, e);
    }
  }

  public Map<Identity, Integer> _getSuggestions(Identity currentIdentity, int maxConnections, 
                                                int maxConnectionsToLoad, 
                                                int maxSuggestions) throws Exception {
    if (maxConnectionsToLoad > 0 && maxConnections > maxConnectionsToLoad)
       maxConnectionsToLoad = maxConnections;
     // Get identities level 1
    Set<Identity> relationIdLevel1 = new HashSet<Identity>();
    RelationshipManager relationshipManager = getRelationshipManager();
    ListAccess<Identity> allConnections = relationshipManager.getConnections(currentIdentity);
    int size = allConnections.getSize();
    // The ideal limit of connection to treat however we could need to go beyond this limit
    // if we cannot reach the expected amount of suggestions
    int endIndex;
    Random random = new Random();
    Identity[] connections;
    if (size > maxConnectionsToLoad && maxConnectionsToLoad > 0 && maxConnections > 0) {
      // The total amount of connections is bigger than the maximum allowed
      // We will then load only a random sample to reduce the best we can the 
      // required time for this task 
      int startIndex = random.nextInt(size - maxConnectionsToLoad);
      endIndex = maxConnections;
      connections= allConnections.load(startIndex, maxConnectionsToLoad);
    } else {
      // The total amount of connections is less than the maximum allowed
      // We call load everything
      endIndex = size;
      connections= allConnections.load(0, size);
    }
    // we need to load all the connections
    for (int i = 0; i < connections.length; i++) {
      Identity id = connections[i];
      relationIdLevel1.add(id);
    }
    relationIdLevel1.remove(currentIdentity);

    // Get identities level 2 (suggested Identities)
    Map<Identity, Integer> suggestedIdentities = new HashMap<Identity, Integer>();
    Iterator<Identity> it = relationIdLevel1.iterator();
    for (int j = 0; j < size && it.hasNext(); j++) {
      Identity id = it.next();
      // We check if we reach the limit of connections to treat and if we have enough suggestions
      if (j >= endIndex && suggestedIdentities.size() > maxSuggestions && maxSuggestions > 0)
        break;
      ListAccess<Identity> allConns = relationshipManager.getConnections(id);
      int allConnSize = allConns.getSize();
      int allConnStartIndex = 0;
      if (allConnSize > maxConnections && maxConnections > 0) {
        // The current identity has more connections that the allowed amount so we will treat a sample
        allConnStartIndex = random.nextInt(allConnSize - maxConnections);
        connections = allConns.load(allConnStartIndex, maxConnections);
      } else {
        // The current identity doesn't have more connections that the allowed amount so we will 
        // treat all of them
        connections = allConns.load(0, allConnSize);
      }
      for (int i = 0; i < connections.length; i++) {
        Identity ids = connections[i];
        // We check if the current connection is not already part of the connections of the identity
        // for which we seek some suggestions
        if (!relationIdLevel1.contains(ids) && !ids.equals(currentIdentity) && !ids.isDeleted()
             && relationshipManager.get(ids, currentIdentity) == null) {
          Integer commonIdentities = suggestedIdentities.get(ids);
          if (commonIdentities == null) {
            commonIdentities = new Integer(1);
          } else {
            commonIdentities = new Integer(commonIdentities.intValue() + 1);
          }
          suggestedIdentities.put(ids, commonIdentities);
        }
      }
    }
    NavigableMap<Integer, List<Identity>> groupByCommonConnections = new TreeMap<Integer, List<Identity>>();
    // This for loop allows to group the suggestions by total amount of common connections
    for (Identity identity : suggestedIdentities.keySet()) {
      Integer commonIdentities = suggestedIdentities.get(identity);
      List<Identity> ids = groupByCommonConnections.get(commonIdentities);
      if (ids == null) {
        ids = new ArrayList<Identity>();
        groupByCommonConnections.put(commonIdentities, ids);
      }
      ids.add(identity);
    }
    Map<Identity, Integer> suggestions = new LinkedHashMap<Identity, Integer>();
    int suggestionLeft = maxSuggestions;
    // We iterate over the suggestions starting from the suggestions with the highest amount of common
    // connections
    main: for (Integer key : groupByCommonConnections.descendingKeySet()) {
      List<Identity> ids = groupByCommonConnections.get(key);
      for (Identity identity : ids) {
        suggestions.put(identity, key);
        // We stop once we have enough suggestions
        if (maxSuggestions > 0 && --suggestionLeft == 0)
          break main;
      }
    }
    return suggestions;
  }

  public void setStorage(RelationshipStorage storage) {
    this.relationshipStorage = storage;
  }
  
  /**
   * {@inheritDoc}
   */
  public List<Relationship> getRelationshipsByStatus(Identity identity, Relationship.Type type, long offset, long limit) {

    try {
      Session jcrSession = getSession().getJCRSession();
      List<Relationship> relationships = new ArrayList<Relationship>();

      //
      IdentityEntity receiverEntity = _findById(IdentityEntity.class, identity.getId());
      String relationshipNodePath = receiverEntity.getPath() + StorageUtils.SLASH_STR + StorageUtils.SOC_RELATIONSHIP;;
      String receiverNodePath = receiverEntity.getPath() + StorageUtils.SLASH_STR + StorageUtils.SOC_RELCEIVER;
      String senderNodePath = receiverEntity.getPath() + StorageUtils.SLASH_STR + StorageUtils.SOC_SENDER;
      Node node = null;
      NodeIterator relationIterator = null;
      NodeIterator senderIterator = null;
      NodeIterator receiverIterator = null;
      
      switch (type) {

      case ALL:
        node = (Node) jcrSession.getItem(relationshipNodePath);
        relationIterator = AbstractService.getNodeIteratorOrderDESC(node);
        node = (Node) jcrSession.getItem(receiverNodePath);
        senderIterator = AbstractService.getNodeIteratorOrderDESC(node);
        node = (Node) jcrSession.getItem(senderNodePath);
        receiverIterator = AbstractService.getNodeIteratorOrderDESC(node);
        getRelationships(relationships, new IteratorIterator<Node>(relationIterator, senderIterator, receiverIterator), offset, limit);
        break;

      case CONFIRMED:
        node = (Node) jcrSession.getItem(relationshipNodePath);
        relationIterator = AbstractService.getNodeIteratorOrderDESC(node);
        
        getRelationships(relationships, new IteratorIterator<Node>(relationIterator), offset, limit);
        break;

      case PENDING:
        node = (Node) jcrSession.getItem(receiverNodePath);
        receiverIterator = AbstractService.getNodeIteratorOrderDESC(node);
        node = (Node) jcrSession.getItem(senderNodePath);
        senderIterator = AbstractService.getNodeIteratorOrderDESC(node);
        getRelationships(relationships, new IteratorIterator<Node>(receiverIterator, senderIterator), offset, limit);
        break;

      case IGNORED:
        break;
      }

      return relationships;
    } catch (NodeNotFoundException e) {
      return new ArrayList<Relationship>();
    } catch (Exception e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
                                             e.getMessage());
    }
  }
  
  /**
   * {@inheritDoc}
   */
  public int getRelationshipsCountByStatus(Identity identity, Relationship.Type type) {
    try {
      IdentityEntity receiverEntity = _findById(IdentityEntity.class, identity.getId());

      switch (type) {

      case ALL:
        return receiverEntity.getRelationship().getRelationships().size()
            + receiverEntity.getReceiver().getRelationships().size()
            + receiverEntity.getSender().getRelationships().size();
      case CONFIRMED:
        return receiverEntity.getRelationship().getRelationships().size();
      case PENDING:
        return receiverEntity.getReceiver().getRelationships().size()
            + receiverEntity.getSender().getRelationships().size();
      case IGNORED:
        return receiverEntity.getIgnored().getRelationships().size();
      }
    } catch (Exception e) {
      return 0;
    }
    
    return 0;
  }
  
  private void getRelationships(List<Relationship> relationships, IteratorIterator<Node> nodeIt, long offset, long limit) {
    //
    int i = 0;

    _skip(nodeIt, offset);

    Relationship relationship = null;
    try {
      while (nodeIt.hasNext()) {
        Node relationshipNode = (Node) nodeIt.next();
        relationship = new Relationship(relationshipNode.getUUID());
        IdentityEntity senderEntity = _findById(IdentityEntity.class, relationshipNode.getProperty(StorageUtils.SOC_FROM).getString());
        IdentityEntity receiverEntity = _findById(IdentityEntity.class, relationshipNode.getProperty(StorageUtils.SOC_TO).getString());
        //
        if (_getMixin(senderEntity, DisabledEntity.class, false) != null ||
            _getMixin(receiverEntity, DisabledEntity.class, false) != null) {
          continue;
        }
        Identity sender = new Identity(senderEntity.getId());
        sender.setRemoteId(senderEntity.getRemoteId());
        sender.setProviderId(senderEntity.getProviderId());
        ProfileEntity senderProfileEntity = senderEntity.getProfile();
  
        if (senderProfileEntity != null) {
          loadProfile(sender);
        }
  
        Identity receiver = new Identity(receiverEntity.getId());
        receiver.setRemoteId(receiverEntity.getRemoteId());
        receiver.setProviderId(receiverEntity.getProviderId());
        ProfileEntity receiverProfileEntity = receiverEntity.getProfile();
  
        if (receiverProfileEntity != null) {
          loadProfile(receiver);
        }
  
        relationship.setSender(sender);
        relationship.setReceiver(receiver);
        if (StorageUtils.SOC_SENDER.equals(relationshipNode.getParent().getName()) ||
            StorageUtils.SOC_RELCEIVER.equals(relationshipNode.getParent().getName())) {
          relationship.setStatus(Relationship.Type.PENDING);
        }
        else {
          relationship.setStatus(Relationship.Type.CONFIRMED);
        }
  
        relationships.add(relationship);
        
        if (limit != -1 && limit > 0 && ++i >= limit) {
          break;
        }
      }
    } catch (Exception e) {
      throw new RelationshipStorageException(RelationshipStorageException.Type.FAILED_TO_GET_RELATIONSHIP,
                                             e.getMessage());
    }
  }
  
  class IteratorIterator<T> implements Iterator<T> {
    private final Iterator<T> is[];

    private int               current;

    public IteratorIterator(Iterator<T>... iterators) {
      is = iterators;
      current = 0;
    }

    public boolean hasNext() {
      while (current < is.length && !is[current].hasNext())
        current++;

      return current < is.length;
    }

    public T next() {
      while (current < is.length && !is[current].hasNext())
        current++;

      return is[current].next();
    }

    public void remove() { /* not implemented */
    }
  }
}
