You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by fm...@apache.org on 2013/02/13 12:51:21 UTC

svn commit: r1445561 - in /syncope/trunk: ./ core/src/main/java/org/apache/syncope/core/connid/ core/src/main/java/org/apache/syncope/core/propagation/impl/ core/src/main/java/org/apache/syncope/core/util/ core/src/main/resources/ core/src/test/java/or...

Author: fmartelli
Date: Wed Feb 13 11:51:21 2013
New Revision: 1445561

URL: http://svn.apache.org/r1445561
Log:
SYNCOPE-154 merged from the branch 1_0_X and fixed

Added:
    syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java
      - copied, changed from r1445543, syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java
    syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java
      - copied, changed from r1445543, syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java
    syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheValue.java
      - copied unchanged from r1445543, syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheValue.java
Modified:
    syncope/trunk/   (props changed)
    syncope/trunk/core/src/main/java/org/apache/syncope/core/connid/ConnObjectUtil.java
    syncope/trunk/core/src/main/java/org/apache/syncope/core/propagation/impl/PropagationManager.java
    syncope/trunk/core/src/main/java/org/apache/syncope/core/util/MappingUtil.java
    syncope/trunk/core/src/main/resources/syncopeContext.xml
    syncope/trunk/core/src/test/java/org/apache/syncope/core/rest/UserTestITCase.java

Propchange: syncope/trunk/
------------------------------------------------------------------------------
  Merged /syncope/branches/1_0_X:r1444851-1445543

Modified: syncope/trunk/core/src/main/java/org/apache/syncope/core/connid/ConnObjectUtil.java
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/java/org/apache/syncope/core/connid/ConnObjectUtil.java?rev=1445561&r1=1445560&r2=1445561&view=diff
==============================================================================
--- syncope/trunk/core/src/main/java/org/apache/syncope/core/connid/ConnObjectUtil.java (original)
+++ syncope/trunk/core/src/main/java/org/apache/syncope/core/connid/ConnObjectUtil.java Wed Feb 13 11:51:21 2013
@@ -60,6 +60,7 @@ import org.apache.syncope.core.util.Attr
 import org.apache.syncope.core.util.InvalidPasswordPolicySpecException;
 import org.apache.syncope.core.util.JexlUtil;
 import org.apache.syncope.core.util.MappingUtil;
+import org.apache.syncope.core.util.VirAttrCache;
 import org.identityconnectors.common.security.GuardedByteArray;
 import org.identityconnectors.common.security.GuardedString;
 import org.identityconnectors.framework.common.objects.Attribute;
