You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by tb...@apache.org on 2014/11/12 21:22:14 UTC

ambari git commit: AMBARI-8295 - Views: support validation (tbeerbower)

Repository: ambari
Updated Branches:
  refs/heads/trunk 252e94c63 -> e2757784c


AMBARI-8295 - Views: support validation (tbeerbower)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/e2757784
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/e2757784
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/e2757784

Branch: refs/heads/trunk
Commit: e2757784ca251c18ecdd248a7a1a8e4a3019fac3
Parents: 252e94c
Author: tbeerbower <tb...@hortonworks.com>
Authored: Wed Nov 12 15:21:40 2014 -0500
Committer: tbeerbower <tb...@hortonworks.com>
Committed: Wed Nov 12 15:22:10 2014 -0500

----------------------------------------------------------------------
 .../controller/internal/BaseProvider.java       |  69 ++++++++++--
 .../internal/ViewInstanceResourceProvider.java  |  23 ++++
 .../ambari/server/orm/entities/ViewEntity.java  |  34 ++++++
 .../server/orm/entities/ViewInstanceEntity.java |  73 ++++++++++---
 .../apache/ambari/server/view/ViewRegistry.java |  54 +++++++---
 .../server/view/configuration/ViewConfig.java   |  37 +++++++
 .../InstanceValidationResultImpl.java           | 106 +++++++++++++++++++
 .../view/validation/ValidationResultImpl.java   |  80 ++++++++++++++
 .../controller/internal/BaseProviderTest.java   |  28 +++++
 .../server/orm/entities/ViewEntityTest.java     |  27 +++++
 .../orm/entities/ViewInstanceEntityTest.java    |  79 +++++++++++++-
 .../view/configuration/ViewConfigTest.java      |  24 +++++
 .../InstanceValidationResultImplTest.java       |  66 ++++++++++++
 .../validation/ValidationResultImplTest.java    |  55 ++++++++++
 .../view/validation/ValidationResult.java       |  54 ++++++++++
 .../ambari/view/validation/Validator.java       |  59 +++++++++++
 ambari-views/src/main/resources/view.xsd        |   5 +
 17 files changed, 835 insertions(+), 38 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BaseProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BaseProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BaseProvider.java
index b4d582a..a95342a 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BaseProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BaseProvider.java
@@ -279,6 +279,21 @@ public abstract class BaseProvider {
   }
 
   /**
+   * Determine whether or not the given property id is part the given set of requested ids.  This
+   * accounts for the cases where the given properties are actually property categories.
+   *
+   * @param propertyId    the property id
+   * @param requestedIds  the requested set of property ids
+   *
+   * @return true if the given property id is part of the given set of requested ids
+   */
+  protected static boolean isPropertyRequested(String propertyId, Set<String> requestedIds) {
+    return requestedIds.contains(propertyId) ||
+        isPropertyCategoryRequested(propertyId, requestedIds) ||
+        isPropertyEntryRequested(propertyId, requestedIds);
+  }
+
+  /**
    * Set a property value on the given resource for the given id and value.
    * Make sure that the id is in the given set of requested ids.
    *
@@ -287,16 +302,9 @@ public abstract class BaseProvider {
    * @param value         the value to set
    * @param requestedIds  the requested set of property ids
    */
