/*
 * 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 java.beans.IntrospectionException;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.xml.bind.JAXBException;

import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.inject.Singleton;

import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.ClusterNotFoundException;
import org.apache.ambari.server.api.resources.ResourceInstanceFactoryImpl;
import org.apache.ambari.server.api.resources.SubResourceDefinition;
import org.apache.ambari.server.api.resources.ViewExternalSubResourceDefinition;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.api.services.views.ViewExternalSubResourceService;
import org.apache.ambari.server.api.services.views.ViewSubResourceService;
import org.apache.ambari.server.configuration.ComponentSSLConfiguration;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.AmbariServer;
import org.apache.ambari.server.controller.AmbariSessionManager;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceProvider;
import org.apache.ambari.server.events.ClusterConfigFinishedEvent;
import org.apache.ambari.server.events.ServiceInstalledEvent;
import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
import org.apache.ambari.server.orm.dao.MemberDAO;
import org.apache.ambari.server.orm.dao.PermissionDAO;
import org.apache.ambari.server.orm.dao.PrincipalDAO;
import org.apache.ambari.server.orm.dao.PrivilegeDAO;
import org.apache.ambari.server.orm.dao.RemoteAmbariClusterDAO;
import org.apache.ambari.server.orm.dao.ResourceDAO;
import org.apache.ambari.server.orm.dao.ResourceTypeDAO;
import org.apache.ambari.server.orm.dao.UserDAO;
import org.apache.ambari.server.orm.dao.ViewDAO;
import org.apache.ambari.server.orm.dao.ViewInstanceDAO;
import org.apache.ambari.server.orm.dao.ViewURLDAO;
import org.apache.ambari.server.orm.entities.GroupEntity;
import org.apache.ambari.server.orm.entities.MemberEntity;
import org.apache.ambari.server.orm.entities.PermissionEntity;
import org.apache.ambari.server.orm.entities.PrincipalEntity;
import org.apache.ambari.server.orm.entities.PrivilegeEntity;
import org.apache.ambari.server.orm.entities.RemoteAmbariClusterEntity;
import org.apache.ambari.server.orm.entities.ResourceEntity;
import org.apache.ambari.server.orm.entities.ResourceTypeEntity;
import org.apache.ambari.server.orm.entities.UserEntity;
import org.apache.ambari.server.orm.entities.ViewEntity;
import org.apache.ambari.server.orm.entities.ViewEntityEntity;
import org.apache.ambari.server.orm.entities.ViewInstanceDataEntity;
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.orm.entities.ViewResourceEntity;
import org.apache.ambari.server.orm.entities.ViewURLEntity;
import org.apache.ambari.server.security.SecurityHelper;
import org.apache.ambari.server.security.authorization.AuthorizationHelper;
import org.apache.ambari.server.security.authorization.ResourceType;
import org.apache.ambari.server.security.authorization.RoleAuthorization;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.Service;
import org.apache.ambari.server.state.ServiceComponent;
import org.apache.ambari.server.state.StackId;
import org.apache.ambari.server.state.stack.OsFamily;
import org.apache.ambari.server.utils.Closeables;
import org.apache.ambari.server.utils.VersionUtils;
import org.apache.ambari.server.view.configuration.AutoInstanceConfig;
import org.apache.ambari.server.view.configuration.EntityConfig;
import org.apache.ambari.server.view.configuration.InstanceConfig;
import org.apache.ambari.server.view.configuration.ParameterConfig;
import org.apache.ambari.server.view.configuration.PermissionConfig;
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.server.view.validation.ValidationException;
import org.apache.ambari.view.AmbariStreamProvider;
import org.apache.ambari.view.ClusterType;
import org.apache.ambari.view.Masker;
import org.apache.ambari.view.SystemException;
import org.apache.ambari.view.View;
import org.apache.ambari.view.ViewContext;
import org.apache.ambari.view.ViewDefinition;
import org.apache.ambari.view.ViewInstanceDefinition;
import org.apache.ambari.view.ViewResourceHandler;
import org.apache.ambari.view.cluster.Cluster;
import org.apache.ambari.view.events.Event;
import org.apache.ambari.view.events.Listener;
import org.apache.ambari.view.migration.ViewDataMigrationException;
import org.apache.ambari.view.validation.Validator;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.log4j.PropertyConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.persist.Transactional;


/**
 * Registry for view and view instance definitions.
 */
