Bladeren bron

AMBARI-15919. Migrate instance specific data from one version to another. (Gaurav Nagar via dipayanb)

Dipayan Bhowmick 9 jaren geleden
bovenliggende
commit
614b12fc38
39 gewijzigde bestanden met toevoegingen van 2502 en 27 verwijderingen
  1. 219 0
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ViewDataMigrationService.java
  2. 5 2
      ambari-server/src/main/java/org/apache/ambari/server/api/services/ViewInstanceService.java
  3. 10 0
      ambari-server/src/main/java/org/apache/ambari/server/orm/dao/PrivilegeDAO.java
  4. 49 3
      ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java
  5. 258 0
      ambari-server/src/main/java/org/apache/ambari/server/view/ViewDataMigrationContextImpl.java
  6. 29 0
      ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
  7. 52 0
      ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java
  8. 215 0
      ambari-server/src/test/java/org/apache/ambari/server/api/services/ViewDataMigrationServiceTest.java
  9. 66 0
      ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java
  10. 403 0
      ambari-server/src/test/java/org/apache/ambari/server/view/ViewDataMigrationContextImplTest.java
  11. 45 0
      ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java
  12. 1 0
      ambari-views/examples/README.md
  13. 1 1
      ambari-views/examples/calculator-view/pom.xml
  14. 2 2
      ambari-views/examples/cluster-view/pom.xml
  15. 1 1
      ambari-views/examples/favorite-view/pom.xml
  16. 2 2
      ambari-views/examples/hello-servlet-view/pom.xml
  17. 2 2
      ambari-views/examples/hello-spring-view/pom.xml
  18. 2 2
      ambari-views/examples/helloworld-view/pom.xml
  19. 131 0
      ambari-views/examples/phone-list-upgrade-view/docs/index.md
  20. 105 0
      ambari-views/examples/phone-list-upgrade-view/pom.xml
  21. 126 0
      ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/DataMigrator.java
  22. 242 0
      ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/PhoneListServlet.java
  23. 113 0
      ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/PhoneUser.java
  24. 37 0
      ambari-views/examples/phone-list-upgrade-view/src/main/resources/WEB-INF/web.xml
  25. 54 0
      ambari-views/examples/phone-list-upgrade-view/src/main/resources/view.xml
  26. 1 1
      ambari-views/examples/phone-list-view/pom.xml
  27. 3 2
      ambari-views/examples/pom.xml
  28. 2 2
      ambari-views/examples/property-validator-view/pom.xml
  29. 2 2
      ambari-views/examples/property-view/pom.xml
  30. 1 1
      ambari-views/examples/restricted-view/pom.xml
  31. 2 2
      ambari-views/examples/simple-view/pom.xml
  32. 1 1
      ambari-views/examples/weather-view/pom.xml
  33. 34 0
      ambari-views/src/main/java/org/apache/ambari/view/migration/EntityConverter.java
  34. 150 0
      ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrationContext.java
  35. 44 0
      ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrationException.java
  36. 60 0
      ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrator.java
  37. 1 1
      ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java
  38. 10 0
      ambari-views/src/main/resources/view.xsd
  39. 21 0
      pom.xml

+ 219 - 0
ambari-server/src/main/java/org/apache/ambari/server/api/services/ViewDataMigrationService.java

@@ -0,0 +1,219 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server.api.services;
+
+import org.apache.ambari.server.orm.entities.ViewInstanceEntity;
+import org.apache.ambari.server.view.ViewDataMigrationContextImpl;
+import org.apache.ambari.server.view.ViewRegistry;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.ambari.view.migration.ViewDataMigrator;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+/**
+ * Service responsible for data migration between view instances.
+ */
+public class ViewDataMigrationService extends BaseService {
+  /**
+   * Logger.
+   */
+  private static final Log LOG = LogFactory.getLog(ViewDataMigrationService.class);
+
+  /**
+   * The current view name.
+   */
+  private final String viewName;
+
+  /**
+   * The current view version.
+   */
+  private final String viewVersion;
+
+  /**
+   * The current view instance name.
+   */
+  private final String instanceName;
+
+  private ViewRegistry viewRegistry;
+
+  /**
+   * Constructor.
+   *
+   * @param viewName       the current view name
+   * @param viewVersion    the current view version
+   * @param instanceName   the current view instance name
+   */
+  public ViewDataMigrationService(String viewName, String viewVersion, String instanceName) {
+    this.viewName = viewName;
+    this.viewVersion = viewVersion;
+    this.instanceName = instanceName;
+    this.viewRegistry = ViewRegistry.getInstance();
+  }
+
+  /**
+   * Migrates view instance persistence data from origin view instance
+   * specified in the path params.
+   *
+   * @param originViewVersion    the origin view version
+   * @param originInstanceName   the origin view instance name
+   */
+  @PUT
+  @Path("{originVersion}/{originInstanceName}")
+  public Response migrateData(@PathParam("originVersion") String originViewVersion,
+                              @PathParam("originInstanceName") String originInstanceName)
+      throws ViewDataMigrationException {
+
+    if (!viewRegistry.checkAdmin()) {
+      throw new WebApplicationException(Response.Status.FORBIDDEN);
+    }
+
+    LOG.info("Data Migration to view instance " + viewName + "/" + viewVersion + "/" + instanceName +
+        " from " + viewName + "/" + originViewVersion + "/" + originInstanceName);
+
+    ViewInstanceEntity instanceDefinition = getViewInstanceEntity(viewName, viewVersion, instanceName);
+    ViewInstanceEntity originInstanceDefinition = getViewInstanceEntity(viewName, originViewVersion, originInstanceName);
+
+    ViewDataMigrationContextImpl migrationContext = getViewDataMigrationContext(instanceDefinition, originInstanceDefinition);
+
+    ViewDataMigrator dataMigrator = getViewDataMigrator(instanceDefinition, migrationContext);
+
+    LOG.debug("Running before-migration hook");
+    if (!dataMigrator.beforeMigration()) {
+      String msg = "View " + viewName + "/" + viewVersion + "/" + instanceName + " canceled the migration process";
+
+      LOG.error(msg);
+      throw new ViewDataMigrationException(msg);
+    }
+
+    Map<String, Class> originClasses = migrationContext.getOriginEntityClasses();
+    Map<String, Class> currentClasses = migrationContext.getCurrentEntityClasses();
+    for (Map.Entry<String, Class> originEntity : originClasses.entrySet()) {
+      LOG.debug("Migrating persistence entity " + originEntity.getKey());
+      if (currentClasses.containsKey(originEntity.getKey())) {
+        Class entity = currentClasses.get(originEntity.getKey());
+        dataMigrator.migrateEntity(originEntity.getValue(), entity);
+      } else {
+        LOG.debug("Entity " + originEntity.getKey() + " not found in target view");
+        dataMigrator.migrateEntity(originEntity.getValue(), null);
+      }
+    }
+
+    LOG.debug("Migrating instance data");
+    dataMigrator.migrateInstanceData();
+
+    LOG.debug("Running after-migration hook");
+    dataMigrator.afterMigration();
+
+    LOG.debug("Copying user permissions");
+    viewRegistry.copyPrivileges(originInstanceDefinition, instanceDefinition);
+
+    Response.ResponseBuilder builder = Response.status(Response.Status.OK);
+    return builder.build();
+  }
+
+  protected ViewDataMigrationContextImpl getViewDataMigrationContext(ViewInstanceEntity instanceDefinition, ViewInstanceEntity originInstanceDefinition) {
+    return new ViewDataMigrationContextImpl(
+        originInstanceDefinition, instanceDefinition);
+  }
+
+  protected ViewInstanceEntity getViewInstanceEntity(String viewName, String viewVersion, String instanceName) {
+    return viewRegistry.getInstanceDefinition(viewName, viewVersion, instanceName);
+  }
+
+  /**
+   * Get the migrator instance for view instance with injected migration context.
+   * If versions of instances are same returns copy-all-data migrator.
+   * If versions are different, loads the migrator from the current view (view should
+   * contain ViewDataMigrator implementation, otherwise exception will be raised).
+   *
+   * @param currentInstanceDefinition    the current view instance definition
+   * @param migrationContext             the migration context to inject into migrator
+   * @throws ViewDataMigrationException  if view does not support migration
+   * @return  the data migration instance
+   */
+  protected ViewDataMigrator getViewDataMigrator(ViewInstanceEntity currentInstanceDefinition,
+                                                 ViewDataMigrationContextImpl migrationContext)
+      throws ViewDataMigrationException {
+    ViewDataMigrator dataMigrator;
+
+    LOG.info("Migrating " + viewName + "/" + viewVersion + "/" + instanceName +
+        " data from " + migrationContext.getOriginDataVersion() + " to " +
+        migrationContext.getCurrentDataVersion() + " data version");
+
+    if (migrationContext.getOriginDataVersion() == migrationContext.getCurrentDataVersion()) {
+
+      LOG.info("Instances of same version, copying all data.");
+      dataMigrator = new CopyAllDataMigrator(migrationContext);
+    } else {
+      try {
+        dataMigrator = currentInstanceDefinition.getDataMigrator(migrationContext);
+        if (dataMigrator == null) {
+          throw new ViewDataMigrationException("A view instance " +
+              viewName + "/" + viewVersion + "/" + instanceName + " does not support migration.");
+        }
+        LOG.debug("Data migrator loaded");
+      } catch (ClassNotFoundException e) {
+        String msg = "Caught exception loading data migrator of " + viewName + "/" + viewVersion + "/" + instanceName;
+
+        LOG.error(msg, e);
+        throw new RuntimeException(msg);
+      }
+    }
+    return dataMigrator;
+  }
+
+  /**
+   * The data migrator implementation that copies all data without modification.
+   * Used to copy data between instances of same version.
+   */
+  public static class CopyAllDataMigrator implements ViewDataMigrator {
+    private ViewDataMigrationContext migrationContext;
+
+    public CopyAllDataMigrator(ViewDataMigrationContext migrationContext) {
+      this.migrationContext = migrationContext;
+    }
+
+    @Override
+    public boolean beforeMigration() {
+      return true;
+    }
+
+    @Override
+    public void afterMigration() {
+    }
+
+    @Override
+    public void migrateEntity(Class originEntityClass, Class currentEntityClass)
+        throws ViewDataMigrationException {
+      migrationContext.copyAllObjects(originEntityClass, currentEntityClass);
+    }
+
+    @Override
+    public void migrateInstanceData() {
+      migrationContext.copyAllInstanceData();
+    }
+  }
+}

+ 5 - 2
ambari-server/src/main/java/org/apache/ambari/server/api/services/ViewInstanceService.java

@@ -58,7 +58,6 @@ public class ViewInstanceService extends BaseService {
    */
   private final ViewRegistry viewRegistry;
 
-
   // ----- Constructors ------------------------------------------------------
 
   /**
@@ -241,7 +240,11 @@ public class ViewInstanceService extends BaseService {
     return new ViewPrivilegeService(viewName, version, instanceName);
   }
 
-
+  @Path("{instanceName}/migrate")
+  public ViewDataMigrationService migrateData(@Context javax.ws.rs.core.Request request,
+                                              @PathParam ("instanceName") String instanceName) {
+    return new ViewDataMigrationService(viewName, version, instanceName);
+  }
   // ----- helper methods ----------------------------------------------------
 
   /**

+ 10 - 0
ambari-server/src/main/java/org/apache/ambari/server/orm/dao/PrivilegeDAO.java

@@ -165,4 +165,14 @@ public class PrivilegeDAO {
   public void remove(PrivilegeEntity entity) {
     entityManagerProvider.get().remove(merge(entity));
   }
+
+  /**
+   * Detach an instance from manager.
+   *
+   * @param entity  entity to detach
+   */
+  @Transactional
+  public void detach(PrivilegeEntity entity) {
+    entityManagerProvider.get().detach(entity);
+  }
 }