@@ -106,6 +107,12 @@ public class ConnObjectUtil {
     @Autowired
     private PasswordGenerator pwdGen;
 
+    /**
+     * Virtual attribute cache.
+     */
+    @Autowired
+    private VirAttrCache virAttrCache;
+
     public ObjectClass fromAttributable(final AbstractAttributable attributable) {
         if (attributable == null
                 || (!(attributable instanceof SyncopeUser) && !(attributable instanceof SyncopeRole))) {
@@ -467,70 +474,106 @@ public class ConnObjectUtil {
         final ConfigurableApplicationContext context = ApplicationContextProvider.getApplicationContext();
         final ConnectorFactory connInstanceLoader = context.getBean(ConnectorFactory.class);
 
-        final Map<ConnectorObject, Set<AbstractMappingItem>> connObj2MapItems =
-                new HashMap<ConnectorObject, Set<AbstractMappingItem>>();
+        final IntMappingType type = owner instanceof SyncopeUser
+                ? IntMappingType.UserVirtualSchema : IntMappingType.RoleVirtualSchema;
 
-        for (ExternalResource resource : owner.getResources()) {
-            LOG.debug("Retrieve remote object from '{}'", resource.getName());
-            try {
-                final String accountId = attrUtil.getAccountIdItem(resource) == null
-                        ? null : MappingUtil.getAccountIdValue(owner, resource, attrUtil.getAccountIdItem(resource));
+        final Map<String, ConnectorObject> externalResources = new HashMap<String, ConnectorObject>();
 
-                LOG.debug("Search for object with accountId '{}'", accountId);
+        // -----------------------
+        // Retrieve virtual attribute values
+        // -----------------------
+        for (AbstractVirAttr virAttr : owner.getVirtualAttributes()) {
+            final String schemaName = virAttr.getVirtualSchema().getName();
+            final List<String> values = virAttrCache.get(attrUtil.getType(), owner.getId(), schemaName);
 
-                if (StringUtils.isNotBlank(accountId)) {
-                    // Retrieve attributes to get
-                    final Set<AbstractMappingItem> virMapItems = new HashSet<AbstractMappingItem>();
-                    final Set<String> extAttrNames = new HashSet<String>();
-
-                    for (AbstractMappingItem item : attrUtil.getMappingItems(resource)) {
-                        if ((AttributableType.USER == attrUtil.getType()
-                                && item.getIntMappingType() == IntMappingType.UserVirtualSchema)
-                                || (AttributableType.ROLE == attrUtil.getType()
-                                && item.getIntMappingType() == IntMappingType.RoleVirtualSchema)) {
+            LOG.debug("Retrieve values for virtual attribute {}", schemaName);
 
-                            virMapItems.add(item);
-                            extAttrNames.add(item.getExtAttrName());
-                        }
-                    }
+            if (values == null) {
+                // non cached ...
+                LOG.debug("Need one or more remote connections");
+                for (ExternalResource resource : getTargetResource(virAttr, type, attrUtil)) {
+                    LOG.debug("Seach values into {}", resource.getName());
+                    try {
+                        final ConnectorObject connectorObject;
 
-                    // Search for remote object
-                    final OperationOptionsBuilder oob = new OperationOptionsBuilder();
-                    oob.setAttributesToGet(extAttrNames);
+                        if (externalResources.containsKey(resource.getName())) {
+                            connectorObject = externalResources.get(resource.getName());
+                        } else {
+                            LOG.debug("Perform connection to {}", resource.getName());
+                            final String accountId = attrUtil.getAccountIdItem(resource) == null
+                                    ? null
+                                    : MappingUtil.getAccountIdValue(
+                                    owner, resource, attrUtil.getAccountIdItem(resource));
 
-                    final SyncopeConnector connector = connInstanceLoader.getConnector(resource);
-                    final ConnectorObject connObj =
-                            connector.getObject(fromAttributable(owner), new Uid(accountId), oob.build());
-                    if (connObj != null) {
-                        connObj2MapItems.put(connObj, virMapItems);
-                    }
+                            if (StringUtils.isBlank(accountId)) {
+                                throw new IllegalArgumentException("No AccountId found for " + resource.getName());
+                            }
 
-                    LOG.debug("Retrieved remote object {}", connObj);
-                }
-            } catch (Exception e) {
-                LOG.error("Unable to retrieve virtual attribute values on '{}'", resource.getName(), e);
-            }
-        }
+                            final Set<String> extAttrNames = new HashSet<String>();
 
-        for (AbstractVirAttr virAttr : owner.getVirtualAttributes()) {
-            LOG.debug("Provide value for virtual attribute '{}'", virAttr.getVirtualSchema().getName());
+                            // retrieve all mapped virtual attribute values
+                            for (AbstractMappingItem item :
+                                    MappingUtil.getMatchingMappingItems(attrUtil.getMappingItems(resource), type)) {
+                                extAttrNames.add(item.getExtAttrName());
+                            }
+
+                            LOG.debug("External attribute ({}) names to get '{}'", type, extAttrNames);
 
-            for (Map.Entry<ConnectorObject, Set<AbstractMappingItem>> entry : connObj2MapItems.entrySet()) {
-                for (AbstractMappingItem vAttrMapItem : entry.getValue()) {
-                    final String extAttrName = vAttrMapItem.getExtAttrName();
-                    final Attribute extAttr = entry.getKey().getAttributeByName(extAttrName);
-                    if (extAttr != null && extAttr.getValue() != null && !extAttr.getValue().isEmpty()) {
-                        for (Object obj : extAttr.getValue()) {
-                            if (obj != null) {
-                                virAttr.addValue(obj.toString());
+                            final OperationOptionsBuilder oob = new OperationOptionsBuilder();
+                            oob.setAttributesToGet(extAttrNames);
+
+                            final SyncopeConnector connector = connInstanceLoader.getConnector(resource);
+                            connectorObject = connector.getObject(ObjectClass.ACCOUNT, new Uid(accountId), oob.build());
+                            externalResources.put(resource.getName(), connectorObject);
+                        }
+
+                        // ask for searched virtual attribute value
+                        final List<AbstractMappingItem> mappings = MappingUtil.getMatchingMappingItems(
+                                attrUtil.getMappingItems(resource), schemaName, type);
+
+                        // the same virtual attribute could be mapped with one or more external attribute 
+                        for (AbstractMappingItem mapping : mappings) {
+                            final Attribute attribute = connectorObject.getAttributeByName(mapping.getExtAttrName());
+
+                            if (attribute != null && attribute.getValue() != null) {
+                                for (Object obj : attribute.getValue()) {
+                                    if (obj != null) {
+                                        virAttr.addValue(obj.toString());
+                                    }
+                                }
                             }
                         }
+
+                        LOG.debug("Retrieved values {}", virAttr.getValues());
+                    } catch (Exception e) {
+                        LOG.error("Error reading connector object from {}", resource.getName(), e);
                     }
                 }
+
+                virAttrCache.put(attrUtil.getType(), owner.getId(), schemaName, virAttr.getValues());
+            } else {
+                // cached ...
+                LOG.debug("Values found in cache {}", values);
+                virAttr.setValues(values);
+            }
+        }
+        // -----------------------
+    }
+
+    private Set<ExternalResource> getTargetResource(
+            final AbstractVirAttr attr, final IntMappingType type, final AttributableUtil attrUtil) {
+
+        final Set<ExternalResource> resources = new HashSet<ExternalResource>();
+
+        for (ExternalResource res : attr.getOwner().getResources()) {
+            if (!MappingUtil.getMatchingMappingItems(
+                    attrUtil.getMappingItems(res), attr.getVirtualSchema().getName(), type).isEmpty()) {
+
+                resources.add(res);
             }
         }
 
-        LOG.debug("Virtual attribute evaluation ended");
+        return resources;
     }
 
     private void fillFromTemplate(final AbstractAttributableTO attributableTO, final AbstractAttributableTO template) {

Modified: syncope/trunk/core/src/main/java/org/apache/syncope/core/propagation/impl/PropagationManager.java
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/java/org/apache/syncope/core/propagation/impl/PropagationManager.java?rev=1445561&r1=1445560&r2=1445561&view=diff
==============================================================================
--- syncope/trunk/core/src/main/java/org/apache/syncope/core/propagation/impl/PropagationManager.java (original)
+++ syncope/trunk/core/src/main/java/org/apache/syncope/core/propagation/impl/PropagationManager.java Wed Feb 13 11:51:21 2013
@@ -29,6 +29,7 @@ import java.util.Set;
 import org.apache.syncope.common.mod.AttributeMod;
 import org.apache.syncope.common.to.AttributeTO;
 import org.apache.syncope.common.types.AttributableType;
+import org.apache.syncope.common.types.IntMappingType;
 import org.apache.syncope.common.types.ResourceOperation;
 import org.apache.syncope.core.connid.ConnObjectUtil;
 import org.apache.syncope.core.connid.PasswordGenerator;
@@ -47,6 +48,7 @@ import org.apache.syncope.core.rest.data
 import org.apache.syncope.core.rest.data.UserDataBinder;
 import org.apache.syncope.core.util.AttributableUtil;
 import org.apache.syncope.core.util.MappingUtil;
+import org.apache.syncope.core.util.VirAttrCache;
 import org.apache.syncope.core.workflow.WorkflowResult;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
@@ -95,6 +97,12 @@ public class PropagationManager {
     private PasswordGenerator passwordGenerator;
 
     /**
+     * Virtual attribute cache.
+     */
+    @Autowired
+    private VirAttrCache virAttrCache;
+
+    /**
      * Create the user on every associated resource.
      *
      * @param wfResult user to be propagated (and info associated), as per result from workflow
@@ -445,6 +453,14 @@ public class PropagationManager {
             LOG.debug("Processing schema {}", mapping.getIntAttrName());
 
             try {
+                if ((attrUtil.getType() == AttributableType.USER
+                        && mapping.getIntMappingType() == IntMappingType.UserVirtualSchema)
+                        || (attrUtil.getType() == AttributableType.ROLE
+                        && mapping.getIntMappingType() == IntMappingType.RoleVirtualSchema)) {
+                    LOG.debug("Expire entry cache {}-{}", subject.getId(), mapping.getIntAttrName());
+                    virAttrCache.expire(attrUtil.getType(), subject.getId(), mapping.getIntAttrName());
+                }
+
                 Map.Entry<String, Attribute> preparedAttribute = MappingUtil.prepareAttribute(
                         resource, mapping, subject, password, passwordGenerator, vAttrsToBeRemoved, vAttrsToBeUpdated);
 

Modified: syncope/trunk/core/src/main/java/org/apache/syncope/core/util/MappingUtil.java
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/java/org/apache/syncope/core/util/MappingUtil.java?rev=1445561&r1=1445560&r2=1445561&view=diff
==============================================================================
--- syncope/trunk/core/src/main/java/org/apache/syncope/core/util/MappingUtil.java (original)
+++ syncope/trunk/core/src/main/java/org/apache/syncope/core/util/MappingUtil.java Wed Feb 13 11:51:21 2013
@@ -71,6 +71,20 @@ public final class MappingUtil {
      */
     private static final Logger LOG = LoggerFactory.getLogger(MappingUtil.class);
 
+    public static <T extends AbstractMappingItem> List<T> getMatchingMappingItems(
+            final Collection<T> items, final IntMappingType type) {
+
+        final List<T> result = new ArrayList<T>();
+
+        for (T mapItem : items) {
+            if (mapItem.getIntMappingType() == type) {
+                result.add(mapItem);
+            }
+        }
+
+        return result;
+    }
+
     public static <T extends AbstractMappingItem> List<T> getMatchingMappingItems(final Collection<T> items,
             final String intAttrName, final IntMappingType type) {
 

Copied: syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java (from r1445543, syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java)
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java?p2=syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java&p1=syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java&r1=1445543&r2=1445561&rev=1445561&view=diff
==============================================================================
--- syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java (original)
+++ syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCache.java Wed Feb 13 11:51:21 2013
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import org.apache.syncope.common.types.AttributableType;
 
 /**
  * Virtual Attribute Value cache.
@@ -76,7 +77,8 @@ public final class VirAttrCache {
      * @param schemaName virtual attribute name.
      * @param values virtual attribute values.
      */
-    public void put(final Long userId, final String schemaName, final List<String> values) {
+    public void put(
+            final AttributableType type, final Long userId, final String schemaName, final List<String> values) {
         synchronized (cache) {
             // this operations (retrieve cache space and put entry on) have to be thread safe.
 
@@ -84,7 +86,7 @@ public final class VirAttrCache {
                 freeCacheSpace(true);
             }
 
-            cache.put(new VirAttrCacheKey(userId, schemaName), new VirAttrCacheValue(values));
+            cache.put(new VirAttrCacheKey(type, userId, schemaName), new VirAttrCacheValue(values));
         }
     }
 
@@ -95,8 +97,8 @@ public final class VirAttrCache {
      * @param schemaName virtual attribute schema name.
      * @return cached values or null in case of virtual attribute not found.
      */
-    public List<String> get(final Long userId, final String schemaName) {
-        final VirAttrCacheValue value = cache.get(new VirAttrCacheKey(userId, schemaName));
+    public List<String> get(final AttributableType type, final Long userId, final String schemaName) {
+        final VirAttrCacheValue value = cache.get(new VirAttrCacheKey(type, userId, schemaName));
         return isValidEntry(value) ? value.getValues() : null;
     }
 
@@ -106,8 +108,8 @@ public final class VirAttrCache {
      * @param userId user id.
      * @param schemaName virtual attribute schema name.
      */
-    public void expire(final Long userId, final String schemaName) {
-        final VirAttrCacheValue value = cache.get(new VirAttrCacheKey(userId, schemaName));
+    public void expire(final AttributableType type, final Long userId, final String schemaName) {
+        final VirAttrCacheValue value = cache.get(new VirAttrCacheKey(type, userId, schemaName));
         if (isValidEntry(value)) {
             synchronized (cache) {
                 value.forceExpiring();

Copied: syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java (from r1445543, syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java)
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java?p2=syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java&p1=syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java&r1=1445543&r2=1445561&rev=1445561&view=diff
==============================================================================
--- syncope/branches/1_0_X/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java (original)
+++ syncope/trunk/core/src/main/java/org/apache/syncope/core/util/VirAttrCacheKey.java Wed Feb 13 11:51:21 2013
@@ -18,28 +18,36 @@
  */
 package org.apache.syncope.core.util;
 
+import org.apache.syncope.common.types.AttributableType;
+
 /**
  * Ccahe entry key.
  */
 public class VirAttrCacheKey {
 
     /**
-     * User ID.
+     * Subject type.
+     */
+    private final AttributableType type;
+
+    /**
+     * Subject ID.
      */
-    private final transient Long userId;
+    private final transient Long id;
 
     /**
      * Virtual attribute schema name.
      */
     private final transient String virAttrSchema;
 
-    public VirAttrCacheKey(final Long userId, final String virAttrSchema) {
-        this.userId = userId;
+    public VirAttrCacheKey(final AttributableType type, final Long id, final String virAttrSchema) {
+        this.type = type;
+        this.id = id;
         this.virAttrSchema = virAttrSchema;
     }
 
-    public Long getUserId() {
-        return userId;
+    public Long getId() {
+        return id;
     }
 
     public String getVirAttrSchema() {
@@ -48,16 +56,17 @@ public class VirAttrCacheKey {
 
     @Override
     public int hashCode() {
-        return 31 * (31
-                + (userId == null ? 0 : userId.hashCode()))
-                + virAttrSchema == null ? 0 : virAttrSchema.hashCode();
+        return 31 * (31 * (31
+                + (id == null ? 0 : id.hashCode()))
+                + (virAttrSchema == null ? 0 : virAttrSchema.hashCode()))
+                + (type == null ? 0 : type.hashCode());
     }
 
     @Override
     public boolean equals(Object o) {
         return o != null && o instanceof VirAttrCacheKey
-                && ((userId == null && ((VirAttrCacheKey) o).getUserId() == null)
-                || userId.equals(((VirAttrCacheKey) o).getUserId()))
+                && ((id == null && ((VirAttrCacheKey) o).getId() == null)
+                || id.equals(((VirAttrCacheKey) o).getId()))
                 && ((virAttrSchema == null && ((VirAttrCacheKey) o).getVirAttrSchema() == null)
                 || virAttrSchema.equals(((VirAttrCacheKey) o).getVirAttrSchema()));
     }

Modified: syncope/trunk/core/src/main/resources/syncopeContext.xml
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/main/resources/syncopeContext.xml?rev=1445561&r1=1445560&r2=1445561&view=diff
==============================================================================
--- syncope/trunk/core/src/main/resources/syncopeContext.xml (original)
+++ syncope/trunk/core/src/main/resources/syncopeContext.xml Wed Feb 13 11:51:21 2013
@@ -88,5 +88,12 @@ under the License.
     <property name="lenient" value="true"/>
     <property name="silent" value="false"/>
   </bean>
+  
   <bean id="jexlUtil" class="org.apache.syncope.core.util.JexlUtil"/>
+  
+  <bean id="virAttrCache" class="org.apache.syncope.core.util.VirAttrCache" scope="singleton">
+    <constructor-arg value="60"/>
+    <constructor-arg value="5000"/>
+    <constructor-arg value="5"/>
+  </bean>
 </beans>

Modified: syncope/trunk/core/src/test/java/org/apache/syncope/core/rest/UserTestITCase.java
URL: http://svn.apache.org/viewvc/syncope/trunk/core/src/test/java/org/apache/syncope/core/rest/UserTestITCase.java?rev=1445561&r1=1445560&r2=1445561&view=diff
==============================================================================
--- syncope/trunk/core/src/test/java/org/apache/syncope/core/rest/UserTestITCase.java (original)
+++ syncope/trunk/core/src/test/java/org/apache/syncope/core/rest/UserTestITCase.java Wed Feb 13 11:51:21 2013
@@ -1958,6 +1958,71 @@ public class UserTestITCase extends Abst
         assertEquals(PropagationTaskExecStatus.SUCCESS, prop.getStatus());
     }
 
+    @Test
+    public void virAttrCache() {
+        UserTO userTO = getUniqueSampleTO("virattrcache@apache.org");
+        userTO.getVirtualAttributes().clear();
+
+        AttributeTO virAttrTO = new AttributeTO();
+        virAttrTO.setSchema("virtualdata");
+        virAttrTO.addValue("virattrcache");
+        userTO.addVirtualAttribute(virAttrTO);
+
+        userTO.getMemberships().clear();
+        userTO.getResources().clear();
+        userTO.addResource("resource-db-virattr");
+
+        // 1. create user
+        UserTO actual = createUser(userTO);
+        assertNotNull(actual);
+
+        // 2. check for virtual attribute value
+        actual = userService.read(actual.getId());
+        assertEquals("virattrcache", actual.getVirtualAttributeMap().get("virtualdata").getValues().get(0));
+
+        Exception exception = null;
+        try {
+            final JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
+
+            String value = jdbcTemplate.queryForObject(
+                    "SELECT USERNAME FROM testsync WHERE ID=?", String.class, actual.getId());
+            assertEquals("virattrcache", value);
+
+            jdbcTemplate.update("UPDATE testsync set USERNAME='virattrcache2' WHERE ID=?", userTO.getId());
+
+            value = jdbcTemplate.queryForObject(
+                    "SELECT USERNAME FROM testsync WHERE ID=?", String.class, userTO.getId());
+            assertEquals("virattrcache2", value);
+
+        } catch (EmptyResultDataAccessException e) {
+            exception = e;
+        }
+        assertNotNull(exception);
+
+        // 2. check for cached attribute value
+        actual = userService.read(actual.getId());
+        assertEquals("virattrcache", actual.getVirtualAttributeMap().get("virtualdata").getValues().get(0));
+
+        UserMod userMod = new UserMod();
+        userMod.setId(actual.getId());
+
+        AttributeMod virtualdata = new AttributeMod();
+        virtualdata.setSchema("virtualdata");
+        virtualdata.addValueToBeAdded("virtualupdated");
+
+        userMod.addVirtualAttributeToBeRemoved("virtualdata");
+        userMod.addVirtualAttributeToBeUpdated(virtualdata);
+
+        // 3. update virtual attribute
+        actual = userService.update(actual.getId(), userMod);
+        assertNotNull(actual);
+
+        // 4. check for virtual attribute value
+        actual = userService.read(actual.getId());
+        assertNotNull(actual);
+        assertEquals("virtualupdated", actual.getVirtualAttributeMap().get("virtualdata").getValues().get(0));
+    }
+
     private boolean getBooleanAttribute(ConnObjectTO connObjectTO, String attrName) {
         return Boolean.parseBoolean(getStringAttribute(connObjectTO, attrName));
     }