-  protected static boolean setResourceProperty(Resource resource, String propertyId, Object value, Set<String> requestedIds) {
-    boolean contains = requestedIds.contains(propertyId);
-
-    if (!contains) {
-      String category = PropertyHelper.getPropertyCategory(propertyId);
-      while (category != null && !contains) {
-        contains = requestedIds.contains(category);
-        category = PropertyHelper.getPropertyCategory(category);
-      }
-    }
+  protected static boolean setResourceProperty(Resource resource, String propertyId, Object value,
+                                               Set<String> requestedIds) {
+    boolean contains = requestedIds.contains(propertyId) || isPropertyCategoryRequested(propertyId, requestedIds);
 
     if (contains) {
       if (LOG.isDebugEnabled()) {
@@ -364,6 +372,47 @@ public abstract class BaseProvider {
     return false;
   }
 
+  /**
+   * Determine whether or not any of the requested ids are an entry for the given property, if
+   * the given property is a category.  For example, if the given property is 'category/subcategory'
+   * and the set of requested ids contains 'category/subcategory/property' then this method should
+   * return true.
+   *
+   * @param propertyId    the property id
+   * @param requestedIds  the requested set of property ids
+   *
+   * @return true if the given property is a category for any of the requested ids
+   */
+  private static boolean isPropertyEntryRequested(String propertyId, Set<String> requestedIds) {
+    for (String requestedId : requestedIds) {
+      if (requestedId.startsWith(propertyId)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Determine whether or not any of the requested ids are a category for the given property.
+   * For example, if the given property is 'category/subcategory/property' and the set of requested ids
+   * contains 'category' or 'category/subcategory' then this method should return true.
+   *
+   * @param propertyId    the property id
+   * @param requestedIds  the requested set of property ids
+   *
+   * @return true if the given property's category is part of the given set of requested ids
+   */
+  private static boolean isPropertyCategoryRequested(String propertyId, Set<String> requestedIds) {
+    String category = PropertyHelper.getPropertyCategory(propertyId);
+    while (category != null ) {
+      if (requestedIds.contains(category)) {
+        return true;
+      }
+      category = PropertyHelper.getPropertyCategory(category);
+    }
+    return false;
+  }
+
 
   // ----- accessors ---------------------------------------------------------
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ViewInstanceResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ViewInstanceResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ViewInstanceResourceProvider.java
index 3f62cc3..a944e95 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ViewInstanceResourceProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ViewInstanceResourceProvider.java
@@ -36,6 +36,9 @@ import org.apache.ambari.server.orm.entities.ViewInstanceEntity;
 import org.apache.ambari.server.orm.entities.ViewInstancePropertyEntity;
 import org.apache.ambari.server.orm.entities.ViewParameterEntity;
 import org.apache.ambari.server.view.ViewRegistry;
+import org.apache.ambari.server.view.validation.InstanceValidationResultImpl;
+import org.apache.ambari.server.view.validation.ValidationResultImpl;
+import org.apache.ambari.view.validation.Validator;
 
 import java.util.Collections;
 import java.util.HashMap;
@@ -65,6 +68,10 @@ public class ViewInstanceResourceProvider extends AbstractResourceProvider {
   public static final String CONTEXT_PATH_PROPERTY_ID  = "ViewInstanceInfo/context_path";
   public static final String STATIC_PROPERTY_ID        = "ViewInstanceInfo/static";
 
+  // validation properties
+  public static final String VALIDATION_RESULT_PROPERTY_ID           = "ViewInstanceInfo/validation_result";
+  public static final String PROPERTY_VALIDATION_RESULTS_PROPERTY_ID = "ViewInstanceInfo/property_validation_results";
+
   /**
    * Property prefix values.
    */
@@ -98,6 +105,8 @@ public class ViewInstanceResourceProvider extends AbstractResourceProvider {
     propertyIds.add(DATA_PROPERTY_ID);
     propertyIds.add(CONTEXT_PATH_PROPERTY_ID);
     propertyIds.add(STATIC_PROPERTY_ID);
+    propertyIds.add(VALIDATION_RESULT_PROPERTY_ID);
+    propertyIds.add(PROPERTY_VALIDATION_RESULTS_PROPERTY_ID);
   }
 
   // ----- Constructors ------------------------------------------------------
@@ -255,6 +264,20 @@ public class ViewInstanceResourceProvider extends AbstractResourceProvider {
     setResourceProperty(resource, ICON_PATH_ID, getIconPath(contextPath, viewInstanceEntity.getIcon()), requestedIds);
     setResourceProperty(resource, ICON64_PATH_ID, getIconPath(contextPath, viewInstanceEntity.getIcon64()), requestedIds);
 
+    // if the view provides its own validator then run it
+    if (viewEntity.hasValidator()) {
+
+      if (isPropertyRequested(VALIDATION_RESULT_PROPERTY_ID, requestedIds) ||
+          isPropertyRequested(PROPERTY_VALIDATION_RESULTS_PROPERTY_ID, requestedIds)) {
+
+        InstanceValidationResultImpl result =
+            viewInstanceEntity.getValidationResult(viewEntity, Validator.ValidationContext.EXISTING);
+
+        setResourceProperty(resource, VALIDATION_RESULT_PROPERTY_ID, ValidationResultImpl.create(result), requestedIds);
+        setResourceProperty(resource, PROPERTY_VALIDATION_RESULTS_PROPERTY_ID, result.getPropertyResults(), requestedIds);
+      }
+    }
+
     return resource;
   }
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewEntity.java
index d42e1a0..2f83e02 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewEntity.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewEntity.java
@@ -24,6 +24,7 @@ import org.apache.ambari.server.controller.spi.ResourceProvider;
 import org.apache.ambari.server.view.ViewSubResourceDefinition;
 import org.apache.ambari.server.view.configuration.ResourceConfig;
 import org.apache.ambari.server.view.configuration.ViewConfig;
+import org.apache.ambari.view.validation.Validator;
 import org.apache.ambari.view.View;
 import org.apache.ambari.view.ViewDefinition;
 
@@ -211,6 +212,12 @@ public class ViewEntity implements ViewDefinition {
   private View view = null;
 
   /**
+   * The view validator.
+   */
+  @Transient
+  private Validator validator = null;
+
+  /**
    * The view status.
    */
   @Transient
@@ -705,6 +712,33 @@ public class ViewEntity implements ViewDefinition {
   }
 
   /**
+   * Set the view validator.
+   *
+   * @param validator  the view validator
+   */
+  public void setValidator(Validator validator) {
+    this.validator = validator;
+  }
+
+  /**
+   * Get the associated view validator.
+   *
+   * @return the view validator
+   */
+  public Validator getValidator() {
+    return validator;
+  }
+
+  /**
+   * Determine whether or not a validator has been specified for this view.
+   *
+   * @return true if this view has a validator
+   */
+  public boolean hasValidator() {
+    return validator != null;
+  }
+
+  /**
    * Set the mask class name.
    *
    * @param mask the mask class name

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java
index efa2818..1dd1b5a 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ViewInstanceEntity.java
@@ -48,9 +48,13 @@ 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.configuration.InstanceConfig;
+import org.apache.ambari.server.view.validation.InstanceValidationResultImpl;
+import org.apache.ambari.server.view.validation.ValidationResultImpl;
+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;
 
 /**
  * Represents an instance of a View.
@@ -158,7 +162,7 @@ public class ViewInstanceEntity implements ViewInstanceDefinition {
 
   @OneToOne(cascade = CascadeType.ALL)
   @JoinColumns({
-      @JoinColumn(name = "resource_id", referencedColumnName = "resource_id", nullable = false),
+      @JoinColumn(name = "resource_id", referencedColumnName = "resource_id", nullable = false)
   })
   private ResourceEntity resource;
 
@@ -712,26 +716,67 @@ public class ViewInstanceEntity implements ViewInstanceDefinition {
    * Validate the state of the instance.
    *
    * @param viewEntity the view entity to which this instance will be bound
+   * @param context the validation context
+   *
    * @throws IllegalStateException if the instance is not in a valid state
    */
-  public void validate(ViewEntity viewEntity) throws IllegalStateException {
+  public void validate(ViewEntity viewEntity, Validator.ValidationContext context) throws IllegalStateException {
+    InstanceValidationResultImpl result = getValidationResult(viewEntity, context);
+    if (!result.isValid()) {
+      throw new IllegalStateException(result.toJson());
+    }
+  }
 
-    // make sure that there is an instance property value defined
-    // for each required view parameter
-    Set<String> requiredParamterNames = new HashSet<String>();
-    for (ViewParameterEntity parameter : viewEntity.getParameters()) {
-      if (parameter.isRequired()) {
-        requiredParamterNames.add(parameter.getName());
+  /**
+   * Get the validation the state of the instance.
+   *
+   * @param viewEntity the view entity to which this instance will be bound
+   * @param context the validation context
+   *
+   * @return the instance validation result
+   */
+  public InstanceValidationResultImpl getValidationResult(ViewEntity viewEntity, Validator.ValidationContext context)
+      throws IllegalStateException {
+
+    Map<String, ValidationResult> propertyResults = new HashMap<String, ValidationResult>();
+
+    if (context.equals(Validator.ValidationContext.PRE_CREATE) ||
+        context.equals(Validator.ValidationContext.PRE_UPDATE)) {
+
+      // make sure that there is an instance property value defined
+      // for each required view parameter
+      Set<String> requiredParameterNames = new HashSet<String>();
+      for (ViewParameterEntity parameter : viewEntity.getParameters()) {
+        if (parameter.isRequired()) {
+          requiredParameterNames.add(parameter.getName());
+        }
+      }
+      Collection<ViewInstancePropertyEntity> propertyEntities = getProperties();
+      for (ViewInstancePropertyEntity property : propertyEntities) {
+        requiredParameterNames.remove(property.getName());
+      }
+      // required but missing instance properties...
+      for (String requiredParameterName : requiredParameterNames) {
+        propertyResults.put(requiredParameterName,
+            new ValidationResultImpl(false,
+                "No property values exist for the required parameter " + requiredParameterName + "."));
       }
-    }
-    for (ViewInstancePropertyEntity property : getProperties()) {
-      requiredParamterNames.remove(property.getName());
     }
 
-    if (!requiredParamterNames.isEmpty()) {
-      throw new IllegalStateException("No property values exist for the required parameters " +
-        requiredParamterNames);
+    ValidationResult instanceResult = null;
+    Validator         validator     = viewEntity.getValidator();
+
+    // if the view provides its own validator, run it
+    if (validator != null) {
+      instanceResult = validator.validateInstance(this, context);
+      for ( String property : getPropertyMap().keySet()) {
+        if (!propertyResults.containsKey(property)) {
+          propertyResults.put(property,
+              ValidationResultImpl.create(validator.validateProperty(property, this, context)));
+        }
+      }
     }
+    return new InstanceValidationResultImpl(ValidationResultImpl.create(instanceResult), propertyResults);
   }
 
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
index a5d11bd..0dbf32c 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/view/ViewRegistry.java
@@ -22,6 +22,7 @@ import com.google.common.collect.Sets;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.persist.Transactional;
 import org.apache.ambari.server.api.resources.ResourceInstanceFactoryImpl;
 import org.apache.ambari.server.api.resources.SubResourceDefinition;
 import org.apache.ambari.server.api.resources.ViewExternalSubResourceDefinition;
@@ -64,6 +65,7 @@ import org.apache.ambari.server.view.configuration.PersistenceConfig;
 import org.apache.ambari.server.view.configuration.PropertyConfig;
 import org.apache.ambari.server.view.configuration.ResourceConfig;
 import org.apache.ambari.server.view.configuration.ViewConfig;
+import org.apache.ambari.view.validation.Validator;
 import org.apache.ambari.view.Masker;
 import org.apache.ambari.view.SystemException;
 import org.apache.ambari.view.View;
@@ -461,6 +463,7 @@ public class ViewRegistry {
    *                                   does not exist
    * @throws SystemException           if the instance can not be installed
    */
+  @Transactional
   public void installViewInstance(ViewInstanceEntity instanceEntity)
       throws IllegalStateException, IllegalArgumentException, SystemException {
     ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());
@@ -476,7 +479,7 @@ public class ViewRegistry {
               version + "/" + instanceName);
         }
 
-        instanceEntity.validate(viewEntity);
+        instanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
 
         ResourceTypeEntity resourceTypeEntity = resourceTypeDAO.findByName(ViewEntity.getViewName(viewName, version));
         // create an admin resource to represent this view instance
@@ -530,7 +533,7 @@ public class ViewRegistry {
     ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());
 
     if (viewEntity != null) {
-      instanceEntity.validate(viewEntity);
+      instanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_UPDATE);
       instanceDAO.merge(instanceEntity);
     }
   }
@@ -541,6 +544,7 @@ public class ViewRegistry {
    * @param instanceEntity  the view instance entity
    * @throws IllegalStateException if the given instance is not in a valid state
    */
+  @Transactional
   public void uninstallViewInstance(ViewInstanceEntity instanceEntity) throws IllegalStateException {
     ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());
 
@@ -577,6 +581,7 @@ public class ViewRegistry {
    * @param instanceEntity  the instance entity
    * @param key             the data key
    */
+  @Transactional
   public void removeInstanceData(ViewInstanceEntity instanceEntity, String key) {
     ViewInstanceDataEntity dataEntity = instanceEntity.getInstanceData(key);
     if (dataEntity != null) {
@@ -903,6 +908,11 @@ public class ViewRegistry {
       view = getView(viewConfig.getViewClass(cl), new ViewContextImpl(viewDefinition, this));
     }
     viewDefinition.setView(view);
+    Validator validator = null;
+    if (viewConfig.getValidator() != null) {
+      validator = getValidator(viewConfig.getValidatorClass(cl), new ViewContextImpl(viewDefinition, this));
+    }
+    viewDefinition.setValidator(validator);
     viewDefinition.setMask(viewConfig.getMasker());
 
     Set<SubResourceDefinition> subResourceDefinitions = new HashSet<SubResourceDefinition>();
@@ -927,7 +937,7 @@ public class ViewRegistry {
       properties.put(propertyConfig.getKey(), propertyConfig.getValue());
     }
     setViewInstanceProperties(viewInstanceDefinition, properties, viewConfig, viewDefinition.getClassLoader());
-    viewInstanceDefinition.validate(viewDefinition);
+    viewInstanceDefinition.validate(viewDefinition, Validator.ValidationContext.PRE_CREATE);
 
     bindViewInstance(viewDefinition, viewInstanceDefinition);
     return viewInstanceDefinition;
@@ -1042,6 +1052,19 @@ public class ViewRegistry {
     return viewInstanceInjector.getInstance(clazz);
   }
 
+  // get the given view validator class from the given class loader; inject a context
+  private static Validator getValidator(Class<? extends Validator> clazz,
+                              final ViewContext viewContext) {
+    Injector viewInstanceInjector = Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ViewContext.class)
+            .toInstance(viewContext);
+      }
+    });
+    return viewInstanceInjector.getInstance(clazz);
+  }
+
   // create masker from given class; probably replace with injector later
   private static Masker getMasker(Class<? extends Masker> clazz) {
     try {
@@ -1302,16 +1325,8 @@ public class ViewRegistry {
         instanceEntity.setXmlDriven(true);
         instanceDefinitions.add(instanceEntity);
       }
-      // ensure that the view entity matches the db
-      syncView(viewDefinition, instanceDefinitions);
-
-      onDeploy(viewDefinition);
+      persistView(viewDefinition, instanceDefinitions);
 
-      // update the registry with the view instances
-      for (ViewInstanceEntity instanceEntity : instanceDefinitions) {
-        addInstanceDefinition(viewDefinition, instanceEntity);
-        handlerList.addViewInstance(instanceEntity);
-      }
       setViewStatus(viewDefinition, ViewEntity.ViewStatus.DEPLOYED, "Deployed " + extractedArchiveDirPath + ".");
 
     } catch (Exception e) {
@@ -1322,6 +1337,21 @@ public class ViewRegistry {
     }
   }
 
+  // persist the given view and its instances
+  @Transactional
+  private void persistView(ViewEntity viewDefinition, Set<ViewInstanceEntity> instanceDefinitions) throws Exception {
+    // ensure that the view entity matches the db
+    syncView(viewDefinition, instanceDefinitions);
+
+    onDeploy(viewDefinition);
+
+    // update the registry with the view instances
+    for (ViewInstanceEntity instanceEntity : instanceDefinitions) {
+      addInstanceDefinition(viewDefinition, instanceEntity);
+      handlerList.addViewInstance(instanceEntity);
+    }
+  }
+
   // extract the view archive for the given path.
   protected static boolean extractViewArchive(String archivePath,
                                             ViewExtractor extractor,

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java b/ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java
index 3a23ea7..ea04a21 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/view/configuration/ViewConfig.java
+++ b/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.validation.Validator;
 import org.apache.ambari.view.Masker;
 import org.apache.ambari.view.View;
 import org.apache.commons.lang.StringUtils;
@@ -84,6 +85,17 @@ public class ViewConfig {
   private Class<? extends View> viewClass = null;
 
   /**
+   * The main view class name.
+   */
+  @XmlElement(name="validator-class")
+  private String validator;
+
+  /**
+   * The view validator class.
+   */
+  private Class<? extends Validator> validatorClass = null;
+
+  /**
    * The masker class name for parameters.
    */
   @XmlElement(name="masker-class")
@@ -213,6 +225,31 @@ public class ViewConfig {
   }
 
   /**
+   * Get the view validator class name.
+   *
+   * @return the view validator class name
+   */
+  public String getValidator() {
+    return validator;
+  }
+
+  /**
+   * Get the view validator class.
+   *
+   * @param cl the class loader
+   *
+   * @return the view validator class
+   *
+   * @throws ClassNotFoundException if the class can not be loaded
+   */
+  public Class<? extends Validator> getValidatorClass(ClassLoader cl) throws ClassNotFoundException {
+    if (validatorClass == null) {
+      validatorClass = cl.loadClass(validator).asSubclass(Validator.class);
+    }
+    return validatorClass;
+  }
+
+  /**
    * Get the masker class name.
    * @return the masker class name
    */

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/view/validation/InstanceValidationResultImpl.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/validation/InstanceValidationResultImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/view/validation/InstanceValidationResultImpl.java
new file mode 100644
index 0000000..c536ae5
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/view/validation/InstanceValidationResultImpl.java
@@ -0,0 +1,106 @@
+/**
+ * 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.validation;
+
+import com.google.gson.Gson;
+import org.apache.ambari.view.validation.ValidationResult;
+
+import java.util.Map;
+
+/**
+ * View instance validation result.  This result includes the validation results
+ * for the associated view instance and its properties.
+ */
+public class InstanceValidationResultImpl extends ValidationResultImpl {
+
+  /**
+   * Static Gson instance.
+   */
+  private static final Gson GSON = new Gson();
+
+  /**
+   * The validation results for the associated instance's properties
+   */
+  private final Map<String, ValidationResult> propertyResults;
+
+
+  // ----- Constructors ------------------------------------------------------
+
+  /**
+   * Construct an instance validation result.
+   *
+   * @param instanceResult   the results of the instance validation
+   * @param propertyResults  the results of the property validations
+   */
+  public InstanceValidationResultImpl(ValidationResult instanceResult, Map<String, ValidationResult> propertyResults) {
+    super(isValid(instanceResult, propertyResults), getDetail(instanceResult, propertyResults));
+    this.propertyResults = propertyResults;
+  }
+
+
+  // ----- InstanceValidationResultImpl --------------------------------------
+
+  /**
+   * Get the validation results for the properties of the associated instance.
+   *
+   * @return the property validation results
+   */
+  public Map<String, ValidationResult> getPropertyResults() {
+    return propertyResults;
+  }
+
+  /**
+   * Return this result as a JSON string.
+   *
+   * @return this result in JSON format
+   */
+  public String toJson() {
+    return GSON.toJson(this);
+  }
+
+
+  // ----- helper methods ----------------------------------------------------
+
+  // determine whether or not the given instance and property results are valid
+  private static boolean isValid(ValidationResult instanceResult, Map<String, ValidationResult> propertyResults) {
+    boolean instanceValid = instanceResult.isValid();
+    if (instanceValid) {
+      for (Map.Entry<String, ValidationResult> entry : propertyResults.entrySet()) {
+        ValidationResult propertyResult = entry.getValue();
+        if (propertyResult != null && !propertyResult.isValid()) {
+          return false;
+        }
+      }
+    }
+    return instanceValid;
+  }
+
+  // get a detail message from the given instance and property results
+  private static String getDetail(ValidationResult instanceResult, Map<String, ValidationResult> propertyResults) {
+    if (instanceResult.isValid()) {
+      for (Map.Entry<String, ValidationResult> entry : propertyResults.entrySet()) {
+        ValidationResult propertyResult = entry.getValue();
+        if (propertyResult != null && !propertyResult.isValid()) {
+          return "The instance has invalid properties.";
+        }
+      }
+    }
+    return instanceResult.getDetail();
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/main/java/org/apache/ambari/server/view/validation/ValidationResultImpl.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/view/validation/ValidationResultImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/view/validation/ValidationResultImpl.java
new file mode 100644
index 0000000..643eb1b
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/view/validation/ValidationResultImpl.java
@@ -0,0 +1,80 @@
+/**
+ * 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.validation;
+
+import org.apache.ambari.view.validation.ValidationResult;
+
+/**
+ * Simple validation result implementation.
+ */
+public class ValidationResultImpl implements ValidationResult{
+  /**
+   * Indicates whether or not the result is valid.
+   */
+  private final boolean valid;
+
+  /**
+   * Detail message for the result.
+   */
+  private final String detail;
+
+
+  // ----- Constructors ------------------------------------------------------
+
+  /**
+   * Construct a validation result.
+   *
+   * @param valid   indicates whether or not the result is valid
+   * @param detail  detail message for the result
+   */
+  public ValidationResultImpl(boolean valid, String detail) {
+    this.valid  = valid;
+    this.detail = detail;
+  }
+
+
+  // ----- ValidationResult --------------------------------------------------
+
+  @Override
+  public boolean isValid() {
+    return valid;
+  }
+
+  @Override
+  public String getDetail() {
+    return detail;
+  }
+
+
+  // ----- helper methods ----------------------------------------------------
+
+  /**
+   * Factory method to create a validation result from an existing result (accounts for null).
+   *
+   * @param result  the validation result; may be null
+   *
+   * @return a new validation result
+   */
+  public static ValidationResult create(ValidationResult result) {
+    if (result == null) {
+      result = SUCCESS;
+    }
+    return new ValidationResultImpl(result.isValid(), result.getDetail());
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BaseProviderTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BaseProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BaseProviderTest.java
index fc8acd0..380142e 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BaseProviderTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BaseProviderTest.java
@@ -171,6 +171,34 @@ public class BaseProviderTest {
   }
 
   @Test
+  public void testIsPropertyRequested() {
+    Set<String> propertyIds = new HashSet<String>();
+    propertyIds.add("p1");
+    propertyIds.add("foo");
+    propertyIds.add("cat1/foo");
+    propertyIds.add("cat2/bar");
+    propertyIds.add("cat2/baz");
+    propertyIds.add("cat3/sub1/bam");
+    propertyIds.add("cat4/sub2/sub3/bat");
+    propertyIds.add("cat5/sub5");
+
+    assertTrue(BaseProvider.isPropertyRequested("foo", propertyIds));
+
+    assertTrue(BaseProvider.isPropertyRequested("cat2", propertyIds));
+
+    assertTrue(BaseProvider.isPropertyRequested("cat2/bar", propertyIds));
+
+    assertFalse(BaseProvider.isPropertyRequested("unsupported", propertyIds));
+
+    // we should allow anything under the category cat5/sub5
+    assertTrue(BaseProvider.isPropertyRequested("cat5/sub5/prop5", propertyIds));
+    assertTrue(BaseProvider.isPropertyRequested("cat5/sub5/sub5a/prop5a", propertyIds));
+
+    // we shouldn't allow anything under the category cat5/sub7
+    assertFalse(BaseProvider.isPropertyRequested("cat5/sub7/unsupported", propertyIds));
+  }
+
+  @Test
   public void testSetResourcePropertyWithMaps() {
     Set<String> propertyIds = new HashSet<String>();
     propertyIds.add("cat1/emptyMapProperty");

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewEntityTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewEntityTest.java b/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewEntityTest.java
index 965cebb..f93403d 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewEntityTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewEntityTest.java
@@ -26,6 +26,9 @@ import org.apache.ambari.server.view.configuration.ResourceConfigTest;
 import org.apache.ambari.server.view.configuration.ViewConfig;
 import org.apache.ambari.server.view.configuration.ViewConfigTest;
 import org.apache.ambari.view.ViewDefinition;
+import org.apache.ambari.view.ViewInstanceDefinition;
+import org.apache.ambari.view.validation.ValidationResult;
+import org.apache.ambari.view.validation.Validator;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -268,6 +271,16 @@ public class ViewEntityTest {
   }
 
   @Test
+  public void testGetSetValidator() throws Exception {
+    ViewEntity viewDefinition = getViewEntity();
+
+    Validator validator = new TestValidator();
+
+    viewDefinition.setValidator(validator);
+    Assert.assertEquals(validator, viewDefinition.getValidator());
+  }
+
+  @Test
   public void testisDeployed() throws Exception {
     ViewEntity viewDefinition = getViewEntity();
 
@@ -294,4 +307,18 @@ public class ViewEntityTest {
     viewDefinition.setSystem(true);
     Assert.assertTrue(viewDefinition.isSystem());
   }
+
+  public static class TestValidator implements Validator {
+    ValidationResult result;
+
+    @Override
+    public ValidationResult validateInstance(ViewInstanceDefinition definition, ValidationContext mode) {
+      return result;
+    }
+
+    @Override
+    public ValidationResult validateProperty(String property, ViewInstanceDefinition definition, ValidationContext mode) {
+      return result;
+    }
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java b/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java
index 08aea6e..3aa17d1 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/orm/entities/ViewInstanceEntityTest.java
@@ -27,7 +27,11 @@ import org.apache.ambari.server.view.configuration.InstanceConfig;
 import org.apache.ambari.server.view.configuration.InstanceConfigTest;
 import org.apache.ambari.server.view.configuration.ViewConfig;
 import org.apache.ambari.server.view.configuration.ViewConfigTest;
+import org.apache.ambari.server.view.validation.InstanceValidationResultImpl;
+import org.apache.ambari.server.view.validation.ValidationResultImpl;
 import org.apache.ambari.view.ResourceProvider;
+import org.apache.ambari.view.validation.ValidationResult;
+import org.apache.ambari.view.validation.Validator;
 import org.junit.Assert;
 import org.junit.Test;
 import org.springframework.security.core.GrantedAuthority;
@@ -405,7 +409,26 @@ public class ViewInstanceEntityTest {
     ViewEntity viewEntity = ViewRegistryTest.getViewEntity(config, ambariConfig, getClass().getClassLoader(), "");
     ViewInstanceEntity viewInstanceEntity = ViewRegistryTest.getViewInstanceEntity(viewEntity, config.getInstances().get(0));
 
-    viewInstanceEntity.validate(viewEntity);
+    viewInstanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
+  }
+
+  @Test
+  public void testValidateWithValidator() throws Exception {
+
+    Properties properties = new Properties();
+    properties.put("p1", "v1");
+
+    Configuration ambariConfig = new Configuration(properties);
+
+    ViewConfig config = ViewConfigTest.getConfig(xml_valid_instance);
+    ViewEntity viewEntity = ViewRegistryTest.getViewEntity(config, ambariConfig, getClass().getClassLoader(), "");
+    ViewInstanceEntity viewInstanceEntity = ViewRegistryTest.getViewInstanceEntity(viewEntity, config.getInstances().get(0));
+
+    ViewEntityTest.TestValidator validator = new ViewEntityTest.TestValidator();
+    validator.result = new ValidationResultImpl(true, "detail");
+    viewEntity.setValidator(validator);
+
+    viewInstanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
   }
 
   @Test
@@ -421,13 +444,65 @@ public class ViewInstanceEntityTest {
     ViewInstanceEntity viewInstanceEntity = ViewRegistryTest.getViewInstanceEntity(viewEntity, config.getInstances().get(0));
 
     try {
-      viewInstanceEntity.validate(viewEntity);
+      viewInstanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
+      Assert.fail("Expected an IllegalStateException");
+    } catch (IllegalStateException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testValidateWithValidator_fail() throws Exception {
+
+    Properties properties = new Properties();
+    properties.put("p1", "v1");
+
+    Configuration ambariConfig = new Configuration(properties);
+
+    ViewConfig config = ViewConfigTest.getConfig(xml_invalid_instance);
+    ViewEntity viewEntity = ViewRegistryTest.getViewEntity(config, ambariConfig, getClass().getClassLoader(), "");
+    ViewInstanceEntity viewInstanceEntity = ViewRegistryTest.getViewInstanceEntity(viewEntity, config.getInstances().get(0));
+
+    ViewEntityTest.TestValidator validator = new ViewEntityTest.TestValidator();
+    validator.result = new ValidationResultImpl(false, "detail");
+    viewEntity.setValidator(validator);
+
+    try {
+      viewInstanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
       Assert.fail("Expected an IllegalStateException");
     } catch (IllegalStateException e) {
       // expected
     }
   }
 
+  @Test
+  public void testGetValidationResult() throws Exception {
+
+    Properties properties = new Properties();
+    properties.put("p1", "v1");
+
+    Configuration ambariConfig = new Configuration(properties);
+
+    ViewConfig config = ViewConfigTest.getConfig(xml_valid_instance);
+    ViewEntity viewEntity = ViewRegistryTest.getViewEntity(config, ambariConfig, getClass().getClassLoader(), "");
+    ViewInstanceEntity viewInstanceEntity = ViewRegistryTest.getViewInstanceEntity(viewEntity, config.getInstances().get(0));
+
+    ViewEntityTest.TestValidator validator = new ViewEntityTest.TestValidator();
+    validator.result = new ValidationResultImpl(true, "detail");
+    viewEntity.setValidator(validator);
+
+    InstanceValidationResultImpl result = viewInstanceEntity.getValidationResult(viewEntity, Validator.ValidationContext.PRE_CREATE);
+
+    Map<String, ValidationResult> propertyResults = result.getPropertyResults();
+
+    junit.framework.Assert.assertEquals(2, propertyResults.size());
+    junit.framework.Assert.assertTrue(propertyResults.containsKey("p1"));
+    junit.framework.Assert.assertTrue(propertyResults.containsKey("p2"));
+
+    junit.framework.Assert.assertTrue(propertyResults.get("p1").isValid());
+    junit.framework.Assert.assertTrue(propertyResults.get("p2").isValid());
+  }
+
   public static ViewInstanceEntity getViewInstanceEntity(SecurityHelper securityHelper)
       throws Exception {
     ViewInstanceEntity viewInstanceEntity = getViewInstanceEntity();

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java b/ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java
index f6ec403..04cbe2b 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/view/configuration/ViewConfigTest.java
@@ -24,6 +24,9 @@ import org.apache.ambari.view.ResourceAlreadyExistsException;
 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.validation.ValidationResult;
+import org.apache.ambari.view.validation.Validator;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -50,6 +53,7 @@ public class ViewConfigTest {
       "    <system>true</system>\n" +
       "    <icon64>/this/is/the/icon/url/icon64.png</icon64>\n" +
       "    <icon>/this/is/the/icon/url/icon.png</icon>\n" +
+      "    <validator-class>org.apache.ambari.server.view.configuration.ViewConfigTest$MyValidator</validator-class>" +
       "    <masker-class>org.apache.ambari.server.view.DefaultMasker</masker-class>" +
       "    <parameter>\n" +
       "        <name>p1</name>\n" +
@@ -169,6 +173,12 @@ public class ViewConfigTest {
   }
 
   @Test
+  public void testGetValidator() throws Exception {
+    ViewConfig config = getConfig();
+    Assert.assertEquals("org.apache.ambari.server.view.configuration.ViewConfigTest$MyValidator", config.getValidator());
+  }
+
+  @Test
   public void testMasker() throws Exception {
     ViewConfig config = getConfig();
     Assert.assertEquals("org.apache.ambari.server.view.DefaultMasker", config.getMasker());
@@ -249,6 +259,20 @@ public class ViewConfigTest {
     // nothing
   }
 
+  public static class MyValidator implements Validator {
+    ValidationResult result;
+
+    @Override
+    public ValidationResult validateInstance(ViewInstanceDefinition definition, ValidationContext mode) {
+      return result;
+    }
+
+    @Override
+    public ValidationResult validateProperty(String property, ViewInstanceDefinition definition, ValidationContext mode) {
+      return result;
+    }
+  }
+
   public static class MyResource {
     private String id;
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/view/validation/InstanceValidationResultImplTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/view/validation/InstanceValidationResultImplTest.java b/ambari-server/src/test/java/org/apache/ambari/server/view/validation/InstanceValidationResultImplTest.java
new file mode 100644
index 0000000..b375cd2
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/view/validation/InstanceValidationResultImplTest.java
@@ -0,0 +1,66 @@
+/**
+ * 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.validation;
+
+import junit.framework.Assert;
+import org.apache.ambari.view.validation.ValidationResult;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * InstanceValidationResultImpl tests.
+ */
+public class InstanceValidationResultImplTest {
+
+  @Test
+  public void testGetPropertyResults() throws Exception {
+    ValidationResult result = new ValidationResultImpl(true, "detail");
+    Map<String, ValidationResult> propertyResults = new HashMap<String, ValidationResult>();
+
+    propertyResults.put("foo", new ValidationResultImpl(true, "foo detail"));
+    propertyResults.put("bar", new ValidationResultImpl(false, "bar detail"));
+
+    InstanceValidationResultImpl instanceValidationResult = new InstanceValidationResultImpl(result, propertyResults);
+
+    propertyResults = instanceValidationResult.getPropertyResults();
+
+    Assert.assertEquals(2, propertyResults.size());
+    Assert.assertTrue(propertyResults.containsKey("foo"));
+    Assert.assertTrue(propertyResults.containsKey("bar"));
+
+    Assert.assertTrue(propertyResults.get("foo").isValid());
+    Assert.assertFalse(propertyResults.get("bar").isValid());
+  }
+
+  @Test
+  public void testToJson() throws Exception {
+    ValidationResult result = new ValidationResultImpl(true, "detail");
+    Map<String, ValidationResult> propertyResults = new HashMap<String, ValidationResult>();
+
+    propertyResults.put("foo", new ValidationResultImpl(true, "foo detail"));
+    propertyResults.put("bar", new ValidationResultImpl(false, "bar detail"));
+
+    InstanceValidationResultImpl instanceValidationResult = new InstanceValidationResultImpl(result, propertyResults);
+
+    Assert.assertEquals("{\"propertyResults\":{\"foo\":{\"valid\":true,\"detail\":\"foo detail\"},\"bar\":{\"valid\":false,\"detail\":\"bar detail\"}},\"valid\":false,\"detail\":\"The instance has invalid properties.\"}",
+        instanceValidationResult.toJson());
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-server/src/test/java/org/apache/ambari/server/view/validation/ValidationResultImplTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/view/validation/ValidationResultImplTest.java b/ambari-server/src/test/java/org/apache/ambari/server/view/validation/ValidationResultImplTest.java
new file mode 100644
index 0000000..9ad201c
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/view/validation/ValidationResultImplTest.java
@@ -0,0 +1,55 @@
+/**
+ * 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.validation;
+
+import junit.framework.Assert;
+import org.apache.ambari.view.validation.ValidationResult;
+import org.junit.Test;
+
+/**
+ * ValidationResultImpl tests.
+ */
+public class ValidationResultImplTest {
+
+  @Test
+  public void testIsValid() throws Exception {
+    ValidationResult result = new ValidationResultImpl(true, "detail");
+    Assert.assertTrue(result.isValid());
+
+    result = new ValidationResultImpl(false, "detail");
+    Assert.assertFalse(result.isValid());
+  }
+
+  @Test
+  public void testGetDetail() throws Exception {
+    ValidationResult result = new ValidationResultImpl(true, "detail");
+    Assert.assertEquals("detail", result.getDetail());
+  }
+
+  @Test
+  public void testCreate() throws Exception {
+    ValidationResult result = ValidationResultImpl.create(new ValidationResultImpl(true, "is true"));
+    Assert.assertTrue(result.isValid());
+    Assert.assertEquals("is true", result.getDetail());
+
+    result = ValidationResultImpl.create(new ValidationResultImpl(false, "is false"));
+    Assert.assertFalse(result.isValid());
+    Assert.assertEquals("is false", result.getDetail());
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-views/src/main/java/org/apache/ambari/view/validation/ValidationResult.java
----------------------------------------------------------------------
diff --git a/ambari-views/src/main/java/org/apache/ambari/view/validation/ValidationResult.java b/ambari-views/src/main/java/org/apache/ambari/view/validation/ValidationResult.java
new file mode 100644
index 0000000..85ea399
--- /dev/null
+++ b/ambari-views/src/main/java/org/apache/ambari/view/validation/ValidationResult.java
@@ -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.
+ */
+
+package org.apache.ambari.view.validation;
+
+/**
+ * Validation result.
+ */
+public interface ValidationResult {
+
+  /**
+   * Determine whether or not the result is valid.
+   *
+   * @return true if the result is valid
+   */
+  public boolean isValid();
+
+  /**
+   * Get the detail of the validation result.
+   *
+   * @return the validation result detail
+   */
+  public String getDetail();
+
+  /**
+   * Successful validation result.
+   */
+  public static final ValidationResult SUCCESS = new ValidationResult() {
+    @Override
+    public boolean isValid() {
+      return true;
+    }
+
+    @Override
+    public String getDetail() {
+      return "OK";
+    }
+  };
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java
----------------------------------------------------------------------
diff --git a/ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java b/ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java
new file mode 100644
index 0000000..ee029a8
--- /dev/null
+++ b/ambari-views/src/main/java/org/apache/ambari/view/validation/Validator.java
@@ -0,0 +1,59 @@
+/**
+ * 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.validation;
+
+import org.apache.ambari.view.ViewInstanceDefinition;
+
+/**
+ * Interface for custom view validation.  The validator is used validate a view instance and
+ * its properties.
+ */
+public interface Validator {
+  /**
+   * Validate the given view instance definition.  Return {@code null} to indicate that
+   * no validation was performed.
+   *
+   * @param definition  the view instance definition
+   * @param mode        the validation mode
+   *
+   * @return the instance validation result; may be {@code null}
+   */
+  public ValidationResult validateInstance(ViewInstanceDefinition definition, ValidationContext mode);
+
+  /**
+   * Validate a property of the given view instance definition.  Return {@code null} to indicate that
+   * no validation was performed.
+   *
+   * @param property    the property name
+   * @param definition  the view instance definition
+   * @param mode        the validation mode
+   *
+   * @return the instance validation result; may be {@code null}
+   */
+  public ValidationResult validateProperty(String property, ViewInstanceDefinition definition, ValidationContext mode);
+
+  /**
+   * The context in which a view instance validation check is performed.
+   */
+  public enum ValidationContext {
+    PRE_CREATE,  // validation prior to creation
+    PRE_UPDATE,  // validation prior to update
+    EXISTING     // validation of an existing view instance
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/e2757784/ambari-views/src/main/resources/view.xsd
----------------------------------------------------------------------
diff --git a/ambari-views/src/main/resources/view.xsd b/ambari-views/src/main/resources/view.xsd
index b95e4ec..a9e5b12 100644
--- a/ambari-views/src/main/resources/view.xsd
+++ b/ambari-views/src/main/resources/view.xsd
@@ -249,6 +249,11 @@
             <xs:documentation>The View class to receive framework events.</xs:documentation>
           </xs:annotation>
         </xs:element>
+        <xs:element type="xs:string" name="validator-class" minOccurs="0" maxOccurs="1">
+          <xs:annotation>
+            <xs:documentation>The Validator class to validate view instances and properties.</xs:documentation>
+          </xs:annotation>
+        </xs:element>
         <xs:element type="xs:string" name="masker-class" minOccurs="0" maxOccurs="1">
           <xs:annotation>
             <xs:documentation>The Masker class for masking view parameters.</xs:documentation>