+ 49 - 3
ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java

@@ -43,18 +43,23 @@ import javax.persistence.TableGenerator;
 import javax.persistence.Transient;
 import javax.persistence.UniqueConstraint;
 
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.security.SecurityHelper;
 import org.apache.ambari.server.security.SecurityHelperImpl;
 import org.apache.ambari.server.security.authorization.AmbariAuthorizationFilter;
+import org.apache.ambari.server.view.ViewContextImpl;
+import org.apache.ambari.server.view.ViewRegistry;
 import org.apache.ambari.server.view.configuration.InstanceConfig;
 import org.apache.ambari.server.view.validation.InstanceValidationResultImpl;
 import org.apache.ambari.server.view.validation.ValidationException;
 import org.apache.ambari.server.view.validation.ValidationResultImpl;
+import org.apache.ambari.view.*;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrator;
 import org.apache.ambari.view.validation.Validator;
-import org.apache.ambari.view.ResourceProvider;
-import org.apache.ambari.view.ViewDefinition;
-import org.apache.ambari.view.ViewInstanceDefinition;
 import org.apache.ambari.view.validation.ValidationResult;
 
 /**
@@ -216,6 +221,11 @@ public class ViewInstanceEntity implements ViewInstanceDefinition {
   @Transient
   private SecurityHelper securityHelper = SecurityHelperImpl.getInstance();
 
+  /**
+   * The view data migrator.
+   */
+  @Transient
+  private ViewDataMigrator dataMigrator;
 
   // ----- Constructors ------------------------------------------------------
 
@@ -797,6 +807,42 @@ public class ViewInstanceEntity implements ViewInstanceDefinition {
     this.resource = resource;
   }
 
+  /**
+   * Get the data migrator instance for view instance.
+   *
+   * @param dataMigrationContext  the data migration context to inject into migrator instance.
+   * @return  the data migrator.
+   * @throws ClassNotFoundException  if class defined in the archive could not be loaded
+   */
+  public ViewDataMigrator getDataMigrator(ViewDataMigrationContext dataMigrationContext)
+      throws ClassNotFoundException {
+    if (view != null) {
+      if (dataMigrator == null && view.getConfiguration().getDataMigrator() != null) {
+        ClassLoader cl = view.getClassLoader();
+        dataMigrator = getDataMigrator(view.getConfiguration().getDataMigratorClass(cl),
+                                       new ViewContextImpl(view, ViewRegistry.getInstance()),
+                                       dataMigrationContext);
+      }
+    }
+    return dataMigrator;
+  }
+
+  // get the data migrator class; inject a migration and view contexts
+  private static ViewDataMigrator getDataMigrator(Class<? extends ViewDataMigrator> clazz,
+                                                  final ViewContext viewContext,
+                                                  final ViewDataMigrationContext dataMigrationContext) {
+    Injector viewInstanceInjector = Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ViewContext.class)
+                .toInstance(viewContext);
+        bind(ViewDataMigrationContext.class)
+            .toInstance(dataMigrationContext);
+      }
+    });
+    return viewInstanceInjector.getInstance(clazz);
+  }
+
   /**
    * Validate the state of the instance.
    *

+ 258 - 0
ambari-server/src/main/java/org/apache/ambari/server/view/ViewDataMigrationContextImpl.java

@@ -0,0 +1,258 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.view;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.apache.ambari.server.orm.entities.ViewEntity;
+import org.apache.ambari.server.orm.entities.ViewInstanceDataEntity;
+import org.apache.ambari.server.orm.entities.ViewInstanceEntity;
+import org.apache.ambari.server.view.configuration.EntityConfig;
+import org.apache.ambari.server.view.configuration.PersistenceConfig;
+import org.apache.ambari.server.view.persistence.DataStoreImpl;
+import org.apache.ambari.server.view.persistence.DataStoreModule;
+import org.apache.ambari.view.DataStore;
+import org.apache.ambari.view.migration.EntityConverter;
+import org.apache.ambari.view.PersistenceException;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.BeanUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * View data migration context implementation.
+ */
+public class ViewDataMigrationContextImpl implements ViewDataMigrationContext {
+
+  /**
+   * Logger.
+   */
+  private static final Log LOG = LogFactory.getLog(ViewDataMigrationContextImpl.class);
+
+  /**
+   * The data store of origin(source) view instance with source data.
+   */
+  private DataStore originDataStore;
+
+  /**
+   * The data store of current(target) view instance to store data.
+   */
+  private DataStore currentDataStore;
+
+  /**
+   * The origin view instance definition.
+   */
+  private final ViewInstanceEntity originInstanceDefinition;
+
+  /**
+   * The current view instance definition.
+   */
+  private final ViewInstanceEntity currentInstanceDefinition;
+
+  /**
+   * Constructor.
+   *
+   * @param originInstanceDefinition    the origin view instance definition
+   * @param currentInstanceDefinition   the current view instance definition
+   */
+  public ViewDataMigrationContextImpl(ViewInstanceEntity originInstanceDefinition,
+                                      ViewInstanceEntity currentInstanceDefinition) {
+    this.originInstanceDefinition = originInstanceDefinition;
+    this.currentInstanceDefinition = currentInstanceDefinition;
+  }
+
+  /**
+   * Instantiates the data store associated with the instance.
+   *
+   * @param instanceDefinition the view instance definition
+   * @return the data store object associated with view instance
+   */
+  protected DataStore getDataStore(ViewInstanceEntity instanceDefinition) {
+    Injector originInjector = Guice.createInjector(new DataStoreModule(instanceDefinition));
+    return originInjector.getInstance(DataStoreImpl.class);
+  }
+
+  @Override
+  public int getCurrentDataVersion() {
+    return currentInstanceDefinition.getViewEntity().getConfiguration().getDataVersion();
+  }
+
+  @Override
+  public int getOriginDataVersion() {
+    return originInstanceDefinition.getViewEntity().getConfiguration().getDataVersion();
+  }
+
+  @Override
+  public DataStore getOriginDataStore() {
+    if (originDataStore == null) {
+      originDataStore = getDataStore(originInstanceDefinition);
+    }
+    return originDataStore;
+  }
+
+  @Override
+  public DataStore getCurrentDataStore() {
+    if (currentDataStore == null) {
+      currentDataStore = getDataStore(currentInstanceDefinition);
+    }
+    return currentDataStore;
+  }
+
+  @Override
+  public void putCurrentInstanceData(String user, String key, String value) {
+    putInstanceData(currentInstanceDefinition, user, key, value);
+  }
+
+  @Override
+  public void copyAllObjects(Class originEntityClass, Class currentEntityClass)
+      throws ViewDataMigrationException {
+    copyAllObjects(originEntityClass, currentEntityClass, new EntityConverter() {
+      @Override
+      public void convert(Object orig, Object dest) {
+          BeanUtils.copyProperties(orig, dest);
+      }
+    });
+  }
+
+  @Override
+  public void copyAllObjects(Class originEntityClass, Class currentEntityClass, EntityConverter entityConverter)
+      throws ViewDataMigrationException {
+    try{
+      for (Object origInstance : getOriginDataStore().findAll(originEntityClass, null)) {
+        Object newInstance = currentEntityClass.newInstance();
+        entityConverter.convert(origInstance, newInstance);
+        getCurrentDataStore().store(newInstance);
+      }
+    } catch (PersistenceException | InstantiationException | IllegalAccessException e) {
+      String msg = "Error occured during copying data. Persistence entities are not compatible.";
+      LOG.error(msg);
+      throw new ViewDataMigrationException(msg, e);
+    }
+  }
+
+  @Override
+  public void copyAllInstanceData() {
+    for (Map.Entry<String, Map<String, String>> userData : getOriginInstanceDataByUser().entrySet()) {
+      for (Map.Entry<String, String> entry : userData.getValue().entrySet()) {
+        putCurrentInstanceData(userData.getKey(), entry.getKey(), entry.getValue());
+      }
+    }
+  }
+
+  @Override
+  public ViewInstanceEntity getOriginInstanceDefinition() {
+    return originInstanceDefinition;
+  }
+
+  @Override
+  public Map<String, Class> getOriginEntityClasses() {
+    ViewEntity viewDefinition = originInstanceDefinition.getViewEntity();
+    return getPersistenceClassesOfView(viewDefinition);
+  }
+
+  @Override
+  public Map<String, Class> getCurrentEntityClasses() {
+    ViewEntity viewDefinition = currentInstanceDefinition.getViewEntity();
+    return getPersistenceClassesOfView(viewDefinition);
+  }
+
+  /**
+   * Get persistence entities of the view instance.
+   *
+   * @param viewDefinition   the view definition.
+   * @return the mapping of entity class name to the class objects,
+   * loaded by the classloader of view version.
+   */
+  private static Map<String, Class> getPersistenceClassesOfView(ViewEntity viewDefinition) {
+    PersistenceConfig persistence = viewDefinition.getConfiguration().getPersistence();
+
+    HashMap<String, Class> classes = new HashMap<>();
+    for (EntityConfig c : persistence.getEntities()) {
+      try {
+        Class entity = viewDefinition.getClassLoader().loadClass(c.getClassName());
+        classes.put(c.getClassName(), entity);
+      } catch (ClassNotFoundException e) {
+        e.printStackTrace();
+      }
+    }
+    return classes;
+  }
+
+  @Override
+  public ViewInstanceEntity getCurrentInstanceDefinition() {
+    return currentInstanceDefinition;
+  }
+
+  @Override
+  public Map<String, Map<String, String>> getOriginInstanceDataByUser() {
+    return getInstanceDataByUser(originInstanceDefinition);
+  }
+
+  @Override
+  public void putOriginInstanceData(String user, String key, String value) {
+    putInstanceData(originInstanceDefinition, user, key, value);
+  }
+
+  @Override
+  public Map<String, Map<String, String>> getCurrentInstanceDataByUser() {
+    return getInstanceDataByUser(currentInstanceDefinition);
+  }
+
+  /**
+   * Save an instance data value for the given key owned by given user.
+   *
+   * @param instanceDefinition  the view instance definition
+   * @param user                the owner of the data value
+   * @param name                the name
+   * @param value               the value
+   */
+  private static void putInstanceData(ViewInstanceEntity instanceDefinition, String user, String name, String value) {
+    ViewInstanceDataEntity viewInstanceDataEntity = new ViewInstanceDataEntity();
+    viewInstanceDataEntity.setViewName(instanceDefinition.getViewName());
+    viewInstanceDataEntity.setViewInstanceName(instanceDefinition.getName());
+    viewInstanceDataEntity.setName(name);
+    viewInstanceDataEntity.setUser(user);
+    viewInstanceDataEntity.setValue(value);
+    viewInstanceDataEntity.setViewInstanceEntity(instanceDefinition);
+
+    instanceDefinition.getData().add(viewInstanceDataEntity);
+  }
+
+  /**
+   * Get the instance data in the mapping of user owning data to the key-value data.
+   *
+   * @param instanceDefinition   the view instance definition
+   * @return mapping of the data owner to the instance data entries
+   */
+  private static Map<String, Map<String, String>> getInstanceDataByUser(ViewInstanceEntity instanceDefinition) {
+    Map<String, Map<String, String>> instanceDataByUser = new HashMap<>();
+    for (ViewInstanceDataEntity entity : instanceDefinition.getData()) {
+
+      if (!instanceDataByUser.containsKey(entity.getUser())) {
+        instanceDataByUser.put(entity.getUser(), new HashMap<String, String>());
+      }
+      instanceDataByUser.get(entity.getUser()).put(entity.getName(), entity.getValue());
+    }
+    return  instanceDataByUser;
+  }
+}