@Singleton
public class ViewRegistry {

  /**
   * Constants
   */
  private static final String EXTRACTED_ARCHIVES_DIR = "work";
  private static final String EXTRACT_COMMAND = "extract";
  private static final String ALL_VIEWS_REG_EXP = ".*";
  protected static final int DEFAULT_REQUEST_CONNECT_TIMEOUT = 5000;
  protected static final int DEFAULT_REQUEST_READ_TIMEOUT = 10000;
  private static final String VIEW_AMBARI_VERSION_REGEXP = "^((\\d+\\.)?)*(\\*|\\d+)$";
  private static final String VIEW_LOG_FILE = "view.log4j.properties";
  private static final String AMBARI_LOG_FILE = "log4j.properties";
  private static final String LOG4J = "log4j.";

  public static final String API_PREFIX = "/api/v1/clusters/";
  public static final String DEFAULT_AUTO_INSTANCE_URL = "auto_instance";

  /**
   * Thread pool
   */
  private static ExecutorService executorService;

  /**
   * Mapping of view names to view definitions.
   */
  private Map<String, ViewEntity> viewDefinitions = new HashMap<>();

  /**
   * Mapping of view instances to view definition and instance name.
   */
  private Map<ViewEntity, Map<String, ViewInstanceEntity>> viewInstanceDefinitions =
    new HashMap<>();

  /**
   * Mapping of view names to sub-resources.
   */
  private final Map<String, Set<SubResourceDefinition>> subResourceDefinitionsMap =
    new ConcurrentHashMap<>();

  /**
   * Mapping of view types to resource providers.
   */
  private final Map<Resource.Type, ResourceProvider> resourceProviders =
    new ConcurrentHashMap<>();

  /**
   * Mapping of view names to registered listeners.
   */
  private final Map<String, Set<Listener>> listeners =
    new ConcurrentHashMap<>();

  /**
   * The singleton view registry instance.
   */
  private static ViewRegistry singleton;

  /**
   * The logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(ViewRegistry.class);

  /**
   * View Data Migration Utility
   */
  protected ViewDataMigrationUtility viewDataMigrationUtility;

  /**
   * View data access object.
   */
  @Inject
  ViewDAO viewDAO;

  /**
   * View instance data access object.
   */
  @Inject
  ViewInstanceDAO instanceDAO;

  /**
   * User data access object.
   */
  @Inject
  UserDAO userDAO;

  /**
   * Group member data access object.
   */
  @Inject
  MemberDAO memberDAO;

  /**
   * Privilege data access object.
   */
  @Inject
  PrivilegeDAO privilegeDAO;

  /**
   * Helper with security related utilities.
   */
  @Inject
  SecurityHelper securityHelper;

  /**
   * Resource data access object.
   */
  @Inject
  ResourceDAO resourceDAO;

  /**
   * Resource type data access object.
   */
  @Inject
  ResourceTypeDAO resourceTypeDAO;

  /**
   * Principal data access object.
   */
  @Inject
  PrincipalDAO principalDAO;

  /**
   * Permission data access objects
   */
  @Inject
  PermissionDAO permissionDAO;

  /**
   * The Ambari managed clusters.
   */
  @Inject
  Provider<Clusters> clustersProvider;

  /**
   * Ambari meta info.
   */
  @Inject
  Provider<AmbariMetaInfo> ambariMetaInfoProvider;

  /**
   * Ambari configuration.
   */
  @Inject
  Configuration configuration;

