content: /*
   * 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.controller.internal;
  
  import java.text.MessageFormat;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.EnumSet;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.StaticallyInject;
  import org.apache.ambari.server.api.resources.AlertTargetResourceDefinition;
  import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
  import org.apache.ambari.server.controller.spi.NoSuchResourceException;
  import org.apache.ambari.server.controller.spi.Predicate;
  import org.apache.ambari.server.controller.spi.Request;
  import org.apache.ambari.server.controller.spi.RequestStatus;
  import org.apache.ambari.server.controller.spi.Resource;
  import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
  import org.apache.ambari.server.controller.spi.SystemException;
  import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
  import org.apache.ambari.server.controller.utilities.PropertyHelper;
  import org.apache.ambari.server.notifications.DispatchFactory;
  import org.apache.ambari.server.notifications.NotificationDispatcher;
  import org.apache.ambari.server.notifications.TargetConfigurationResult;
  import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
  import org.apache.ambari.server.orm.entities.AlertGroupEntity;
  import org.apache.ambari.server.orm.entities.AlertTargetEntity;
  import org.apache.ambari.server.security.authorization.RoleAuthorization;
  import org.apache.ambari.server.state.AlertState;
  import org.apache.ambari.server.state.alert.AlertGroup;
  import org.apache.ambari.server.state.alert.AlertTarget;
  import org.apache.commons.lang.StringUtils;
  import org.apache.commons.text.StringEscapeUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.gson.Gson;
  import com.google.inject.Inject;
  import com.google.inject.persist.Transactional;
  
  /**
   * The {@link AlertTargetResourceProvider} class deals with managing the CRUD
   * operations for {@link AlertTarget}, including property coercion to and from
   * {@link AlertTargetEntity}.
   */
  @StaticallyInject
  public class AlertTargetResourceProvider extends
   AbstractAuthorizedResourceProvider {
  
    private static final Logger LOG = LoggerFactory.getLogger(AlertTargetResourceProvider.class);
      
    public static final String ALERT_TARGET = "AlertTarget";
  
    public static final String ID_PROPERTY_ID = "id";
    public static final String NAME_PROPERTY_ID = "name";
    public static final String DESCRIPTION_PROPERTY_ID = "description";
    public static final String NOTIFICATION_TYPE_PROPERTY_ID = "notification_type";
    public static final String PROPERTIES_PROPERTY_ID = "properties";
    public static final String GROUPS_PROPERTY_ID = "groups";
    public static final String STATES_PROPERTY_ID = "alert_states";
    public static final String GLOBAL_PROPERTY_ID = "global";
    public static final String ENABLED_PROPERTY_ID = "enabled";
  
    public static final String ALERT_TARGET_ID = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + ID_PROPERTY_ID;
    public static final String ALERT_TARGET_NAME = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + NAME_PROPERTY_ID;
    public static final String ALERT_TARGET_DESCRIPTION = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + DESCRIPTION_PROPERTY_ID;
    public static final String ALERT_TARGET_NOTIFICATION_TYPE = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + NOTIFICATION_TYPE_PROPERTY_ID;
    public static final String ALERT_TARGET_PROPERTIES = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + PROPERTIES_PROPERTY_ID;
    public static final String ALERT_TARGET_GROUPS = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + GROUPS_PROPERTY_ID;
    public static final String ALERT_TARGET_STATES = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + STATES_PROPERTY_ID;
    public static final String ALERT_TARGET_GLOBAL = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + GLOBAL_PROPERTY_ID;
    public static final String ALERT_TARGET_ENABLED = ALERT_TARGET + PropertyHelper.EXTERNAL_PATH_SEP + ENABLED_PROPERTY_ID;
  
    private static final Set<String> PK_PROPERTY_IDS = new HashSet<>(
        Arrays.asList(ALERT_TARGET_ID, ALERT_TARGET_NAME));
  
    /**
     * The property ids for an alert target resource.
     */
    private static final Set<String> PROPERTY_IDS = new HashSet<>();
  
    /**
     * The key property ids for an alert target resource.
     */
    private static final Map<Resource.Type, String> KEY_PROPERTY_IDS = new HashMap<>();
  
    static {
      // properties
      PROPERTY_IDS.add(ALERT_TARGET_ID);
      PROPERTY_IDS.add(ALERT_TARGET_NAME);
      PROPERTY_IDS.add(ALERT_TARGET_DESCRIPTION);
      PROPERTY_IDS.add(ALERT_TARGET_NOTIFICATION_TYPE);
      PROPERTY_IDS.add(ALERT_TARGET_PROPERTIES);
      PROPERTY_IDS.add(ALERT_TARGET_GROUPS);
      PROPERTY_IDS.add(ALERT_TARGET_STATES);
      PROPERTY_IDS.add(ALERT_TARGET_GLOBAL);
      PROPERTY_IDS.add(ALERT_TARGET_ENABLED);
  
      // keys
      KEY_PROPERTY_IDS.put(Resource.Type.AlertTarget, ALERT_TARGET_ID);
    }
  
    /**
     * Target DAO
     */
    @Inject
    private static AlertDispatchDAO s_dao;
  
    @Inject
    private static DispatchFactory dispatchFactory;
  
    /**
     * Used for serialization and deserialization of some fields.
     */
    private static final Gson s_gson = new Gson();
  
    /**
     * Constructor.
     */
    @Inject
    AlertTargetResourceProvider() {
      super(Resource.Type.AlertTarget, PROPERTY_IDS, KEY_PROPERTY_IDS);
  
      EnumSet<RoleAuthorization> requiredAuthorizations = EnumSet.of(RoleAuthorization.CLUSTER_MANAGE_ALERT_NOTIFICATIONS);
      setRequiredCreateAuthorizations(requiredAuthorizations);
      setRequiredUpdateAuthorizations(requiredAuthorizations);
      setRequiredDeleteAuthorizations(requiredAuthorizations);
    }
  
    @Override
    protected RequestStatus createResourcesAuthorized(final Request request)
        throws SystemException,
        UnsupportedPropertyException, ResourceAlreadyExistsException,
        NoSuchParentResourceException {
  
      createResources(new Command<Void>() {
        @Override
        public Void invoke() throws AmbariException {
          createAlertTargets(request.getProperties(), request.getRequestInfoProperties());
          return null;
        }
      });
  
      notifyCreate(Resource.Type.AlertTarget, request);
      return getRequestStatus(null);
    }
  
    @Override
    public Set<Resource> getResources(Request request, Predicate predicate)
        throws SystemException, UnsupportedPropertyException,
        NoSuchResourceException, NoSuchParentResourceException {
  
      Set<Resource> results = new HashSet<>();
      Set<String> requestPropertyIds = getRequestPropertyIds(request, predicate);
  
      if( null == predicate ){
        List<AlertTargetEntity> entities = s_dao.findAllTargets();
        for (AlertTargetEntity entity : entities) {
          results.add(toResource(entity, requestPropertyIds));
        }
      } else {
        for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) {
          String id = (String) propertyMap.get(ALERT_TARGET_ID);
          if (null == id) {
            continue;
          }
  
          AlertTargetEntity entity = s_dao.findTargetById(Long.parseLong(id));
          if (null != entity) {
            results.add(toResource(entity, requestPropertyIds));
          }
        }
      }
  
      return results;
    }
  
    @Override
    protected RequestStatus updateResourcesAuthorized(final Request request,
        Predicate predicate)
        throws SystemException, UnsupportedPropertyException,
        NoSuchResourceException, NoSuchParentResourceException {
  
      modifyResources(new Command<Void>() {
        @Override
        public Void invoke() throws AmbariException {
          Set<Map<String, Object>> requestMaps = request.getProperties();
          for (Map<String, Object> requestMap : requestMaps) {
            String stringId = (String) requestMap.get(ALERT_TARGET_ID);
  
            if (StringUtils.isEmpty(stringId)) {
              throw new IllegalArgumentException(
                  "The ID of the alert target is required when updating an existing target");
            }
  
            long alertTargetId = Long.parseLong(stringId);
            updateAlertTargets(alertTargetId, requestMap);
          }
          return null;
        }
      });
  
      notifyUpdate(Resource.Type.AlertTarget, request, predicate);
      return getRequestStatus(null);
    }
  
    @Override
    protected RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate)
        throws SystemException, UnsupportedPropertyException,
        NoSuchResourceException, NoSuchParentResourceException {
  
      Set<Resource> resources = getResources(new RequestImpl(null, null, null,
          null), predicate);
  
      Set<Long> targetIds = new HashSet<>();
  
      for (final Resource resource : resources) {
        Long id = (Long) resource.getPropertyValue(ALERT_TARGET_ID);
        targetIds.add(id);
      }
  
      for (Long targetId : targetIds) {
        LOG.info("Deleting alert target {}", targetId);
  
        final AlertTargetEntity entity = s_dao.findTargetById(targetId.longValue());
  
        modifyResources(new Command<Void>() {
          @Override
          public Void invoke() throws AmbariException {
            s_dao.remove(entity);
            return null;
          }
        });
      }
  
      notifyDelete(Resource.Type.AlertTarget, predicate);
      return getRequestStatus(null);
    }
  
    @Override
    protected Set<String> getPKPropertyIds() {
      return PK_PROPERTY_IDS;
    }
  
    /**
     * Create and persist {@link AlertTargetEntity} from the map of properties.
     *
     * @param requestMaps
     * @param requestInfoProps
     * @throws AmbariException
     */
    @SuppressWarnings("unchecked")
    private void createAlertTargets(Set<Map<String, Object>> requestMaps, Map<String, String> requestInfoProps)
        throws AmbariException {
      for (Map<String, Object> requestMap : requestMaps) {
        String name = StringEscapeUtils.escapeHtml4((String) requestMap.get(ALERT_TARGET_NAME));
        String description = (String) requestMap.get(ALERT_TARGET_DESCRIPTION);
        String notificationType = (String) requestMap.get(ALERT_TARGET_NOTIFICATION_TYPE);
        Collection<String> alertStates = (Collection<String>) requestMap.get(ALERT_TARGET_STATES);
        String globalProperty = (String) requestMap.get(ALERT_TARGET_GLOBAL);
        String enabledProperty = (String) requestMap.get(ALERT_TARGET_ENABLED);
  
        if (StringUtils.isEmpty(name)) {
          throw new IllegalArgumentException(
              "The name of the alert target is required.");
        }
  
        if (StringUtils.isEmpty(notificationType)) {
          throw new IllegalArgumentException(
              "The type of the alert target is required.");
        }
  
        Map<String, Object> properties = extractProperties(requestMap);
  
        String propertiesJson = s_gson.toJson(properties);
        if (StringUtils.isEmpty(propertiesJson)) {
          throw new IllegalArgumentException(
              "Alert targets must be created with their connection properties");
        }
  
        String validationDirective = requestInfoProps.get(AlertTargetResourceDefinition.VALIDATE_CONFIG_DIRECTIVE);
        if (validationDirective != null
            && validationDirective.equalsIgnoreCase("true")) {
          validateTargetConfig(notificationType, properties);
        }
  
        boolean overwriteExisting = false;
        if (requestInfoProps.containsKey(AlertTargetResourceDefinition.OVERWRITE_DIRECTIVE)) {
          overwriteExisting = true;
        }
  
        // global not required
        boolean isGlobal = false;
        if (null != globalProperty) {
          isGlobal = Boolean.parseBoolean(globalProperty);
        }
  
        // enabled not required
        boolean isEnabled = true;
        if (null != enabledProperty) {
          isEnabled = Boolean.parseBoolean(enabledProperty);
        }
  
        // set the states that this alert target cares about
        final Set<AlertState> alertStateSet;
        if (null != alertStates) {
          alertStateSet = new HashSet<>(alertStates.size());
          for (String state : alertStates) {
            alertStateSet.add(AlertState.valueOf(state));
          }
        } else {
          alertStateSet = EnumSet.allOf(AlertState.class);
        }
  
        // if we are overwriting an existing, determine if one exists first
        AlertTargetEntity entity = s_dao.findTargetByName(name);
  
        if (null == entity) {
          entity = new AlertTargetEntity();
        } else {
          if( !overwriteExisting ) {
            throw new IllegalArgumentException(
                "Alert targets already exists and can't be created");
          }
        }
  
        // groups are not required on creation
        if (requestMap.containsKey(ALERT_TARGET_GROUPS)) {
          Collection<Long> groupIds = (Collection<Long>) requestMap.get(ALERT_TARGET_GROUPS);
          if( !groupIds.isEmpty() ){
            Set<AlertGroupEntity> groups = new HashSet<>();
            List<Long> ids = new ArrayList<>(groupIds);
            groups.addAll(s_dao.findGroupsById(ids));
            entity.setAlertGroups(groups);
          }
        }
  
        entity.setDescription(description);
        entity.setNotificationType(notificationType);
        entity.setProperties(propertiesJson);
        entity.setTargetName(name);
        entity.setAlertStates(alertStateSet);
        entity.setGlobal(isGlobal);
        entity.setEnabled(isEnabled);
  
        if (null == entity.getTargetId() || 0 == entity.getTargetId()) {
          s_dao.create(entity);
        } else {
          s_dao.merge(entity);
        }
      }
    }
  
    /**
     * Updates existing {@link AlertTargetEntity}s with the specified properties.
     *
     * @param requestMap
     *          a set of property maps, one map for each entity.
     * @throws AmbariException
     *           if the entity could not be found.
     */
    @Transactional
    @SuppressWarnings("unchecked")
    void updateAlertTargets(long alertTargetId,
        Map<String, Object> requestMap)
        throws AmbariException {
  
      AlertTargetEntity entity = s_dao.findTargetById(alertTargetId);
  
      if (null == entity) {
        String message = MessageFormat.format(
            "The alert target with ID {0} could not be found", alertTargetId);
  
        throw new AmbariException(message);
      }
  
      String name = (String) requestMap.get(ALERT_TARGET_NAME);
      String description = (String) requestMap.get(ALERT_TARGET_DESCRIPTION);
      String notificationType = (String) requestMap.get(ALERT_TARGET_NOTIFICATION_TYPE);
      Collection<String> alertStates = (Collection<String>) requestMap.get(ALERT_TARGET_STATES);
      Collection<Long> groupIds = (Collection<Long>) requestMap.get(ALERT_TARGET_GROUPS);
      String isGlobal = (String) requestMap.get(ALERT_TARGET_GLOBAL);
      String isEnabled = (String) requestMap.get(ALERT_TARGET_ENABLED);
  
      if(null != isGlobal){
        entity.setGlobal(Boolean.parseBoolean(isGlobal));
      }
  
      if (null != isEnabled) {
        entity.setEnabled(Boolean.parseBoolean(isEnabled));
      }
  
      if (!StringUtils.isBlank(name)) {
        entity.setTargetName(name);
      }
  
      if (null != description) {
        entity.setDescription(description);
      }
  
      if (!StringUtils.isBlank(notificationType)) {
        entity.setNotificationType(notificationType);
      }
  
      Map<String, Object> propertiesMap = extractProperties(requestMap);
  
      if (propertiesMap != null) {
        String properties = s_gson.toJson(propertiesMap);
        if (!StringUtils.isEmpty(properties)) {
          LOG.debug("Updating Alert Target properties map to: " + properties);
          entity.setProperties(properties);
        }
      }
  
      // a null alert state implies that the key was not set and no update
      // should occur for this field, while an empty list implies all alert
      // states should be set
      if (null != alertStates) {
        final Set<AlertState> alertStateSet;
        if (alertStates.isEmpty()) {
          alertStateSet = EnumSet.allOf(AlertState.class);
        } else {
          alertStateSet = new HashSet<>(alertStates.size());
          for (String state : alertStates) {
            alertStateSet.add(AlertState.valueOf(state));
          }
        }
  
        entity.setAlertStates(alertStateSet);
      }
  
      // if groups were supplied, replace existing
      if (null != groupIds) {
        Set<AlertGroupEntity> groups = new HashSet<>();
        List<Long> ids = new ArrayList<>(groupIds);
  
        if (ids.size() > 0) {
          groups.addAll(s_dao.findGroupsById(ids));
        }
  
        entity.setAlertGroups(groups);
      } else if (entity.isGlobal()){
        Set<AlertGroupEntity> groups = new HashSet<>(s_dao.findAllGroups());
        entity.setAlertGroups(groups);
      }
  
      // merge the entity, cascading the merge to other entities, like groups
      s_dao.merge(entity);
    }
  
    /**
     * Convert the given {@link AlertTargetEntity} to a {@link Resource}.
     *
     * @param entity
     *          the entity to convert.
     * @param requestedIds
     *          the properties that were requested or {@code null} for all.
     * @return the resource representation of the entity (never {@code null}).
     */
    private Resource toResource(AlertTargetEntity entity, Set<String> requestedIds) {
  
      Resource resource = new ResourceImpl(Resource.Type.AlertTarget);
      resource.setProperty(ALERT_TARGET_ID, entity.getTargetId());
      resource.setProperty(ALERT_TARGET_NAME, entity.getTargetName());
  
      resource.setProperty(ALERT_TARGET_DESCRIPTION, entity.getDescription());
      resource.setProperty(ALERT_TARGET_NOTIFICATION_TYPE,
          entity.getNotificationType());
  
      resource.setProperty(ALERT_TARGET_ENABLED, entity.isEnabled());
  
      // these are expensive to deserialize; only do it if asked for
      if (requestedIds.contains(ALERT_TARGET_PROPERTIES)) {
        String properties = entity.getProperties();
        Map<String, String> map = s_gson.<Map<String, String>> fromJson(
            properties,
            Map.class);
  
        resource.setProperty(ALERT_TARGET_PROPERTIES, map);
      }
  
      setResourceProperty(resource, ALERT_TARGET_STATES, entity.getAlertStates(),
          requestedIds);
  
      setResourceProperty(resource, ALERT_TARGET_GLOBAL, entity.isGlobal(),
          requestedIds);
  
      if (BaseProvider.isPropertyRequested(ALERT_TARGET_GROUPS, requestedIds)) {
        Set<AlertGroupEntity> groupEntities = entity.getAlertGroups();
        List<AlertGroup> groups = new ArrayList<>(
          groupEntities.size());
  
        for (AlertGroupEntity groupEntity : groupEntities) {
          AlertGroup group = new AlertGroup();
          group.setId(groupEntity.getGroupId());
          group.setName(groupEntity.getGroupName());
          group.setClusterName(groupEntity.getClusterId());
          group.setDefault(groupEntity.isDefault());
          groups.add(group);
        }
  
        resource.setProperty(ALERT_TARGET_GROUPS, groups);
      }
  
      return resource;
    }
  
    /**
     * Looks through the flat list of propery keys in the supplied map and builds
     * a JSON string of key:value pairs for the {@link #ALERT_TARGET_PROPERTIES}
     * key.
     *
     * @param requestMap
     *          the map of flattened properties (not {@code null}).
     * @return the JSON representing the key/value pairs of all properties, or
     *         {@code null} if none.
     */
    private Map<String, Object> extractProperties(Map<String, Object> requestMap) {
      Map<String, Object> normalizedMap = new HashMap<>(
        requestMap.size());
      boolean has_properties = false;
  
      for (Entry<String, Object> entry : requestMap.entrySet()) {
        String key = entry.getKey();
        String propCat = PropertyHelper.getPropertyCategory(key);
  
        if (propCat.equals(ALERT_TARGET_PROPERTIES)) {
          has_properties = true;
          String propKey = PropertyHelper.getPropertyName(key);
          normalizedMap.put(propKey, entry.getValue());
        }
      }
  
      if (!has_properties) {
        normalizedMap = null;
      }
  
      return normalizedMap;
    }
  
    /**
     * Finds dispatcher for given notification type and validates on it given alert target configuration properties.
     * @param notificationType type of dispatcher
     * @param properties alert target configuration properties
     */
    private void validateTargetConfig(String notificationType, Map<String, Object> properties) {
      NotificationDispatcher dispatcher = dispatchFactory.getDispatcher(notificationType);
      if (dispatcher == null) {
        throw new IllegalArgumentException("Dispatcher for given notification type doesn't exist");
      }
      TargetConfigurationResult validationResult = dispatcher.validateTargetConfig(properties);
      if (validationResult.getStatus() == TargetConfigurationResult.Status.INVALID) {
        throw new IllegalArgumentException(validationResult.getMessage());
      }
    }
  }
  