+ 29 - 0
ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java

@@ -669,6 +669,35 @@ public class ViewRegistry {
     instanceDAO.merge(instanceEntity);
   }
 
+  /**
+   * Copy all privileges from one view instance to another
+   *
+   * @param sourceInstanceEntity  the source instance entity
+   * @param targetInstanceEntity  the target instance entity
+   */
+  @Transactional
+  public void copyPrivileges(ViewInstanceEntity sourceInstanceEntity,
+                             ViewInstanceEntity targetInstanceEntity) {
+    LOG.debug("Copy all privileges from " + sourceInstanceEntity.getName() + " to " +
+              targetInstanceEntity.getName());
+    List<PrivilegeEntity> targetInstancePrivileges = privilegeDAO.findByResourceId(targetInstanceEntity.getResource().getId());
+    if (targetInstancePrivileges.size() > 0) {
+      LOG.warn("Old privileges will be removed for " + targetInstanceEntity.getName());
+      for (PrivilegeEntity privilegeEntity : targetInstancePrivileges) {
+        removePrivilegeEntity(privilegeEntity);
+      }
+    }
+
+    List<PrivilegeEntity> sourceInstancePrivileges = privilegeDAO.findByResourceId(sourceInstanceEntity.getResource().getId());
+    for (PrivilegeEntity privilegeEntity : sourceInstancePrivileges) {
+      privilegeDAO.detach(privilegeEntity);
+      privilegeEntity.setResource(targetInstanceEntity.getResource());
+      privilegeEntity.setId(null);
+      privilegeDAO.create(privilegeEntity);
+      privilegeEntity.getPrincipal().getPrivileges().add(privilegeEntity);
+    }
+  }
+
   /**
    * Notify any registered listeners of the given event.
    *

+ 52 - 0
ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java

@@ -19,6 +19,7 @@
 package org.apache.ambari.server.view.configuration;
 
 import org.apache.ambari.server.view.DefaultMasker;
+import org.apache.ambari.view.migration.ViewDataMigrator;
 import org.apache.ambari.view.validation.Validator;
 import org.apache.ambari.view.Masker;
 import org.apache.ambari.view.View;
@@ -109,6 +110,23 @@ public class ViewConfig {
    */
   private Class<? extends View> viewClass = null;
 
+  /**
+   * The main view class name.
+   */
+  @XmlElement(name="data-migrator-class")
+  private String dataMigrator;
+
+  /**
+   * The main view class name.
+   */
+  @XmlElement(name="data-version")
+  private String dataVersion;
+
+  /**
+   * The view class.
+   */
+  private Class<? extends ViewDataMigrator> dataMigratorClass = null;
+
   /**
    * The main view class name.
    */
@@ -292,6 +310,40 @@ public class ViewConfig {
     return viewClass;
   }
 