  /**
   * The handler list.
   */
  @Inject
  ViewInstanceHandlerList handlerList;

  /**
   * The view extractor;
   */
  @Inject
  ViewExtractor extractor;

  /**
   * The view archive utility.
   */
  @Inject
  ViewArchiveUtility archiveUtility;

  /**
   * The Ambari session manager.
   */
  @Inject
  AmbariSessionManager ambariSessionManager;

  /**
   * Registry for Remote Ambari Cluster
   */
  @Inject
  RemoteAmbariClusterRegistry remoteAmbariClusterRegistry;

  /**
   * Remote Ambari Cluster Dao
   */
  @Inject
  RemoteAmbariClusterDAO remoteAmbariClusterDAO;

  @Inject
  ViewURLDAO viewURLDAO;

  @Inject
  ViewInstanceOperationHandler viewInstanceOperationHandler;
  // ----- Constructors -----------------------------------------------------

  /**
   * Create the view registry.
   */
  @Inject
  public ViewRegistry(AmbariEventPublisher publisher) {
    publisher.register(this);
  }


// ----- ViewRegistry ------------------------------------------------------

  /**
   * Registry main method.
   *
   * @param args the command line arguments
   */
  public static void main(String[] args) {

    if (args.length >= 2) {

      if (args[0].equals(EXTRACT_COMMAND)) {

        String archivePath = args[1];
        ViewModule viewModule = new ViewModule();

        try {
          if (extractViewArchive(archivePath, viewModule, true)) {
            System.exit(0);
          }
        } catch (Exception e) {
          String msg = "Caught exception extracting view archive " + archivePath + ".";
          LOG.error(msg, e);
          System.exit(2);
        }
      }
    }
    System.exit(1);
  }

  /**
   * Get the collection of all the view definitions.
   *
   * @return the collection of view definitions
   */
  public Collection<ViewEntity> getDefinitions() {
    return viewDefinitions.values();
  }

  /**
   * Get a view definition for the given name.
   *
   * @param viewName the view name
   * @param version  the version
   * @return the view definition for the given name
   */
  public ViewEntity getDefinition(String viewName, String version) {
    return getDefinition(ViewEntity.getViewName(viewName, version));
  }

  /**
   * Get the view definition for the given resource type.
   *
   * @param resourceTypeEntity the resource type
   * @return the view definition for the given resource type or null
   */
  public ViewEntity getDefinition(ResourceTypeEntity resourceTypeEntity) {

    for (ViewEntity viewEntity : viewDefinitions.values()) {
      if (viewEntity.isDeployed()) {
        if (viewEntity.getResourceType().equals(resourceTypeEntity)) {
          return viewEntity;
        }
      }
    }
    return null;
  }

  /**
   * Add a view definition to the registry.
   *
   * @param definition the definition
   */
  public void addDefinition(ViewEntity definition) {
    viewDefinitions.put(definition.getName(), definition);
  }

  /**
   * Get the collection of view instances for the given view definition.
   *
   * @param definition the view definition
   * @return the collection of view instances for the view definition
   */
  public Collection<ViewInstanceEntity> getInstanceDefinitions(ViewEntity definition) {
    if (definition != null) {
      Map<String, ViewInstanceEntity> instanceEntityMap = viewInstanceDefinitions.get(definition);
      if (instanceEntityMap != null) {
        return instanceEntityMap.values();
      }
    }
    return Collections.emptyList();
  }

  /**
   * Get the instance definition for the given view name and instance name.
   *
   * @param viewName     the view name
   * @param version      the version
   * @param instanceName the instance name
   * @return the view instance definition for the given view and instance name
   */
  public ViewInstanceEntity getInstanceDefinition(String viewName, String version, String instanceName) {
    Map<String, ViewInstanceEntity> viewInstanceDefinitionMap =
        viewInstanceDefinitions.get(getDefinition(viewName, version));

    return viewInstanceDefinitionMap == null ? null : viewInstanceDefinitionMap.get(instanceName);
  }