+  /**
+   * Get the view class name.
+   *
+   * @return the view class name
+   */
+  public String getDataMigrator() {
+    return dataMigrator;
+  }
+
+  /**
+   * Get the view class.
+   *
+   * @param cl the class loader
+   *
+   * @return the view class
+   *
+   * @throws ClassNotFoundException if the class can not be loaded
+   */
+  public Class<? extends ViewDataMigrator> getDataMigratorClass(ClassLoader cl) throws ClassNotFoundException {
+    if (dataMigratorClass == null) {
+      dataMigratorClass = cl.loadClass(dataMigrator).asSubclass(ViewDataMigrator.class);
+    }
+    return dataMigratorClass;
+  }
+
+  /**
+   * Get the view data version. If not specified, data version is 0.
+   *
+   * @return the data version
+   */
+  public int getDataVersion() {
+    return (dataVersion == null) ? 0 : Integer.valueOf(dataVersion);
+  }
+
   /**
    * Get the view validator class name.
    *

+ 215 - 0
ambari-server/src/test/java/org/apache/ambari/server/api/services/ViewDataMigrationServiceTest.java

@@ -0,0 +1,215 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server.api.services;
+
+import junit.framework.Assert;
+import org.apache.ambari.server.orm.entities.ViewEntity;
+import org.apache.ambari.server.orm.entities.ViewInstanceEntity;
+import org.apache.ambari.server.view.ViewDataMigrationContextImpl;
+import org.apache.ambari.server.view.ViewRegistry;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.ambari.view.migration.ViewDataMigrator;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.easymock.EasyMock.*;
+
+/**
+ * ViewDataMigrationService tests.
+ */
+public class ViewDataMigrationServiceTest {
+
+  private static String viewName = "MY_VIEW";
+  private static String instanceName = "INSTANCE1";
+  private static String version1 = "1.0.0";
+  private static String version2 = "2.0.0";
+
+  private static String xml_view_with_migrator_v2 = "<view>\n" +
+      "    <name>" + viewName + "</name>\n" +
+      "    <label>My View!</label>\n" +
+      "    <version>" + version2 + "</version>\n" +
+      "    <data-version>1</data-version>\n" +
+      "    <data-migrator-class>org.apache.ambari.server.api.services.ViewDataMigrationServiceTest$MyDataMigrator</data-migrator-class>\n" +
+      "    <instance>\n" +
+      "        <name>" + instanceName + "</name>\n" +
+      "        <label>My Instance 1!</label>\n" +
+      "    </instance>\n" +
+      "</view>";
+
+  private static String xml_view_with_migrator_v1 = "<view>\n" +
+      "    <name>" + viewName + "</name>\n" +
+      "    <label>My View!</label>\n" +
+      "    <version>" + version1 + "</version>\n" +
+      "    <instance>\n" +
+      "        <name>" + instanceName + "</name>\n" +
+      "        <label>My Instance 1!</label>\n" +
+      "    </instance>\n" +
+      "</view>";
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Before
+  public void setUp() throws Exception {
+    ViewRegistry viewRegistry = createNiceMock(ViewRegistry.class);
+    expect(viewRegistry.checkAdmin()).andReturn(true).anyTimes();
+    viewRegistry.copyPrivileges(anyObject(ViewInstanceEntity.class), anyObject(ViewInstanceEntity.class));
+    expectLastCall().anyTimes();
+    replay(viewRegistry);
+    ViewRegistry.initInstance(viewRegistry);
+  }
+
+  @Test
+  public void testMigrateDataSameVersions() throws Exception {
+    TestViewDataMigrationService service = new TestViewDataMigrationService(viewName, version2, instanceName);
+
+    ViewDataMigrationContextImpl context = createNiceMock(ViewDataMigrationContextImpl.class);
+    expect(context.getOriginDataVersion()).andReturn(42);
+    expect(context.getCurrentDataVersion()).andReturn(42);
+    replay(context);
+    service.setMigrationContext(context);
+
+    ViewDataMigrator migrator =  service.getViewDataMigrator(
+        service.getViewInstanceEntity(viewName, version2, instanceName), context);
+
+    Assert.assertTrue(migrator instanceof ViewDataMigrationService.CopyAllDataMigrator);
+  }
+
+  @Test
+  public void testMigrateDataDifferentVersions() throws Exception {
+    TestViewDataMigrationService service = new TestViewDataMigrationService(viewName, version2, instanceName);
+
+    ViewDataMigrationContextImpl context = getViewDataMigrationContext();
+    service.setMigrationContext(context);
+
+    ViewDataMigrator migrator = createStrictMock(ViewDataMigrator.class);
+    expect(migrator.beforeMigration()).andReturn(true);
+    migrator.migrateEntity(anyObject(Class.class), anyObject(Class.class)); expectLastCall();
+    migrator.migrateInstanceData(); expectLastCall();
+    migrator.afterMigration(); expectLastCall();
+
+    replay(migrator);
+    service.setMigrator(migrator);
+
+    service.migrateData(version1, instanceName);
+
+    verify(migrator);
+  }
+
+  @Test
+  public void testMigrateDataDifferentVersionsCancel() throws Exception {
+    TestViewDataMigrationService service = new TestViewDataMigrationService(viewName, version2, instanceName);
+
+    ViewDataMigrationContextImpl context = getViewDataMigrationContext();
+    service.setMigrationContext(context);
+
+    ViewDataMigrator migrator = createStrictMock(ViewDataMigrator.class);
+    expect(migrator.beforeMigration()).andReturn(false);
+
+    replay(migrator);
+    service.setMigrator(migrator);
+
+    thrown.expect(ViewDataMigrationException.class);
+    service.migrateData(version1, instanceName);
+  }
+
+  private static ViewDataMigrationContextImpl getViewDataMigrationContext() {
+    Map<String, Class> entities = Collections.<String, Class>singletonMap("MyEntityClass", Object.class);
+    ViewDataMigrationContextImpl context = createNiceMock(ViewDataMigrationContextImpl.class);
+    expect(context.getOriginDataVersion()).andReturn(2).anyTimes();
+    expect(context.getCurrentDataVersion()).andReturn(1).anyTimes();
+    expect(context.getOriginEntityClasses()).andReturn(entities).anyTimes();
+    expect(context.getCurrentEntityClasses()).andReturn(entities).anyTimes();
+    replay(context);
+    return context;
+  }
+
+  //Migration service that avoids ViewRegistry and DB calls
+  private static class TestViewDataMigrationService extends ViewDataMigrationService {
+
+    private ViewDataMigrator migrator;
+    private ViewDataMigrationContextImpl migrationContext;
+
+    public TestViewDataMigrationService(String viewName, String viewVersion, String instanceName) {
+      super(viewName, viewVersion, instanceName);
+    }
+
+    @Override
+    protected ViewInstanceEntity getViewInstanceEntity(String viewName, String viewVersion, String instanceName) {
+      ViewEntity viewEntity = createNiceMock(ViewEntity.class);
+      expect(viewEntity.getViewName()).andReturn(viewName);
+      expect(viewEntity.getVersion()).andReturn(viewVersion);
+
+      replay(viewEntity);
+
+      ViewInstanceEntity instanceEntity = createNiceMock(ViewInstanceEntity.class);
+      expect(instanceEntity.getViewEntity()).andReturn(viewEntity);
+      expect(instanceEntity.getViewName()).andReturn(viewName);
+      expect(instanceEntity.getInstanceName()).andReturn(instanceName);
+
+      try {
+        ViewDataMigrator mockMigrator;
+        if (migrator == null) {
+          mockMigrator = createNiceMock(ViewDataMigrator.class);
+        } else {
+          mockMigrator = migrator;
+        }
+        expect(instanceEntity.getDataMigrator(anyObject(ViewDataMigrationContext.class))).
+            andReturn(mockMigrator);
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+
+      replay(instanceEntity);
+      return instanceEntity;
+    }
+
+    @Override
+    protected ViewDataMigrationContextImpl getViewDataMigrationContext(ViewInstanceEntity instanceDefinition,
+                                                                       ViewInstanceEntity originInstanceDefinition) {
+      if (migrationContext == null) {
+        ViewDataMigrationContextImpl contextMock = createNiceMock(ViewDataMigrationContextImpl.class);
+        replay(contextMock);
+        return contextMock;
+      }
+      return migrationContext;
+    }
+
+    public ViewDataMigrator getMigrator() {
+      return migrator;
+    }
+
+    public void setMigrator(ViewDataMigrator migrator) {
+      this.migrator = migrator;
+    }
+
+    public ViewDataMigrationContextImpl getMigrationContext() {
+      return migrationContext;
+    }
+
+    public void setMigrationContext(ViewDataMigrationContextImpl migrationContext) {
+      this.migrationContext = migrationContext;
+    }
+  }
+}

+ 66 - 0
ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java

@@ -22,6 +22,7 @@ import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.security.SecurityHelper;
 import org.apache.ambari.server.security.authorization.AmbariAuthorizationFilter;
+import org.apache.ambari.server.view.ViewDataMigrationContextImpl;
 import org.apache.ambari.server.view.ViewRegistryTest;
 import org.apache.ambari.server.view.configuration.InstanceConfig;
 import org.apache.ambari.server.view.configuration.InstanceConfigTest;
@@ -31,6 +32,9 @@ import org.apache.ambari.server.view.validation.InstanceValidationResultImpl;
 import org.apache.ambari.server.view.validation.ValidationException;
 import org.apache.ambari.server.view.validation.ValidationResultImpl;
 import org.apache.ambari.view.ResourceProvider;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.ambari.view.migration.ViewDataMigrator;
 import org.apache.ambari.view.validation.ValidationResult;
 import org.apache.ambari.view.validation.Validator;
 import org.junit.Assert;
@@ -129,6 +133,28 @@ public class ViewInstanceEntityTest {
       "    </instance>\n" +
       "</view>";
 
+  private static String xml_view_with_migrator_v2 = "<view>\n" +
+      "    <name>MY_VIEW</name>\n" +
+      "    <label>My View!</label>\n" +
+      "    <version>2.0.0</version>\n" +
+      "    <data-version>1</data-version>\n" +
+      "    <data-migrator-class>org.apache.ambari.server.orm.entities.ViewInstanceEntityTest$MyDataMigrator</data-migrator-class>\n" +
+      "    <instance>\n" +
+      "        <name>INSTANCE1</name>\n" +
+      "        <label>My Instance 1!</label>\n" +
+      "    </instance>\n" +
+      "</view>";
+
+  private static String xml_view_with_migrator_v1 = "<view>\n" +
+      "    <name>MY_VIEW</name>\n" +
+      "    <label>My View!</label>\n" +
+      "    <version>1.0.0</version>\n" +
+      "    <instance>\n" +
+      "        <name>INSTANCE1</name>\n" +
+      "        <label>My Instance 1!</label>\n" +
+      "    </instance>\n" +
+      "</view>";
+
   private static String XML_CONFIG_INSTANCE = "<view>\n" +
       "    <name>MY_VIEW</name>\n" +
       "    <label>My View!</label>\n" +
@@ -442,6 +468,27 @@ public class ViewInstanceEntityTest {
     viewInstanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
   }
 
+  @Test
+  public void testDataMigrator() throws Exception {
+    Configuration ambariConfig = new Configuration();
+
+    ViewConfig config2 = ViewConfigTest.getConfig(xml_view_with_migrator_v2);
+    ViewEntity viewEntity2 = ViewRegistryTest.getViewEntity(config2, ambariConfig, getClass().getClassLoader(), "");
+    ViewInstanceEntity viewInstanceEntity2 = ViewRegistryTest.getViewInstanceEntity(viewEntity2, config2.getInstances().get(0));
+
+    ViewConfig config1 = ViewConfigTest.getConfig(xml_view_with_migrator_v1);
+    ViewEntity viewEntity1 = ViewRegistryTest.getViewEntity(config1, ambariConfig, getClass().getClassLoader(), "");
+    ViewInstanceEntity viewInstanceEntity1 = ViewRegistryTest.getViewInstanceEntity(viewEntity1, config1.getInstances().get(0));
+
+    ViewDataMigrationContext context = new ViewDataMigrationContextImpl(viewInstanceEntity1, viewInstanceEntity2);
+    ViewDataMigrator migrator2 = viewInstanceEntity2.getDataMigrator(context);
+    Assert.assertTrue(migrator2 instanceof MyDataMigrator);
+
+    //in the view xml version 1 migrator is not defined
+    ViewDataMigrator migrator1 = viewInstanceEntity1.getDataMigrator(context);
+    Assert.assertNull(migrator1);
+  }
+
   @Test
   public void testValidateWithClusterConfig() throws Exception {
 
@@ -555,6 +602,25 @@ public class ViewInstanceEntityTest {
     return viewInstanceEntity;
   }
 
+  public static class MyDataMigrator implements ViewDataMigrator {
+    @Override
+    public boolean beforeMigration() throws ViewDataMigrationException {
+      return true;
+    }
+
+    @Override
+    public void afterMigration() throws ViewDataMigrationException {
+    }
+
+    @Override
+    public void migrateEntity(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException {
+    }
+
+    @Override
+    public void migrateInstanceData() throws ViewDataMigrationException {
+    }
+  }
+
   protected static class TestSecurityHelper implements SecurityHelper {
 
     private String user;

+ 403 - 0
ambari-server/src/test/java/org/apache/ambari/server/view/ViewDataMigrationContextImplTest.java

@@ -0,0 +1,403 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.server.view;
+
+import junit.framework.Assert;
+import org.apache.ambari.server.orm.entities.ViewEntity;
+import org.apache.ambari.server.orm.entities.ViewInstanceDataEntity;
+import org.apache.ambari.server.orm.entities.ViewInstanceEntity;
+import org.apache.ambari.server.view.configuration.EntityConfig;
+import org.apache.ambari.server.view.configuration.PersistenceConfig;
+import org.apache.ambari.server.view.configuration.ViewConfig;
+import org.apache.ambari.view.DataStore;
+import org.apache.ambari.view.migration.EntityConverter;
+import org.easymock.Capture;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+import static org.easymock.EasyMock.*;
+
+/**
+ * ViewDataMigrationContextImpl tests.
+ */
+public class ViewDataMigrationContextImplTest {
+
+  public static final String VERSION_1 = "1.0.0";
+  public static final String VERSION_2 = "2.0.0";
+  public static final String INSTANCE = "INSTANCE_1";
+  public static final String VIEW_NAME = "MY_VIEW";
+
+  @Test
+  public void getDataVersion() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+
+    ViewConfig config1 = createNiceMock(ViewConfig.class);
+    expect(config1.getDataVersion()).andReturn(41);
+    ViewConfig config2 = createNiceMock(ViewConfig.class);
+    expect(config2.getDataVersion()).andReturn(42);
+    replay(config1, config2);
+
+    expect(entity1.getConfiguration()).andReturn(config1);
+    expect(entity2.getConfiguration()).andReturn(config2);
+    replay(entity1, entity2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+
+    Assert.assertEquals(41, context.getOriginDataVersion());
+    Assert.assertEquals(42, context.getCurrentDataVersion());
+  }
+
+  @Test
+  public void getDataStore() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+
+    Assert.assertNotSame(context.getCurrentDataStore(), context.getOriginDataStore());
+  }
+
+  @Test
+  public void putCurrentInstanceData() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    Capture<ViewInstanceDataEntity> capturedInstanceData1 = Capture.newInstance();
+    Collection data1 = createNiceMock(Collection.class);
+    expect(data1.add(capture(capturedInstanceData1))).andReturn(true);
+    replay(data1);
+
+    Capture<ViewInstanceDataEntity> capturedInstanceData2 = Capture.newInstance();
+    Collection data2 = createStrictMock(Collection.class);
+    expect(data2.add(capture(capturedInstanceData2))).andReturn(true);
+    replay(data2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    expect(instanceEntity1.getData()).andReturn(data1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    expect(instanceEntity2.getData()).andReturn(data2);
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+    context.putOriginInstanceData("user1", "key1", "val1");
+    context.putCurrentInstanceData("user2", "key2", "val2");
+
+    verify(data2);
+    Assert.assertEquals("user1", capturedInstanceData1.getValue().getUser());
+    Assert.assertEquals("key1", capturedInstanceData1.getValue().getName());
+    Assert.assertEquals("val1", capturedInstanceData1.getValue().getValue());
+
+    Assert.assertEquals("user2", capturedInstanceData2.getValue().getUser());
+    Assert.assertEquals("key2", capturedInstanceData2.getValue().getName());
+    Assert.assertEquals("val2", capturedInstanceData2.getValue().getValue());
+  }
+
+  @Test
+  public void copyAllObjects() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    replay(instanceEntity1, instanceEntity2);
+
+    TestViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+
+    DataStore dataStore1 = createStrictMock(DataStore.class);
+    expect(dataStore1.findAll(eq(SampleEntity.class), (String) isNull())).andReturn(
+        Arrays.asList(new SampleEntity("data1"), new SampleEntity("data2")));
+    replay(dataStore1);
+
+    DataStore dataStore2 = createStrictMock(DataStore.class);
+    Capture<SampleEntity> copiedEntity1 = Capture.newInstance();
+    Capture<SampleEntity> copiedEntity2 = Capture.newInstance();
+
+    dataStore2.store(capture(copiedEntity1)); expectLastCall();
+    dataStore2.store(capture(copiedEntity2)); expectLastCall();
+    replay(dataStore2);
+    context.setMockOriginDataStore(dataStore1);
+    context.setMockCurrentDataStore(dataStore2);
+
+    context.copyAllObjects(SampleEntity.class, SampleEntity.class);
+
+    verify(dataStore1);
+    verify(dataStore2);
+
+    Assert.assertEquals("data1", copiedEntity1.getValue().getField());
+    Assert.assertEquals("data2", copiedEntity2.getValue().getField());
+  }
+
+  @Test
+  public void copyAllObjectsWithCustomConverter() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    replay(instanceEntity1, instanceEntity2);
+
+    TestViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+
+    DataStore dataStore1 = createStrictMock(DataStore.class);
+    SampleEntity sampleEntity1 = new SampleEntity("data1");
+    SampleEntity sampleEntity2 = new SampleEntity("data2");
+    expect(dataStore1.findAll(eq(SampleEntity.class), (String) isNull())).andReturn(
+        Arrays.asList(sampleEntity1, sampleEntity2));
+    replay(dataStore1);
+
+    DataStore dataStore2 = createStrictMock(DataStore.class);
+    Capture<SampleEntity> copiedEntity1 = Capture.newInstance();
+    Capture<SampleEntity> copiedEntity2 = Capture.newInstance();
+
+    Capture<SampleEntity> convertedEntity1 = Capture.newInstance();
+    Capture<SampleEntity> convertedEntity2 = Capture.newInstance();
+
+    dataStore2.store(capture(copiedEntity1)); expectLastCall();
+    dataStore2.store(capture(copiedEntity2)); expectLastCall();
+    replay(dataStore2);
+    context.setMockOriginDataStore(dataStore1);
+    context.setMockCurrentDataStore(dataStore2);
+
+    EntityConverter converter = createStrictMock(EntityConverter.class);
+    converter.convert(eq(sampleEntity1), capture(convertedEntity1)); expectLastCall();
+    converter.convert(eq(sampleEntity2), capture(convertedEntity2)); expectLastCall();
+    replay(converter);
+
+    context.copyAllObjects(SampleEntity.class, SampleEntity.class, converter);
+
+    verify(dataStore1);
+    verify(dataStore2);
+    verify(converter);
+    Assert.assertSame(copiedEntity1.getValue(), convertedEntity1.getValue());
+    Assert.assertSame(copiedEntity2.getValue(), convertedEntity2.getValue());
+  }
+
+  @Test
+  public void copyAllInstanceData() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    ViewInstanceDataEntity dataEntity = new ViewInstanceDataEntity();
+    dataEntity.setName("name1");
+    dataEntity.setValue("value1");
+    dataEntity.setUser("user1");
+    Collection data1 = Arrays.asList(dataEntity);
+
+    Capture<ViewInstanceDataEntity> capturedInstanceData = Capture.newInstance();
+    Collection data2 = createStrictMock(Collection.class);
+    expect(data2.add(capture(capturedInstanceData))).andReturn(true);
+    replay(data2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    expect(instanceEntity1.getData()).andReturn(data1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    expect(instanceEntity2.getData()).andReturn(data2);
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+    context.copyAllInstanceData();
+
+    verify(data2);
+    Assert.assertEquals("user1", capturedInstanceData.getValue().getUser());
+    Assert.assertEquals("name1", capturedInstanceData.getValue().getName());
+    Assert.assertEquals("value1", capturedInstanceData.getValue().getValue());
+  }
+
+  @Test
+  public void getEntityClasses() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+
+    EntityConfig entityConfig = createNiceMock(EntityConfig.class);
+    expect(entityConfig.getClassName()).andReturn(SampleEntity.class.getName()).anyTimes();
+    replay(entityConfig);
+
+    PersistenceConfig persistenceConfig1 = createStrictMock(PersistenceConfig.class);
+    expect(persistenceConfig1.getEntities()).andReturn(Arrays.asList(entityConfig));
+    PersistenceConfig persistenceConfig2 = createStrictMock(PersistenceConfig.class);
+    expect(persistenceConfig2.getEntities()).andReturn(Arrays.asList(entityConfig));
+    replay(persistenceConfig1, persistenceConfig2);
+
+    ViewConfig config1 = createNiceMock(ViewConfig.class);
+    expect(config1.getPersistence()).andReturn(persistenceConfig1);
+    ViewConfig config2 = createNiceMock(ViewConfig.class);
+    expect(config2.getPersistence()).andReturn(persistenceConfig2);
+    replay(config1, config2);
+
+    expect(entity1.getConfiguration()).andReturn(config1);
+    expect(entity2.getConfiguration()).andReturn(config2);
+    replay(entity1, entity2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+
+    Map<String, Class> current = context.getCurrentEntityClasses();
+    Assert.assertEquals(1, current.size());
+    Assert.assertEquals(SampleEntity.class.getName(), current.entrySet().iterator().next().getKey());
+    Assert.assertEquals(SampleEntity.class, current.entrySet().iterator().next().getValue());
+
+    Map<String, Class> origin = context.getOriginEntityClasses();
+    Assert.assertEquals(1, origin.size());
+    Assert.assertEquals(SampleEntity.class.getName(), origin.entrySet().iterator().next().getKey());
+    Assert.assertEquals(SampleEntity.class, origin.entrySet().iterator().next().getValue());
+  }
+
+  @Test
+  public void getInstanceDataByUser() throws Exception {
+    ViewEntity entity1 = getViewEntityMock(VERSION_1);
+    ViewEntity entity2 = getViewEntityMock(VERSION_2);
+    replay(entity1, entity2);
+
+    ViewInstanceDataEntity dataEntityUser1 = new ViewInstanceDataEntity();
+    dataEntityUser1.setName("key1");
+    dataEntityUser1.setUser("user1");
+    ViewInstanceDataEntity dataEntityUser2 = new ViewInstanceDataEntity();
+    dataEntityUser2.setName("key1");
+    dataEntityUser2.setUser("user2");
+    ViewInstanceDataEntity dataEntity2User2 = new ViewInstanceDataEntity();
+    dataEntity2User2.setName("key2");
+    dataEntity2User2.setUser("user2");
+    Collection data2 = Arrays.asList(dataEntityUser2, dataEntity2User2);
+    Collection data1 = Arrays.asList(dataEntityUser1, dataEntityUser2, dataEntity2User2);
+
+    ViewInstanceEntity instanceEntity1 = getViewInstanceEntityMock(entity1);
+    expect(instanceEntity1.getData()).andReturn(data1);
+    ViewInstanceEntity instanceEntity2 = getViewInstanceEntityMock(entity2);
+    expect(instanceEntity2.getData()).andReturn(data2);
+    replay(instanceEntity1, instanceEntity2);
+
+    ViewDataMigrationContextImpl context = new TestViewDataMigrationContextImpl(instanceEntity1, instanceEntity2);
+    Map<String, Map<String,String>> instanceData2 = context.getCurrentInstanceDataByUser();
+    Assert.assertEquals(1, instanceData2.size());
+    Assert.assertEquals(2, instanceData2.get("user2").size());
+
+    Map<String, Map<String,String>> instanceData1 = context.getOriginInstanceDataByUser();
+    Assert.assertEquals(2, instanceData1.size());
+    Assert.assertEquals(1, instanceData1.get("user1").size());
+    Assert.assertEquals(2, instanceData1.get("user2").size());
+  }
+
+  private ViewInstanceEntity getViewInstanceEntityMock(ViewEntity viewEntity) {
+    ViewInstanceEntity instanceEntity = createNiceMock(ViewInstanceEntity.class);
+    expect(instanceEntity.getViewEntity()).andReturn(viewEntity).anyTimes();
+    expect(instanceEntity.getViewName()).andReturn(VIEW_NAME).anyTimes();
+    expect(instanceEntity.getInstanceName()).andReturn(INSTANCE).anyTimes();
+    return instanceEntity;
+  }
+
+  private ViewEntity getViewEntityMock(String version) {
+    ViewEntity viewEntity = createNiceMock(ViewEntity.class);
+    expect(viewEntity.getViewName()).andReturn(VIEW_NAME).anyTimes();
+    expect(viewEntity.getVersion()).andReturn(version).anyTimes();
+    expect(viewEntity.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+    return viewEntity;
+  }
+
+  //Avoid accessing DB
+  private static class TestViewDataMigrationContextImpl extends ViewDataMigrationContextImpl {
+    private DataStore mockOriginDataStore;
+    private DataStore mockCurrentDataStore;
+
+    public TestViewDataMigrationContextImpl(ViewInstanceEntity originInstanceDefinition,
+                                            ViewInstanceEntity currentInstanceDefinition) {
+      super(originInstanceDefinition, currentInstanceDefinition);
+    }
+
+    @Override
+    protected DataStore getDataStore(ViewInstanceEntity instanceDefinition) {
+      if (instanceDefinition.getViewEntity().getVersion().equals(VERSION_1)) {
+        if (mockOriginDataStore == null) {
+          return createDataStoreMock();
+        }
+        return mockOriginDataStore;
+      }
+
+      if (instanceDefinition.getViewEntity().getVersion().equals(VERSION_2)) {
+        if (mockCurrentDataStore == null) {
+          return createDataStoreMock();
+        }
+        return mockCurrentDataStore;
+      }
+      return null;
+    }
+
+    private DataStore createDataStoreMock() {
+      DataStore dataStoreMock = createNiceMock(DataStore.class);
+      replay(dataStoreMock);
+      return dataStoreMock;
+    }
+
+    public DataStore getMockOriginDataStore() {
+      return mockOriginDataStore;
+    }
+
+    public void setMockOriginDataStore(DataStore mockOriginDataStore) {
+      this.mockOriginDataStore = mockOriginDataStore;
+    }
+
+    public DataStore getMockCurrentDataStore() {
+      return mockCurrentDataStore;
+    }
+
+    public void setMockCurrentDataStore(DataStore mockCurrentDataStore) {
+      this.mockCurrentDataStore = mockCurrentDataStore;
+    }
+  }
+
+  private static class SampleEntity {
+    private String field;
+
+    public SampleEntity() {
+    }
+
+    public SampleEntity(String field) {
+      this.field = field;
+    }
+
+    public String getField() {
+      return field;
+    }
+
+    public void setField(String field) {
+      this.field = field;
+    }
+  }
+}

+ 45 - 0
ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java

@@ -25,6 +25,8 @@ import org.apache.ambari.view.ResourceProvider;
 import org.apache.ambari.view.SystemException;
 import org.apache.ambari.view.UnsupportedPropertyException;
 import org.apache.ambari.view.ViewInstanceDefinition;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.ambari.view.migration.ViewDataMigrator;
 import org.apache.ambari.view.validation.ValidationResult;
 import org.apache.ambari.view.validation.Validator;
 import org.junit.Assert;
@@ -51,9 +53,11 @@ public class ViewConfigTest {
       "    <description>Description</description>" +
       "    <version>1.0.0</version>\n" +
       "    <build>99</build>\n" +
+      "    <data-version>42</data-version>\n" +
       "    <system>true</system>\n" +
       "    <icon64>/this/is/the/icon/url/icon64.png</icon64>\n" +
       "    <icon>/this/is/the/icon/url/icon.png</icon>\n" +
+      "    <data-migrator-class>org.apache.ambari.server.view.configuration.ViewConfigTest$MyDataMigrator</data-migrator-class>" +
       "    <validator-class>org.apache.ambari.server.view.configuration.ViewConfigTest$MyValidator</validator-class>" +
       "    <masker-class>org.apache.ambari.server.view.DefaultMasker</masker-class>" +
       "    <parameter>\n" +
@@ -206,6 +210,15 @@ public class ViewConfigTest {
     Assert.assertEquals("99", config.getBuild());
   }
 
+  @Test
+  public void testGetDataVersion() throws Exception {
+    ViewConfig config = getConfig();
+    Assert.assertEquals(42, config.getDataVersion());
+
+    config = getConfig(minimal_xml);
+    Assert.assertEquals(0, config.getDataVersion());
+  }
+
   @Test
   public void testGetIcon() throws Exception {
     ViewConfig config = getConfig();
@@ -224,6 +237,19 @@ public class ViewConfigTest {
     Assert.assertEquals("org.apache.ambari.server.view.configuration.ViewConfigTest$MyValidator", config.getValidator());
   }
 
+  @Test
+  public void testGetDataMigrator() throws Exception {
+    ViewConfig config = getConfig();
+    Assert.assertEquals("org.apache.ambari.server.view.configuration.ViewConfigTest$MyDataMigrator", config.getDataMigrator());
+  }
+
+  @Test
+  public void testGetDataMigratorClass() throws Exception {
+    ViewConfig config = getConfig();
+    Class migrator = config.getDataMigratorClass(getClass().getClassLoader());
+    Assert.assertEquals(MyDataMigrator.class, migrator);
+  }
+
   @Test
   public void testMasker() throws Exception {
     ViewConfig config = getConfig();
@@ -355,6 +381,25 @@ public class ViewConfigTest {
     }
   }
 
+  public static class MyDataMigrator implements ViewDataMigrator {
+    @Override
+    public boolean beforeMigration() throws ViewDataMigrationException {
+      return true;
+    }
+
+    @Override
+    public void afterMigration() throws ViewDataMigrationException {
+    }
+
+    @Override
+    public void migrateEntity(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException {
+    }
+
+    @Override
+    public void migrateInstanceData() throws ViewDataMigrationException {
+    }
+  }
+
   public static class MyResource {
     private String id;
 

+ 1 - 0
ambari-views/examples/README.md

@@ -27,6 +27,7 @@ See the documentation pages for the view examples.
 * [Favorite view](favorite-view/docs/index.md) : Exposes a simple resource to work with instance parameters and data.
 * [Calculator View](calculator-view/docs/index.md) : Includes a simple resource.
 * [Phone List View](phone-list-view/docs/index.md) : Demonstrates simple view persistence.
+* [Phone List Upgrade View](phone-list-upgrade-view/docs/index.md) : Demonstrates view data migration from Phone List View.
 * [Property View](property-view/docs/index.md) : Demonstrates view configuration property options.
 * [Property Validator View](property-validator-view/docs/index.md) : Demonstrates configuration property validator.
 * [Weather view](weather-view/docs/index.md) : Demonstrates the the use of instance parameters, a servlet for a dynamic UI and a managed resource.

+ 1 - 1
ambari-views/examples/calculator-view/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>calculator-view</artifactId>

+ 2 - 2
ambari-views/examples/cluster-view/pom.xml

@@ -19,11 +19,11 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>cluster-view</artifactId>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Ambari Cluster View</name>
   <url>http://maven.apache.org</url>

+ 1 - 1
ambari-views/examples/favorite-view/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>favorite-view</artifactId>

+ 2 - 2
ambari-views/examples/hello-servlet-view/pom.xml

@@ -19,13 +19,13 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>hello-servlet-view</artifactId>
   <packaging>jar</packaging>
   <name>Ambari Hello Servlet View</name>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <url>http://maven.apache.org</url>
   <properties>
     <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>

+ 2 - 2
ambari-views/examples/hello-spring-view/pom.xml

@@ -19,13 +19,13 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>hello-spring-view</artifactId>
   <packaging>war</packaging>
   <name>Ambari Hello Spring View</name>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <url>http://maven.apache.org</url>
   <properties>
     <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>

+ 2 - 2
ambari-views/examples/helloworld-view/pom.xml

@@ -19,13 +19,13 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>helloworld-view</artifactId>
   <packaging>jar</packaging>
   <name>Ambari Hello World View</name>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <url>http://maven.apache.org</url>
   <properties>
     <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>

+ 131 - 0
ambari-views/examples/phone-list-upgrade-view/docs/index.md

@@ -0,0 +1,131 @@
+<!---
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+Phone List Upgrade View Example
+========
+Description
+-----
+The Phone List Upgrade view is upgraded version of Phone List View, that demonstrates changing the data schema
+and data migration from previous version. The new version adds support of storing user surname, in addition to
+names and phone numbers. The user may add, modify or delete numbers from the list through the view UI.
+This document also describes migration process and how to add data migration support to any view.
+
+Data migration
+-----
+
+Any view instance can have two types of persistent data: persistence entities (separate table for each one) and
+instance data (key-value storage). So the view should support migration of both types of data.
+
+To initiate the migration process, API should be called
+
+    PUT http://<server>:8080/api/v1/views/<targetView>/versions/<targetVersion>/instances/<targetInstance>/migrate/<originVersion>/<originInstance>
+
+In the case of Phone List Upgrade View, to test migration of persistence entities it would be
+
+    PUT http://<server>:8080/api/v1/views/PHONE_LIST/versions/2.0.0/instances/LIST_2/migrate/1.0.0/LIST_2
+
+And for the instance data (key-value storage):
+
+    PUT http://<server>:8080/api/v1/views/PHONE_LIST/versions/2.0.0/instances/LIST_1/migrate/1.0.0/LIST_1
+
+In order to support data migration, view should implement the ViewDataMigrator interface and define the data-version in view.xml.
+
+NOTE: Data migration for instances of same data-versions (including those which does not define data-version) IS supported
+and in fact just copies all data - the class defined in the data-migrator-class in view.xml WILL NOT be instantiated.
+
+#####view.xml
+
+View can define the data version and ViewDataMigrator implementation in the view.xml.
+
+      <view>
+        <name>PHONE_LIST</name>
+        <label>The Phone List View</label>
+        <version>2.0.0</version>
+        <data-version>1</data-version>
+        <data-migrator-class>org.apache.ambari.view.phonelist.DataMigrator</data-migrator-class>
+      </view>
+
+If data-version is not defined, 0 is implied.
+
+
+#####DataMigrator.java
+
+To support migrations between different data versions, view should implement ViewDataMigrator interface.
+Views framework calls beforeMigration() method to check if view is ready to migrate data.
+View can return false and the migration will be canceled. Otherwise, methods will be called in this order:
+
+  1. migrateEntity() for each persistence entity in the origin view. Parameters are Class objects of same entity loaded by
+  corresponding ClassLoaders of origin and current view,
+  2. migrateInstanceData() called once, view should copy instance data here,
+  3. afterMigration() called in the end. View can do some cleanup or additional migrations if needed.
+
+In the DataMigrator object the ViewDataMigrationContext is injected. It provides all needed methods to operate with
+both origin and current DataStore/instance data and also some utility methods to simplify copying data.
+
+    public class DataMigrator implements ViewDataMigrator {
+      @Inject
+      private ViewDataMigrationContext migrationContext;
+
+      @Override
+      public boolean beforeMigration() {
+        return migrationContext.getOriginDataVersion() == 1;
+      }
+
+      @Override
+      public void afterMigration() {
+      }
+
+      @Override
+      public void migrateEntity(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException {
+        if (currentEntityClass == PhoneUser.class) {
+          migrationContext.copyAllObjects(originEntityClass, currentEntityClass, new PhoneUserConverter());
+        } else {
+          migrationContext.copyAllObjects(originEntityClass, currentEntityClass);
+        }
+      }
+
+      @Override
+      public void migrateInstanceData() {
+        for (Map.Entry<String, Map<String, String>> userData : migrationContext.getOriginInstanceDataByUser().entrySet()) {
+          for (Map.Entry<String, String> entry : userData.getValue().entrySet()) {
+            String newValue = String.format("<no surname>;%s", entry.getValue());
+            migrationContext.putCurrentInstanceData(userData.getKey(), entry.getKey(), newValue);
+          }
+        }
+      }
+
+      private static class PhoneUserConverter implements EntityConverter {
+        @Override
+        public void convert(Object orig, Object dest) {
+          PhoneUser destPhone = (PhoneUser) dest;
+
+          BeanUtils.copyProperties(orig, dest);
+          if (destPhone.getName() == null) {
+            destPhone.setSurname("<no surname>");
+          } else {
+            String[] parts = destPhone.getName().split(" ");
+            if (parts.length > 1) {
+              destPhone.setSurname(parts[parts.length - 1]);
+            } else {
+              destPhone.setSurname("<no surname>");
+            }
+          }
+
+        }
+
+      }
+    }
+
+In this example, migrator supports both migration of persistence entity and instance data.

+ 105 - 0
ambari-views/examples/phone-list-upgrade-view/pom.xml

@@ -0,0 +1,105 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <parent>
+    <groupId>org.apache.ambari</groupId>
+    <artifactId>ambari-view-examples</artifactId>
+    <version>2.0.0.0-SNAPSHOT</version>
+  </parent>
+  <version>2.1.0.0-SNAPSHOT</version>
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>phone-list-upgrade-view</artifactId>
+  <packaging>jar</packaging>
+  <name>Ambari Phone List View</name>
+  <url>http://maven.apache.org</url>
+  <properties>
+    <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <version>1</version>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.8.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <version>3.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.ambari</groupId>
+      <artifactId>ambari-views</artifactId>
+      <version>[1.7.0.0,)</version>
+    </dependency>
+    <dependency>
+      <groupId>com.sun.jersey</groupId>
+      <artifactId>jersey-server</artifactId>
+      <version>1.8</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.5</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.ambari</groupId>
+      <artifactId>ambari-views</artifactId>
+      <version>2.0.0.0-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-beans</artifactId>
+      <version>3.1.2.RELEASE</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>rpm-maven-plugin</artifactId>
+        <version>2.0.1</version>
+        <executions>
+          <execution>
+            <phase>none</phase>
+            <goals>
+              <goal>rpm</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>

+ 126 - 0
ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/DataMigrator.java

@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.phonelist;
+
+import org.apache.ambari.view.*;
+import org.apache.ambari.view.migration.EntityConverter;
+import org.apache.ambari.view.migration.ViewDataMigrationContext;
+import org.apache.ambari.view.migration.ViewDataMigrationException;
+import org.apache.ambari.view.migration.ViewDataMigrator;
+import org.springframework.beans.BeanUtils;
+
+import javax.inject.Inject;
+import java.util.Map;
+
+/**
+ * Class responsible for migration from previous version (phone-list-view example)
+ */
+public class DataMigrator implements ViewDataMigrator {
+
+  /**
+   * The view context of target migration instance.
+   */
+  @Inject
+  private ViewContext viewContext;
+
+  /**
+   * The migration context.
+   */
+  @Inject
+  private ViewDataMigrationContext migrationContext;
+
+  /**
+   * Called by the framework before migration.
+   * Supports only migration from version 1.0.0, so we cancel migration
+   * if the origin version is not 0.
+   *
+   * @return true if migration started against the instance with data version "0"
+   */
+  @Override
+  public boolean beforeMigration() {
+    return migrationContext.getOriginDataVersion() == 0;
+  }
+
+  /**
+   * Called by the framework after migration.
+   */
+  @Override
+  public void afterMigration() {
+  }
+
+  /**
+   * Migrate a single persistence entity.
+   *
+   * @param originEntityClass    class object of origin (migration source) instance
+   * @param currentEntityClass   class object of current (migration target) instance
+   */
+  @Override
+  public void migrateEntity(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException {
+    if (currentEntityClass == PhoneUser.class) {
+      migrationContext.copyAllObjects(originEntityClass, currentEntityClass, new PhoneUserConverter());
+    } else {
+      migrationContext.copyAllObjects(originEntityClass, currentEntityClass);
+    }
+  }
+
+  /**
+   * Migrate instance data by adding surname.
+   */
+  @Override
+  public void migrateInstanceData() {
+    for (Map.Entry<String, Map<String, String>> userData : migrationContext.getOriginInstanceDataByUser().entrySet()) {
+      for (Map.Entry<String, String> entry : userData.getValue().entrySet()) {
+        String newValue = String.format("<no surname>;%s", entry.getValue());
+        migrationContext.putCurrentInstanceData(userData.getKey(), entry.getKey(), newValue);
+      }
+    }
+  }
+
+  /**
+   * The entity converter class responsible of converting PhoneUser data.
+   */
+  private static class PhoneUserConverter implements EntityConverter {
+
+    /**
+     * Adds surname to the new version of PhoneData. If original user name
+     * contained several words, last word is used as surname.
+     *
+     * @param orig a single origin entity object.
+     * @param dest an empty object of current persistence entity class.
+     */
+    @Override
+    public void convert(Object orig, Object dest) {
+      PhoneUser destPhone = (PhoneUser) dest;
+
+      BeanUtils.copyProperties(orig, dest);
+      if (destPhone.getName() == null) {
+        destPhone.setSurname("<no surname>");
+      } else {
+        String[] parts = destPhone.getName().split(" ");
+        if (parts.length > 1) {
+          destPhone.setSurname(parts[parts.length - 1]);
+        } else {
+          destPhone.setSurname("<no surname>");
+        }
+      }
+
+    }
+
+  }
+}

+ 242 - 0
ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/PhoneListServlet.java

@@ -0,0 +1,242 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.phonelist;
+
+import org.apache.ambari.view.DataStore;
+import org.apache.ambari.view.ViewContext;
+import org.apache.ambari.view.PersistenceException;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Servlet for phone list view.
+ */
+public class PhoneListServlet extends HttpServlet {
+
+  /**
+   * The view context.
+   */
+  private ViewContext viewContext;
+
+  /**
+   * The view data store.
+   * <code>null</code> indicates that the view properties should be used instead of the data store.
+   */
+  private DataStore dataStore = null;
+
+
+  // ----- GenericServlet ----------------------------------------------------
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+
+    ServletContext context = config.getServletContext();
+    viewContext = (ViewContext) context.getAttribute(ViewContext.CONTEXT_ATTRIBUTE);
+    dataStore = Boolean.parseBoolean(viewContext.getProperties().get("data.store.enabled")) ?
+        viewContext.getDataStore() : null;
+  }
+
+
+  // ----- HttpServlet -------------------------------------------------------
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    String name = request.getParameter("name");
+    String surname = request.getParameter("surname");
+    String phone = request.getParameter("phone");
+
+    try {
+      if (name != null && name.length() > 0 && phone != null && phone.length() > 0) {
+        if (request.getParameter("add") != null) {
+          addUser(name, surname, phone);
+        } else if (request.getParameter("update") != null) {
+          updateUser(name, surname, phone);
+        } else if (request.getParameter("delete") != null) {
+          removeUser(name, surname, phone);
+        }
+      }
+      listAll(request, response);
+    } catch (Exception e) {
+      throw new ServletException(e);
+    }
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    response.setContentType("text/html");
+    response.setStatus(HttpServletResponse.SC_OK);
+
+    PrintWriter writer = response.getWriter();
+
+    try {
+      String name = request.getParameter("name");
+      PhoneUser phone = getUser(name);
+
+      if (phone != null) {
+        editNumber(writer, name, phone.getSurname(), phone.getPhone(), request);
+      } else {
+        listAll(request, response);
+      }
+    } catch (PersistenceException e) {
+      throw new ServletException(e);
+    }
+  }
+
+
+  // ----- helper methods ----------------------------------------------------
+
+  // form to add new user
+  private void enterNumber(PrintWriter writer, HttpServletRequest request) {
+    writer.println("<form name=\"input\" action = \""+ request.getRequestURI() +"\" method=\"POST\">");
+    writer.println("<table>");
+    writer.println("<tr>");
+    writer.println("<td>Name:</td><td><input type=\"text\" name=\"name\"></td><br/>");
+    writer.println("</tr>");
+    writer.println("<tr>");
+    writer.println("<td>Surname:</td><td><input type=\"text\" name=\"surname\"></td><br/>");
+    writer.println("</tr>");
+    writer.println("<tr>");
+    writer.println("<td>Phone Number:</td><td><input type=\"text\" name=\"phone\"></td><br/><br/>");
+    writer.println("</tr>");
+    writer.println("</table>");
+    writer.println("<input type=\"submit\" value=\"Add\" name=\"add\">");
+    writer.println("</form>");
+  }
+
+  // for to update / delete existing user
+  private void editNumber(PrintWriter writer, String name, String surname, String phone, HttpServletRequest request) {
+    writer.println("<form name=\"input\" action = \""+ request.getRequestURI() +"\" method=\"POST\">");
+    writer.println("<table>");
+    writer.println("<tr>");
+    writer.println("<td>Name:</td><td><input type=\"text\" name=\"name\" value=\"" + name + "\" readonly></td><br/>");
+    writer.println("</tr>");
+    writer.println("<tr>");
+    writer.println("<td>Surname:</td><td><input type=\"text\" name=\"Surname\" value=\"" + surname + "\" readonly></td><br/>");
+    writer.println("</tr>");
+    writer.println("<tr>");
+    writer.println("<td>Phone Number:</td><td><input type=\"text\" name=\"phone\" value=\"" + phone + "\"></td><br/><br/>");
+    writer.println("</tr>");
+    writer.println("</table>");
+    writer.println("<input type=\"submit\" value=\"Update\" name=\"update\">");
+    writer.println("<input type=\"submit\" value=\"Delete\" name=\"delete\">");
+    writer.println("</form>");
+  }
+
+  // list all of the users
+  private void listAll(HttpServletRequest request, HttpServletResponse response) throws IOException, PersistenceException {
+
+    PrintWriter writer = response.getWriter();
+
+    writer.println("<h1>Phone List :" + viewContext.getInstanceName() + "</h1>");
+
+    writer.println("<table border=\"1\" style=\"width:300px\">");
+    writer.println("<tr>");
+    writer.println("<td>Name</td>");
+    writer.println("<td>Phone Number</td>");
+    writer.println("</tr>");
+
+    Collection<PhoneUser> phoneUsers = getAllUsers();
+    for (PhoneUser phoneUser : phoneUsers) {
+      String name = phoneUser.getName();
+      writer.println("<tr>");
+      writer.println("<td><A href=" + request.getRequestURI() + "?name=" + name + ">" + name + "</A></td>");
+      writer.println("<td>" + phoneUser.getPhone() + "</td>");
+      writer.println("</tr>");
+    }
+
+    writer.println("</table><br/><hr/>");
+
+    enterNumber(writer, request);
+  }
+
+  // determine whether a user has been persisted
+  private boolean userExists(String name) throws PersistenceException {
+    return dataStore != null &&
+        dataStore.find(PhoneUser.class, name) != null || viewContext.getInstanceData(name) != null;
+  }
+
+  // persist a new user
+  private void addUser(String name, String surname, String phone) throws PersistenceException {
+    if (userExists(name)) {
+      throw new IllegalArgumentException("A number for " + name + " already exists.");
+    }
+    updateUser(name, surname, phone);
+  }
+
+  // update an existing user
+  private void updateUser(String name, String surname, String phone) throws PersistenceException {
+    if (dataStore != null) {
+      dataStore.store(new PhoneUser(name, surname, phone));
+    } else {
+      viewContext.putInstanceData(name, surname + ";" + phone);
+    }
+  }
+
+  // remove an existing user
+  private void removeUser(String name, String surname, String phone) throws PersistenceException {
+    if (dataStore != null) {
+      dataStore.remove(new PhoneUser(name, surname, phone));
+    } else {
+      viewContext.removeInstanceData(name);
+    }
+  }
+
+  // get the phone entry for the given name
+  private PhoneUser getUser(String name) throws PersistenceException {
+    if (name != null && name.length() > 0) {
+      if (dataStore != null) {
+        return dataStore.find(PhoneUser.class, name);
+      } else {
+        String[] userInfo = viewContext.getInstanceData(name).split(";");
+        return new PhoneUser(name, userInfo[0], userInfo[1]);
+      }
+    }
+    return null;
+  }
+
+  // get all of the phone users
+  private Collection<PhoneUser> getAllUsers() throws PersistenceException {
+    if (dataStore != null) {
+      return dataStore.findAll(PhoneUser.class, null);
+    }
+    Map<String, String> data = new LinkedHashMap<String, String>(viewContext.getInstanceData());
+    Collection<PhoneUser> users = new HashSet<PhoneUser>();
+
+    for (Map.Entry<String,String> entry : data.entrySet()) {
+      String[] userInfo = entry.getValue().split(";");
+      users.add(new PhoneUser(entry.getKey(), userInfo[0], userInfo[1]));
+      entry.getKey();
+    }
+    return users;
+  }
+}

+ 113 - 0
ambari-views/examples/phone-list-upgrade-view/src/main/java/org/apache/ambari/view/phonelist/PhoneUser.java

@@ -0,0 +1,113 @@
+/* 
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.phonelist;
+
+/**
+ *  Phone user (name/phone number) entity for the phone list view example.
+ */
+public class PhoneUser {
+
+  /**
+   * The user name.
+   */
+  private String name;
+
+  /**
+   * The user surname.
+   */
+  private String surname;
+
+  /**
+   * The phone number.
+   */
+  private String phone;
+
+  /**
+   * No-arg constructor required for JPA.
+   */
+  public PhoneUser() {
+  }
+
+  /**
+   * Construct a phone user.
+   *
+   * @param name   the user name
+   * @param surname   the user surname
+   * @param phone  the phone number
+   */
+  public PhoneUser(String name, String surname, String phone) {
+    this.name  = name;
+    this.surname  = surname;
+    this.phone = phone;
+  }
+
+  /**
+   * Get the user name.
+   *
+   * @return the name
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Set the user surname.
+   *
+   * @param surname  the surname
+   */
+  public void setSurname(String surname) {
+    this.surname = surname;
+  }
+
+  /**
+   * Get the user surname.
+   *
+   * @return the surname
+   */
+  public String getSurname() {
+    return surname;
+  }
+
+  /**
+   * Set the user name.
+   *
+   * @param name  the name
+   */
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  /**
+   * Get the phone number.
+   *
+   * @return the phone number
+   */
+  public String getPhone() {
+    return phone;
+  }
+
+  /**
+   * Set the phone number.
+   *
+   * @param phone  the phone number
+   */
+  public void setPhone(String phone) {
+    this.phone = phone;
+  }
+}

+ 37 - 0
ambari-views/examples/phone-list-upgrade-view/src/main/resources/WEB-INF/web.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
+-->
+
+<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
+         version="2.4">
+
+  <display-name>Phone List Application</display-name>
+  <description>
+    This is the phone list view application.
+  </description>
+  <servlet>
+    <servlet-name>PhoneListServlet</servlet-name>
+    <servlet-class>org.apache.ambari.view.phonelist.PhoneListServlet</servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>PhoneListServlet</servlet-name>
+    <url-pattern>/</url-pattern>
+  </servlet-mapping>
+</web-app>

+ 54 - 0
ambari-views/examples/phone-list-upgrade-view/src/main/resources/view.xml

@@ -0,0 +1,54 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
+-->
+<view>
+  <name>PHONE_LIST</name>
+  <label>The Phone List View</label>
+  <version>2.0.0</version>
+  <build>001</build>
+  <data-version>1</data-version>
+  <data-migrator-class>org.apache.ambari.view.phonelist.DataMigrator</data-migrator-class>
+  <parameter>
+    <name>data.store.enabled</name>
+    <description>
+      Determine whether or not to use the view persistence data store.
+      A value of false indicates that the view properties should be used instead of the data store.
+    </description>
+    <default-value>false</default-value>
+    <required>false</required>
+  </parameter>
+
+  <persistence>
+    <entity>
+      <class>org.apache.ambari.view.phonelist.PhoneUser</class>
+      <id-property>name</id-property>
+    </entity>
+  </persistence>
+  <instance>
+    <name>LIST_1</name>
+    <property>
+      <key>data.store.enabled</key>
+      <value>false</value>
+    </property>
+  </instance>
+  <instance>
+    <name>LIST_2</name>
+    <property>
+      <key>data.store.enabled</key>
+      <value>true</value>
+    </property>
+  </instance>
+</view>

+ 1 - 1
ambari-views/examples/phone-list-view/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>phone-list-view</artifactId>

+ 3 - 2
ambari-views/examples/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-project</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
     <relativePath>../../ambari-project</relativePath>
   </parent>
   <modelVersion>4.0.0</modelVersion>
@@ -27,13 +27,14 @@
   <artifactId>ambari-view-examples</artifactId>
   <packaging>pom</packaging>
   <name>Ambari View Examples</name>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <modules>
     <module>helloworld-view</module>
     <module>hello-servlet-view</module>
     <module>hello-spring-view</module>
     <module>favorite-view</module>
     <module>phone-list-view</module>
+    <module>phone-list-upgrade-view</module>
     <module>calculator-view</module>
     <module>cluster-view</module>
     <module>weather-view</module>

+ 2 - 2
ambari-views/examples/property-validator-view/pom.xml

@@ -19,11 +19,11 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>property-validator-view</artifactId>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Ambari Property Validator View</name>
   <url>http://maven.apache.org</url>

+ 2 - 2
ambari-views/examples/property-view/pom.xml

@@ -19,11 +19,11 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>property-view</artifactId>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Ambari Property View</name>
   <url>http://maven.apache.org</url>

+ 1 - 1
ambari-views/examples/restricted-view/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>restricted-view</artifactId>

+ 2 - 2
ambari-views/examples/simple-view/pom.xml

@@ -19,11 +19,11 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>simple-view</artifactId>
-  <version>2.0.0-SNAPSHOT</version>
+  <version>2.0.0.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Ambari Simple View</name>
   <url>http://maven.apache.org</url>

+ 1 - 1
ambari-views/examples/weather-view/pom.xml

@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.apache.ambari</groupId>
     <artifactId>ambari-view-examples</artifactId>
-    <version>2.0.0-SNAPSHOT</version>
+    <version>2.0.0.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <artifactId>weather-view</artifactId>

+ 34 - 0
ambari-views/src/main/java/org/apache/ambari/view/migration/EntityConverter.java

@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.migration;
+
+/**
+ * Interface for persistence entity conversion class.
+ */
+public interface EntityConverter {
+
+  /**
+   * Called for every persistence entity object. Copies data from orig to dest
+   * objects.
+   *
+   * @param orig a single origin entity object.
+   * @param dest an empty object of current persistence entity class.
+   */
+  void convert(Object orig, Object dest);
+}

+ 150 - 0
ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrationContext.java

@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.migration;
+
+import org.apache.ambari.view.DataStore;
+import org.apache.ambari.view.ViewInstanceDefinition;
+
+import java.util.Map;
+
+/**
+ * Interface for view data migration context class. Provides access
+ * to the information about origin(source) and current(target) instances.
+ * Also provides utility methods for copying persistence entities and
+ * instance data.
+ */
+public interface ViewDataMigrationContext {
+  /**
+   * Get the current(target) instance data version.
+   *
+   * @return the data version of current instance
+   */
+  int getCurrentDataVersion();
+
+  /**
+   * Get the instance definition of current instance.
+   *
+   * @return the instance definition of current instance
+   */
+  ViewInstanceDefinition getCurrentInstanceDefinition();
+
+  /**
+   * Get persistence entities of the current view instance.
+   *
+   * @return the mapping of entity class name to the class objects,
+   * loaded by the classloader of current view version.
+   */
+  Map<String, Class> getCurrentEntityClasses();
+
+  /**
+   * Get a data store for current view persistence entities.
+   *
+   * @return a data store of current view instance
+   */
+  DataStore getCurrentDataStore();
+
+  /**
+   * Save an instance data value for the given key and given user
+   * to current instance.
+   *
+   * @param user   the user (owner of instance data)
+   * @param key    the key
+   * @param value  the value
+   */
+  void putCurrentInstanceData(String user, String key, String value);
+
+  /**
+   * Get the current instance data in the mapping of user owning data to the key-value data.
+   *
+   * @return mapping of the data owner to the current instance data entries
+   */
+  Map<String, Map<String, String>> getCurrentInstanceDataByUser();
+
+  /**
+   * Get the origin(source) instance data version.
+   *
+   * @return the data version of origin instance
+   */
+  int getOriginDataVersion();
+
+  /**
+   * Get the instance definition of origin instance.
+   *
+   * @return the instance definition of origin instance
+   */
+  ViewInstanceDefinition getOriginInstanceDefinition();
+
+  /**
+   * Get persistence entities of the origin view instance.
+   *
+   * @return the mapping of entity class name to the class objects,
+   * loaded by the classloader of origin view version.
+   */
+  Map<String, Class> getOriginEntityClasses();
+
+  /**
+   * Get a data store for origin view persistence entities.
+   *
+   * @return a data store of origin view instance
+   */
+  DataStore getOriginDataStore();
+
+  /**
+   * Save an instance data value for the given key and given user
+   * to origin instance.
+   *
+   * @param user   the user (owner of instance data)
+   * @param key    the key
+   * @param value  the value
+   */
+  void putOriginInstanceData(String user, String key, String value);
+
+  /**
+   * Get the origin instance data in the mapping of user owning data to the key-value data.
+   *
+   * @return mapping of the data owner to the origin instance data entries
+   */
+  Map<String, Map<String, String>> getOriginInstanceDataByUser();
+
+  /**
+   * Utility method provides ability to copy all entity objects from origin to current
+   * DataStore. Copies all fields as is without any processing.
+   *
+   * @param originEntityClass    class object of origin (migration source) instance
+   * @param currentEntityClass   class object of current (migration target) instance
+   */
+  void copyAllObjects(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException;
+
+  /**
+   * Utility method provides ability to copy all entity objects from origin to current
+   * DataStore. Entity converter can be provided to do any required migration.
+   *
+   * @param originEntityClass    class object of origin (migration source) instance
+   * @param currentEntityClass   class object of current (migration target) instance
+   * @param entityConverter      object responsible of converting every single instance.
+   */
+  void copyAllObjects(Class originEntityClass, Class currentEntityClass, EntityConverter entityConverter)
+      throws ViewDataMigrationException;
+
+  /**
+   * Utility method that copies all instance data from origin to current
+   * instance without changing.
+   */
+  void copyAllInstanceData();
+}

+ 44 - 0
ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrationException.java

@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.migration;
+
+/**
+ * View data migration exception. Indicates that an error occurred while
+ * migrating data from one instance to another.
+ */
+public class ViewDataMigrationException extends Exception {
+  /**
+   * Constructor.
+   *
+   * @param msg        message
+   */
+  public ViewDataMigrationException(String msg) {
+    super(msg);
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param msg        message
+   * @param throwable  root exception
+   */
+  public ViewDataMigrationException(String msg, Throwable throwable) {
+    super(msg, throwable);
+  }
+}

+ 60 - 0
ambari-views/src/main/java/org/apache/ambari/view/migration/ViewDataMigrator.java

@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ambari.view.migration;
+
+/**
+ * Interface for data migration logic class.
+ */
+public interface ViewDataMigrator {
+  /**
+   * Called by the view framework before migration process. View can cancel migration by returning false.
+   *
+   * @return  true if view can do the migration, false otherwise.
+   * @throws  ViewDataMigrationException   migration exception occurred.
+   */
+  boolean beforeMigration() throws ViewDataMigrationException;
+
+  /**
+   * Called by the view framework after migration finished.
+   * View can do cleanup and additional migration procedures.
+   *
+   * @throws ViewDataMigrationException    migration exception occurred.
+   */
+  void afterMigration() throws ViewDataMigrationException;
+
+  /**
+   * Called by the view framework during migration process for every persistence entity
+   * of the origin (source) instance.
+   * View can migrate a single persistence entity in the implementation of this method.
+   *
+   * @param originEntityClass    class object of origin (migration source) instance
+   * @param currentEntityClass   class object of current (migration target) instance
+   *
+   * @throws ViewDataMigrationException    migration exception occurred.
+   */
+  void migrateEntity(Class originEntityClass, Class currentEntityClass) throws ViewDataMigrationException;
+
+  /**
+   * Called by the view framework during migration process.
+   * View can migrate the instance data in the implementation of this method.
+   *
+   * @throws ViewDataMigrationException    migration exception occurred.
+   */
+  void migrateInstanceData() throws ViewDataMigrationException;
+}

+ 1 - 1
ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java

@@ -42,7 +42,7 @@ public interface Validator {
    *
    * @param property    the property name
    * @param definition  the view instance definition
-   * @param mode        the validation mode
+   * @param mode        the validation modes
    *
    * @return the instance validation result; may be {@code null}
    */

+ 10 - 0
ambari-views/src/main/resources/view.xsd

@@ -288,6 +288,11 @@
             <xs:documentation>The build number of the view.</xs:documentation>
           </xs:annotation>
         </xs:element>
+        <xs:element type="xs:string" name="data-version" minOccurs="0" maxOccurs="1">
+          <xs:annotation>
+            <xs:documentation>The data version. Used for data migrations.</xs:documentation>
+          </xs:annotation>
+        </xs:element>
         <xs:element type="xs:string" name="min-ambari-version" minOccurs="0" maxOccurs="1">
           <xs:annotation>
             <xs:documentation>The minimum version of Ambari server required to run this view.</xs:documentation>
@@ -342,6 +347,11 @@
             <xs:documentation>The Masker class for masking view parameters.</xs:documentation>
           </xs:annotation>
         </xs:element>
+        <xs:element type="xs:string" name="data-migrator-class" minOccurs="0" maxOccurs="1">
+          <xs:annotation>
+            <xs:documentation>The data migration class.</xs:documentation>
+          </xs:annotation>
+        </xs:element>
         <xs:element type="ParameterType" name="parameter" minOccurs="0" maxOccurs="unbounded">
           <xs:annotation>
             <xs:documentation>Defines a configuration parameter that is used to when creating a view instance.

+ 21 - 0
pom.xml

@@ -446,6 +446,27 @@
         <module>ambari-logsearch</module>
       </modules>
     </profile>
+    <profile>
+      <id>example-views</id>
+      <activation>
+        <property>
+          <name>exampleViews</name>
+        </property>
+      </activation>
+      <modules>
+        <module>ambari-web</module>
+        <module>ambari-project</module>
+        <module>ambari-views</module>
+        <module>ambari-admin</module>
+        <module>ambari-views/examples</module>
+        <module>ambari-metrics</module>
+        <module>ambari-server</module>
+        <module>ambari-funtest</module>
+        <module>ambari-agent</module>
+        <module>ambari-client</module>
+        <module>ambari-shell</module>
+      </modules>
+    </profile>
     <profile>
       <id>static-web</id>
       <modules>