  /**
   * Add an instance definition for the given view definition.
   *
   * @param definition         the owning view definition
   * @param instanceDefinition the instance definition
   */
  public void addInstanceDefinition(ViewEntity definition, ViewInstanceEntity instanceDefinition) {
    Map<String, ViewInstanceEntity> instanceDefinitions = viewInstanceDefinitions.get(definition);
    if (instanceDefinitions == null) {
      instanceDefinitions = new HashMap<>();
      viewInstanceDefinitions.put(definition, instanceDefinitions);
    }

    View view = definition.getView();
    if (view != null) {
      view.onCreate(instanceDefinition);
    }
    instanceDefinitions.put(instanceDefinition.getName(), instanceDefinition);
  }

  /**
   * Remove an instance definition for the given view definition.
   *
   * @param definition   the owning view definition
   * @param instanceName the instance name
   */
  public void removeInstanceDefinition(ViewEntity definition, String instanceName) {
    Map<String, ViewInstanceEntity> instanceDefinitions = viewInstanceDefinitions.get(definition);
    if (instanceDefinitions != null) {

      ViewInstanceEntity instanceDefinition = instanceDefinitions.get(instanceName);
      if (instanceDefinition != null) {
        View view = definition.getView();
        if (view != null) {
          view.onDestroy(instanceDefinition);
        }
        instanceDefinitions.remove(instanceName);
      }
    }
  }

  /**
   * Init the singleton instance.
   *
   * @param singleton the view registry
   */
  public static void initInstance(ViewRegistry singleton) {
    ViewRegistry.singleton = singleton;
  }

  /**
   * Get the view registry singleton.
   *
   * @return the view registry
   */
  public static ViewRegistry getInstance() {
    return singleton;
  }

  /**
   * Get the sub-resource definitions for the given view name.
   *
   * @param viewName the instance name
   * @param version  the version
   * @return the set of sub-resource definitions
   */
  public Set<SubResourceDefinition> getSubResourceDefinitions(
      String viewName, String version) {


    viewName = ViewEntity.getViewName(viewName, version);

    return subResourceDefinitionsMap.get(viewName);
  }

  /**
   * Read all view archives.
   */
  public void readViewArchives() {
    boolean systemViewsOnly = configuration.extractViewsAfterClusterConfig() && clustersProvider.get().getClusters().isEmpty();
    LOG.info("Triggering loading of [{}] views", systemViewsOnly ? "SYSTEM" : "ALL");
    readViewArchives(systemViewsOnly, false, ALL_VIEWS_REG_EXP);
  }

  /**
   * Read only view archives with names corresponding to given regular expression.
   *
   * @param viewNameRegExp view name regular expression
   */
  public void readViewArchives(String viewNameRegExp) {
    readViewArchives(false, false, viewNameRegExp);
  }

  /**
   * Determine whether or not the given view instance exists.
   *
   * @param instanceEntity the view instance entity
   * @return true if the the given view instance exists; false otherwise
   */
  public boolean instanceExists(ViewInstanceEntity instanceEntity) {

    ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());

    return viewEntity != null &&
        (getInstanceDefinition(viewEntity.getCommonName(), viewEntity.getVersion(), instanceEntity.getName()) != null);
  }

  /**
   * TODO : This should move to {@link ViewInstanceOperationHandler}
   * Install the given view instance with its associated view.
   *
   * @param instanceEntity the view instance entity
   * @throws ValidationException      if the given instance fails the validation checks
   * @throws IllegalArgumentException if the view associated with the given instance
   *                                  does not exist
   * @throws SystemException          if the instance can not be installed
   */
  public void installViewInstance(ViewInstanceEntity instanceEntity)
      throws ValidationException, IllegalArgumentException, SystemException {
    ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());

    if (viewEntity != null) {
      String instanceName = instanceEntity.getName();
      String viewName = viewEntity.getCommonName();
      String version = viewEntity.getVersion();

      if (getInstanceDefinition(viewName, version, instanceName) == null) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Creating view instance {}/{}/{}", viewName, version, instanceName);
        }

        instanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_CREATE);
        setPersistenceEntities(instanceEntity);

        ViewInstanceEntity persistedInstance = mergeViewInstance(instanceEntity, viewEntity.getResourceType());

        instanceEntity.setViewInstanceId(persistedInstance.getViewInstanceId());
        syncViewInstance(instanceEntity, persistedInstance);

        try {
          // bind the view instance to a view
          bindViewInstance(viewEntity, instanceEntity);
        } catch (Exception e) {
          String message = "Caught exception installing view instance.";
          LOG.error(message, e);
          throw new IllegalStateException(message, e);
        }
        // update the registry
        addInstanceDefinition(viewEntity, instanceEntity);

        // add the web app context
        handlerList.addViewInstance(instanceEntity);
      }

    } else {
      String message = "Attempt to install an instance for an unknown view " +
          instanceEntity.getViewName() + ".";

      LOG.error(message);
      throw new IllegalArgumentException(message);
    }
  }

  /**
   * Update a view instance for the view with the given view name.
   *
   * @param instanceEntity the view instance entity
   * @throws ValidationException if the given instance fails the validation checks
   * @throws SystemException     if the instance can not be updated
   */
  public void updateViewInstance(ViewInstanceEntity instanceEntity)
      throws ValidationException, SystemException {
    ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());

    if (viewEntity != null) {
      instanceEntity.validate(viewEntity, Validator.ValidationContext.PRE_UPDATE);
      instanceDAO.merge(instanceEntity);

      syncViewInstance(instanceEntity);
    }
  }

  /**
   * Calls onUpdate hook on View class
   *
   * @param instanceEntity
   */
  public void updateView(ViewInstanceEntity instanceEntity) {
    ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());
    if (null != viewEntity && null != viewEntity.getView()) {
      viewEntity.getView().onUpdate(instanceEntity);
    }
  }

  /**
   * Get a view instance entity for the given view name and instance name.
   *
   * @param viewName     the view name
   * @param instanceName the instance name
   * @return a view instance entity for the given view name and instance name.
   */
  public ViewInstanceEntity getViewInstanceEntity(String viewName, String instanceName) {
    return instanceDAO.findByName(viewName, instanceName);
  }

  /**
   * Uninstall a view instance for the view with the given view name.
   *
   * @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 {
    try {
      viewInstanceOperationHandler.uninstallViewInstance(instanceEntity);
      updateCaches(instanceEntity);
    } catch (IllegalStateException illegalStateExcpetion) {
      LOG.error("Exception occurred while uninstalling view : {}", instanceEntity, illegalStateExcpetion);
      throw illegalStateExcpetion;
    }
  }

  private void updateCaches(ViewInstanceEntity instanceEntity) {
    ViewEntity viewEntity = getDefinition(instanceEntity.getViewName());
    viewEntity.removeInstanceDefinition(instanceEntity.getInstanceName());
    removeInstanceDefinition(viewEntity, instanceEntity.getInstanceName());
    // remove the web app context
    handlerList.removeViewInstance(instanceEntity);
  }

  /**
   * Remove the data entry keyed by the given key from the given instance entity.
   *
   * @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) {
      instanceDAO.removeData(dataEntity);
    }
    instanceEntity.removeInstanceData(key);
    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 {} to {}", sourceInstanceEntity.getName(), targetInstanceEntity.getName());
    List<PrivilegeEntity> targetInstancePrivileges = privilegeDAO.findByResourceId(targetInstanceEntity.getResource().getId());
    if (targetInstancePrivileges.size() > 0) {
      LOG.warn("Target instance {} already has privileges assigned, these will not be deleted. Manual clean up may be needed", targetInstanceEntity.getName());
    }

    List<PrivilegeEntity> sourceInstancePrivileges = privilegeDAO.findByResourceId(sourceInstanceEntity.getResource().getId());
    for (PrivilegeEntity sourcePrivilege : sourceInstancePrivileges) {
      PrivilegeEntity targetPrivilege = new PrivilegeEntity();
      targetPrivilege.setPrincipal(sourcePrivilege.getPrincipal());
      targetPrivilege.setResource(targetInstanceEntity.getResource());
      targetPrivilege.setPermission(sourcePrivilege.getPermission());
      try {
        privilegeDAO.create(targetPrivilege);
        targetPrivilege.getPrincipal().getPrivileges().add(sourcePrivilege);
      } catch (Exception e) {
        LOG.warn("Could not migrate privilege {} ", targetPrivilege);
        LOG.error("Caught exception", e);
      }

    }
  }

  /**
   * Notify any registered listeners of the given event.
   *
   * @param event the event
   */
  public void fireEvent(Event event) {

    ViewDefinition subject = event.getViewSubject();

    fireEvent(event, subject.getViewName());
    fireEvent(event, ViewEntity.getViewName(subject.getViewName(), subject.getVersion()));
  }

  /**
   * Register the given listener to listen for events from the view identified by the given name and version.
   *
   * @param listener    the listener
   * @param viewName    the view name
   * @param viewVersion the view version; null indicates all versions
   */
  public synchronized void registerListener(Listener listener, String viewName, String viewVersion) {

    String name = viewVersion == null ? viewName : ViewEntity.getViewName(viewName, viewVersion);

    Set<Listener> listeners = this.listeners.get(name);

    if (listeners == null) {
      listeners = Sets.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>());
      this.listeners.put(name, listeners);
    }

    listeners.add(listener);
  }

  /**
   * Un-register the given listener from the view identified by the given name and version.
   *
   * @param listener    the listener
   * @param viewName    the view name
   * @param viewVersion the view version; null indicates all versions
   */
  public synchronized void unregisterListener(Listener listener, String viewName, String viewVersion) {

    String name = viewVersion == null ? viewName : ViewEntity.getViewName(viewName, viewVersion);

    Set<Listener> listeners = this.listeners.get(name);

    if (listeners != null) {
      listeners.remove(listener);
    }
  }

  /**
   * Determine whether or not the access specified by the given permission
   * is permitted for the given user on the view instance identified by
   * the given resource.
   *
   * @param permissionEntity the permission entity
   * @param resourceEntity   the resource entity
   * @param userName         the user name
   * @return true if the access specified by the given permission
   * is permitted for the given user.
   */
  public boolean hasPermission(PermissionEntity permissionEntity, ResourceEntity resourceEntity, String userName) {

    UserEntity userEntity = userDAO.findUserByName(userName);

    if (userEntity == null) {
      return false;
    }

    if (privilegeDAO.exists(userEntity.getPrincipal(), resourceEntity, permissionEntity)) {
      return true;
    }

    List<MemberEntity> memberEntities = memberDAO.findAllMembersByUser(userEntity);

    for (MemberEntity memberEntity : memberEntities) {

      GroupEntity groupEntity = memberEntity.getGroup();

      if (privilegeDAO.exists(groupEntity.getPrincipal(), resourceEntity, permissionEntity)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Determine whether or not access to the view instance resource identified
   * by the given instance name should be allowed based on the permissions
   * granted to the current user.
   *
   * @param viewName     the view name
   * @param version      the view version
   * @param instanceName the name of the view instance resource
   * @param readOnly     indicate whether or not this is for a read only operation
   * @return true if the access to the view instance is allowed
   */
  public boolean checkPermission(String viewName, String version, String instanceName, boolean readOnly) {

    ViewInstanceEntity instanceEntity =
        instanceName == null ? null : getInstanceDefinition(viewName, version, instanceName);

    return checkPermission(instanceEntity, readOnly);
  }

  /**
   * Determine whether or not access to the given view instance should be allowed based
   * on the permissions granted to the current user.
   *
   * @param instanceEntity the view instance entity
   * @param readOnly       indicate whether or not this is for a read only operation
   * @return true if the access to the view instance is allowed
   */
  public boolean checkPermission(ViewInstanceEntity instanceEntity, boolean readOnly) {

    ResourceEntity resourceEntity = instanceEntity == null ? null : instanceEntity.getResource();

    return (resourceEntity == null && readOnly) || checkAuthorization(resourceEntity);
  }

  /**
   * Determine whether or not the current view user has admin rights.
   *
   * @return true if the current view user is an admin
   */
  public boolean checkAdmin() {
    return checkAuthorization(null);
  }

  /**
   * Determine whether or not the given view definition resource should be included
   * based on the permissions granted to the current user.
   *
   * @param definitionEntity the view definition entity
   * @return true if the view instance should be included based on the permissions of the current user
   */
  public boolean includeDefinition(ViewEntity definitionEntity) {

    if (checkPermission(null, false)) {
      return true;
    }

    for (ViewInstanceEntity instanceEntity : definitionEntity.getInstances()) {
      if (checkPermission(instanceEntity, true)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Set the properties of the given view instance from the given property set.
   *
   * @param instanceEntity the view instance entity
   * @param properties     the view instance properties
   * @param viewConfig     the view configuration
   * @param classLoader    the class loader for the view
   * @throws SystemException if the view instance properties can not be set
   */
  public void setViewInstanceProperties(ViewInstanceEntity instanceEntity, Map<String, String> properties,
                                        ViewConfig viewConfig, ClassLoader classLoader) throws SystemException {
    try {
      Masker masker = getMasker(viewConfig.getMaskerClass(classLoader));

      Map<String, ParameterConfig> parameterConfigMap = new HashMap<>();
      for (ParameterConfig paramConfig : viewConfig.getParameters()) {
        parameterConfigMap.put(paramConfig.getName(), paramConfig);
      }
      for (Map.Entry<String, String> entry : properties.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();

        ParameterConfig parameterConfig = parameterConfigMap.get(name);

        if (parameterConfig != null && parameterConfig.isMasked()) {
          value = masker.mask(value);
        }
        instanceEntity.putProperty(name, value);
      }
    } catch (Exception e) {
      throw new SystemException("Caught exception while setting instance property.", e);
    }
  }

  /**
   * Get the cluster associated with the given view instance.
   *
   * @param viewInstance the view instance
   * @return the cluster
   */
  public Cluster getCluster(ViewInstanceDefinition viewInstance) {
    if (viewInstance != null) {
      Long clusterId = viewInstance.getClusterHandle();

      if (clusterId != null && viewInstance.getClusterType() == ClusterType.LOCAL_AMBARI) {
        try {
          return new ClusterImpl(clustersProvider.get().getCluster(clusterId));
        } catch (AmbariException e) {
          LOG.error("Could not find the cluster identified by {}.", clusterId);
          throw new IllegalClusterException(e);
        }

      } else if (clusterId != null && viewInstance.getClusterType() == ClusterType.REMOTE_AMBARI) {
        try {
          return remoteAmbariClusterRegistry.get(clusterId);
        } catch (MalformedURLException e) {
          LOG.error("Remote Cluster with id={} had invalid URL.", clusterId, e);
          throw new IllegalClusterException(e);
        } catch (ClusterNotFoundException e) {
          LOG.error("Cannot get Remote Cluster with id={}.", clusterId, e);
          throw new IllegalClusterException(e);
        }
      }
    }
    return null;
  }

  /**
   * Receive notification that a new service has been added to a cluster.
   * </p>
   * Used for view instance auto creation.
   *
   * @param event the service installed event
   */
  @Subscribe
  @AllowConcurrentEvents
  public void onAmbariEvent(ServiceInstalledEvent event) {

    Clusters clusters = clustersProvider.get();
    Long clusterId = event.getClusterId();

    try {
      org.apache.ambari.server.state.Cluster cluster = clusters.getClusterById(clusterId);
