/**
* 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.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.DuplicateResourceException;
import org.apache.ambari.server.ObjectNotFoundException;
import org.apache.ambari.server.ParentObjectNotFoundException;
import org.apache.ambari.server.StaticallyInject;
import org.apache.ambari.server.controller.AmbariManagementController;
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.orm.dao.ArtifactDAO;
import org.apache.ambari.server.orm.entities.ArtifactEntity;
import org.apache.ambari.server.state.Cluster;
import com.google.gson.Gson;
import com.google.inject.Inject;
/**
* Provider for cluster artifacts.
* Artifacts contain an artifact name as the PK and artifact data in the form of
* a map which is the content of the artifact.
* <p>
* An example of an artifact is a kerberos descriptor.
*/
//todo: implement ExtendedResourceProvider???
@StaticallyInject
public class ArtifactResourceProvider extends AbstractResourceProvider {
/**
* artifact name
*/
public static final String ARTIFACT_NAME_PROPERTY =
PropertyHelper.getPropertyId("Artifacts", "artifact_name");
/**
* artifact data
*/
public static final String ARTIFACT_DATA_PROPERTY = "artifact_data";
/**
* primary key fields
*/
private static Set<String> pkPropertyIds = new HashSet<>();
/**
* map of resource type to fk field
*/
private static Map<Resource.Type, String> keyPropertyIds =
new HashMap<>();
/**
* resource properties
*/
private static Set<String> propertyIds = new HashSet<>();
/**
* map of resource type to type registration
*/
private static final Map<Resource.Type, TypeRegistration> typeRegistrations =
new HashMap<>();
/**
* map of foreign key field to type registration
*/
private static final Map<String, TypeRegistration> typeRegistrationsByFK =
new HashMap<>();
/**
* map of short foreign key field to type registration
*/
private static final Map<String, TypeRegistration> typeRegistrationsByShortFK =
new HashMap<>();
/**
* serializer used to convert json to map
*/
private static final Gson jsonSerializer = new Gson();
/**
* artifact data access object
*/
@Inject
private static ArtifactDAO artifactDAO;
/**
* set resource properties, pk and fk's
*/
static {
// resource properties
propertyIds.add(ARTIFACT_NAME_PROPERTY);
propertyIds.add(ARTIFACT_DATA_PROPERTY);
// pk property
pkPropertyIds.add(ARTIFACT_NAME_PROPERTY);
// key properties
keyPropertyIds.put(Resource.Type.Artifact, ARTIFACT_NAME_PROPERTY);
//todo: external registration
// cluster registration
ClusterTypeRegistration clusterTypeRegistration = new ClusterTypeRegistration();
typeRegistrations.put(clusterTypeRegistration.getType(), clusterTypeRegistration);
//service registration
ServiceTypeRegistration serviceTypeRegistration = new ServiceTypeRegistration();
typeRegistrations.put(serviceTypeRegistration.getType(), serviceTypeRegistration);
//todo: detect resource type and fk name collisions during registration
for (TypeRegistration registration: typeRegistrations.values()) {
String fkProperty = registration.getFKPropertyName();
keyPropertyIds.put(registration.getType(), fkProperty);
propertyIds.add(fkProperty);
typeRegistrationsByFK.put(fkProperty, registration);
typeRegistrationsByShortFK.put(registration.getShortFKPropertyName(), registration);
for (Map.Entry<Resource.Type, String> ancestor : registration.getForeignKeyInfo().entrySet()) {
Resource.Type ancestorType = ancestor.getKey();
if (! keyPropertyIds.containsKey(ancestorType)) {
String ancestorFK = ancestor.getValue();
keyPropertyIds.put(ancestorType, ancestorFK);
propertyIds.add(ancestorFK);
}
}
}
}
/**
* Constructor.
*
* @param controller management controller
*/
@Inject
protected ArtifactResourceProvider(AmbariManagementController controller) {
super(propertyIds, keyPropertyIds);
for (TypeRegistration typeRegistration : typeRegistrations.values()) {
typeRegistration.setManagementController(controller);
}
}
@Override
protected Set<String> getPKPropertyIds() {
return pkPropertyIds;
}
@Override
public RequestStatus createResources(Request request)
throws SystemException,
UnsupportedPropertyException,
ResourceAlreadyExistsException,
NoSuchParentResourceException {
for (Map<String, Object> properties : request.getProperties()) {
createResources(getCreateCommand(properties, request.getRequestInfoProperties()));
}
notifyCreate(Resource.Type.Artifact, request);
return getRequestStatus(null);
}
@Override
public Set<Resource> getResources(Request request, Predicate predicate)
throws SystemException,
UnsupportedPropertyException,
NoSuchResourceException,
NoSuchParentResourceException {
Set<Map<String, Object>> requestProps = getPropertyMaps(predicate);
Set<Resource> resources = new LinkedHashSet<>();
for (Map<String, Object> props : requestProps) {
resources.addAll(getResources(getGetCommand(request, predicate, props)));
}
if (resources.isEmpty() && isInstanceRequest(requestProps)) {
throw new NoSuchResourceException(
"The requested resource doesn't exist: Artifact not found, " + predicate);
}
return resources;
}
@Override
public RequestStatus updateResources(final Request request, Predicate predicate)
throws SystemException,
UnsupportedPropertyException,
NoSuchResourceException,
NoSuchParentResourceException {
for (Resource resource : getResources(request, predicate)) {
modifyResources(getUpdateCommand(request, resource));
}
notifyUpdate(Resource.Type.Artifact, request, predicate);
return getRequestStatus(null);
}
@Override
public RequestStatus deleteResources(Request request, Predicate predicate)
throws SystemException,
UnsupportedPropertyException,
NoSuchResourceException,
NoSuchParentResourceException {
// get resources to update
Set<Resource> setResources = getResources(
new RequestImpl(null, null, null, null), predicate);
for (final Resource resource : setResources) {
modifyResources(getDeleteCommand(resource));
}
notifyDelete(Resource.Type.Artifact, predicate);
return getRequestStatus(null);
}
/**
* Create a command to create a resource.
*
* @param properties request properties
* @param requestInfoProps request info properties
*
* @return a new create command
*/
private Command<Void> getCreateCommand(final Map<String, Object> properties,
final Map<String, String> requestInfoProps) {
return new Command<Void>() {
@Override
public Void invoke() throws AmbariException {
// ensure that parent exists
validateParent(properties);
String artifactName = String.valueOf(properties.get(ARTIFACT_NAME_PROPERTY));
TreeMap<String, String> foreignKeyMap = createForeignKeyMap(properties);
if (artifactDAO.findByNameAndForeignKeys(artifactName, foreignKeyMap) != null) {
throw new DuplicateResourceException(String.format(
"Attempted to create an artifact which already exists, artifact_name='%s', foreign_keys='%s'",
artifactName, getRequestForeignKeys(properties)));
}
LOG.debug("Creating Artifact Resource with name '{}'. Parent information: {}",
artifactName, getRequestForeignKeys(properties));
artifactDAO.create(toEntity(properties, requestInfoProps.get(Request.REQUEST_INFO_BODY_PROPERTY)));
return null;
}
};
}
/**
* Create a command to get the requested resources.
*
* @param properties request properties
*
* @return a new get command
*/
private Command<Set<Resource>> getGetCommand(final Request request,
final Predicate predicate,
final Map<String, Object> properties) {
return new Command<Set<Resource>>() {
@Override
public Set<Resource> invoke() throws AmbariException {
String name = (String) properties.get(ARTIFACT_NAME_PROPERTY);
validateParent(properties);
Set<Resource> matchingResources = new HashSet<>();
TreeMap<String, String> foreignKeys = createForeignKeyMap(properties);
Set<String> requestPropertyIds = getRequestPropertyIds(request, predicate);
if (name != null) {
// find instance using name and foreign keys
ArtifactEntity entity = artifactDAO.findByNameAndForeignKeys(name, foreignKeys);
if (entity != null) {
Resource instance = (toResource(entity, requestPropertyIds));
if (predicate.evaluate(instance)) {
matchingResources.add(instance);
}
}
} else {
// find collection using foreign keys only
List<ArtifactEntity> results = artifactDAO.findByForeignKeys(foreignKeys);
for (ArtifactEntity entity : results) {
Resource resource = toResource(entity, requestPropertyIds);
if (predicate.evaluate(resource)) {
matchingResources.add(resource);
}
}
}
return matchingResources;
}
};
}
/**
* Create a command to update a resource.
*
* @param request update request
* @param resource resource to update
*
* @return a update resource command
*/
private Command<Void> getUpdateCommand(final Request request, final Resource resource) {
return new Command<Void>() {
@Override
public Void invoke() throws AmbariException {
Map<String, Object> entityUpdateProperties =
new HashMap<>(request.getProperties().iterator().next());
// ensure name is set. It won't be in case of query
entityUpdateProperties.put(ARTIFACT_NAME_PROPERTY,
String.valueOf(resource.getPropertyValue(ARTIFACT_NAME_PROPERTY)));
artifactDAO.merge(toEntity(entityUpdateProperties,
request.getRequestInfoProperties().get(Request.REQUEST_INFO_BODY_PROPERTY)));
return null;
}
};
}
/**
* Create a command to delete a resource.
*
* @param resource the resource to delete
*
* @return a delete resource command
*/
private Command<Void> getDeleteCommand(final Resource resource) {
return new Command<Void>() {
@Override
public Void invoke() throws AmbariException {
// flatten out key properties as is expected by createForeignKeyMap()
Map<String, Object> keyProperties = new HashMap<>();
for (Map.Entry<String, Object> entry : resource.getPropertiesMap().get("Artifacts").entrySet()) {
keyProperties.put(String.format("Artifacts/%s", entry.getKey()), entry.getValue());
}
// find entity to remove
String artifactName = String.valueOf(resource.getPropertyValue(ARTIFACT_NAME_PROPERTY));
TreeMap<String, String> artifactForeignKeys = createForeignKeyMap(keyProperties);
ArtifactEntity entity = artifactDAO.findByNameAndForeignKeys(artifactName, artifactForeignKeys);
if (entity != null) {
LOG.info("Deleting Artifact: name = {}, foreign keys = {}",
entity.getArtifactName(), entity.getForeignKeys());
artifactDAO.remove(entity);
} else {
LOG.info("Cannot find Artifact to delete, ignoring: name = {}, foreign keys = {}",
artifactName, artifactForeignKeys);
}
return null;
}
};
}
/**
* Validate that parent resources exist.
*
* @param properties request properties
*
* @throws ParentObjectNotFoundException if the parent resource doesn't exist
* @throws AmbariException if an error occurred while attempting to validate the parent
*/
private void validateParent(Map<String, Object> properties) throws AmbariException {
Resource.Type parentType = getRequestType(properties);
if (! typeRegistrations.get(parentType).instanceExists(keyPropertyIds, properties)) {
throw new ParentObjectNotFoundException(String.format(
"Parent resource doesn't exist: %s", getRequestForeignKeys(properties)));
}
}
/**
* Get the type of the parent resource from the request properties.
*
* @param properties request properties
*
* @return the parent resource type based on the request properties
*
* @throws AmbariException if unable to determine the parent resource type
*/
private Resource.Type getRequestType(Map<String, Object> properties) throws AmbariException {
Set<String> requestFKs = getRequestForeignKeys(properties).keySet();
for (TypeRegistration registration : typeRegistrations.values()) {
Collection<String> typeFKs = new HashSet<>(registration.getForeignKeyInfo().values());
typeFKs.add(registration.getFKPropertyName());
if (requestFKs.equals(typeFKs)) {
return registration.getType();
}
}
throw new AmbariException("Couldn't determine resource type based on request properties");
}
/**
* Get a map of foreign key to value for the given request properties.
* The foreign key map will only include the foreign key properties which
* are included in the request properties. This is useful for reporting
* errors back to the user.
* .
* @param properties request properties
*
* @return map of foreign key to value for the provided request properties
*/
private Map<String, String> getRequestForeignKeys(Map<String, Object> properties) {
Map<String, String> requestFKs = new HashMap<>();
for (String property : properties.keySet()) {
if (! property.equals(ARTIFACT_NAME_PROPERTY) && ! property.startsWith(ARTIFACT_DATA_PROPERTY)) {
requestFKs.put(property, String.valueOf(properties.get(property)));
}
}
return requestFKs;
}
/**
* Convert a map of properties to an artifact entity.
*
* @param properties property map
*
* @return new artifact entity
*/
@SuppressWarnings("unchecked")
private ArtifactEntity toEntity(Map<String, Object> properties, String rawRequestBody)
throws AmbariException {
String name = (String) properties.get(ARTIFACT_NAME_PROPERTY);
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Artifact name must be provided");
}
ArtifactEntity artifact = new ArtifactEntity();
artifact.setArtifactName(name);
artifact.setForeignKeys(createForeignKeyMap(properties));
Map<String, Object> rawBodyMap = jsonSerializer.<Map<String, Object>>fromJson(
rawRequestBody, Map.class);
Object artifactData = rawBodyMap.get(ARTIFACT_DATA_PROPERTY);
if (artifactData == null) {
throw new IllegalArgumentException("artifact_data property must be provided");
}
if (! (artifactData instanceof Map)) {
throw new IllegalArgumentException("artifact_data property must be a map");
}
artifact.setArtifactData((Map<String, Object>) artifactData);
return artifact;
}
/**
* Create a map of foreign keys and values which can be persisted.
* This map will include the short fk names of the key properties as well
* as the 'persist id' representation of the value which is returned
* by the type registration.
*
* @param properties request properties
* @return an ordered map of key name to value
*
* @throws AmbariException an unexpected exception occurred
*/
private TreeMap<String, String> createForeignKeyMap(Map<String, Object> properties) throws AmbariException {
TreeMap<String, String> foreignKeys = new TreeMap<>();
for (String keyProperty : keyPropertyIds.values()) {
if (! keyProperty.equals(ARTIFACT_NAME_PROPERTY)) {
String origValue = (String) properties.get(keyProperty);
if (origValue != null && ! origValue.isEmpty()) {
TypeRegistration typeRegistration = typeRegistrationsByFK.get(keyProperty);
foreignKeys.put(typeRegistration.getShortFKPropertyName(), typeRegistration.toPersistId(origValue));
}
}
}
return foreignKeys;
}
/**
* Create a resource instance from an artifact entity.
* This will convert short fk property names to the full property name as well
* as converting the value from the 'persist id' representation which is written
* to the database.
*
* @param entity artifact entity
* @param requestedIds requested id's
*
* @return a new resource instance for the given artifact entity
*/
private Resource toResource(ArtifactEntity entity, Set<String> requestedIds) throws AmbariException {
Resource resource = new ResourceImpl(Resource.Type.Artifact);
setResourceProperty(resource, ARTIFACT_NAME_PROPERTY, entity.getArtifactName(), requestedIds);
setResourceProperty(resource, ARTIFACT_DATA_PROPERTY, entity.getArtifactData(), requestedIds);
for (Map.Entry<String, String> entry : entity.getForeignKeys().entrySet()) {
TypeRegistration typeRegistration = typeRegistrationsByShortFK.get(entry.getKey());
setResourceProperty(resource, typeRegistration.getFKPropertyName(),
typeRegistration.fromPersistId(entry.getValue()), requestedIds);
}
return resource;
}
/**
* Determine if the request was for an instance resource.
*
* @param requestProps request properties
*
* @return true if the request was for a specific instance, false otherwise
*/
private boolean isInstanceRequest(Set<Map<String, Object>> requestProps) {
return requestProps.size() == 1 &&
requestProps.iterator().next().get(ARTIFACT_NAME_PROPERTY) != null;
}
//todo: when static registration is changed to external registration, this interface
//todo: should be extracted as a first class interface.
/**
* Used to register a dynamic sub-resource with an existing resource type.
*/
public interface TypeRegistration {
/**
* Allows the management controller to be set on the registration.
* This is called as part of the registration process.
* For registrations that need access to the management controller,
* they should assign this controller to a member field.
*
* @param controller management controller
*/
void setManagementController(AmbariManagementController controller);
/**
* Get the type of the registering resource.
*
* @return type of the register resource
*/
Resource.Type getType();
/**
* Full foreign key property name to use in the artifact resource.
* At this time, all foreign key properties should be in the "Artifacts" category.
*
* @return the absolute foreign key property name.
* For example: "Artifacts/cluster_name
*/
//todo: use relative property names
String getFKPropertyName();
/**
* Shortened foreign key name that is written to the database.
* This name doesn't need to be in any category but must be unique
* across all registrations.
*
* @return short fk name. For example: "cluster_name"
*/
String getShortFKPropertyName();
/**
* Convert the foreign key value to a value that is persisted to the database.
* In most cases this will be the original value.
* <p>
* An example of when this will be different is when the fk value value needs
* to be converted to the unique id for the resource.
* <p>
* For example, the cluster_name to the cluster_id.
* <p>
* This returned value will later be converted back to the normal form via
* {@link #fromPersistId(String)}.
*
* @param value normal form of the fk value used by the api
*
* @return persist id form of the fk value
*
* @throws AmbariException if unable to convert the value
*/
String toPersistId(String value) throws AmbariException;
/**
* Convert the persist id form of the foreign key which is written to the database
* to the form used by the api. In most cases, this will be the same.
* <p>
* This method takes the value returned from {@link #toPersistId(String)} and converts
* it back to the original value which is used by the api.
* <p>
* An example of this is the converting the cluster name to the cluster id in
* {@link #toPersistId(String)} and then back to the cluster name by this method. The
* api always uses the cluster name so we wouldn't want to return the id back as the
* value for a cluster_name foreign key.
*
* @param value persist id form of the fk value
*
* @return normal form of the fk value used by the api
*
* @throws AmbariException if unable to convert the value
*/
String fromPersistId(String value) throws AmbariException;
/**
* Get a map of ancestor type to foreign key.
* <p>
* <b>Note: Currently, if a parent resource has also registered the same dynamic resource,
* the foreign key name used here has to match the value returned by the parent resource
* in {@link #getFKPropertyName()}</b>
*
* @return map of ancestor type to foreign key
*/
//todo: look at the need to use the same name as specified by ancestors
Map<Resource.Type, String> getForeignKeyInfo();
/**
* Determine if the instance identified by the provided properties exists.
*
* @param keyMap map of resource type to foreign key properties
* @param properties request properties
*
* @return true if the resource instance exists, false otherwise
*
* @throws AmbariException an exception occurs trying to determine if the instance exists
*/
boolean instanceExists(Map<Resource.Type, String> keyMap,
Map<String, Object> properties) throws AmbariException;
}
//todo: Registration should be done externally and these implementations should be moved
//todo: to a location where the registering resource definition has access to them.
/**
* Cluster resource registration.
*/
private static class ClusterTypeRegistration implements TypeRegistration {
/**
* management controller instance
*/
private AmbariManagementController controller = null;
/**
* cluster name property name
*/
private static final String CLUSTER_NAME = PropertyHelper.getPropertyId("Artifacts", "cluster_name");
@Override
public void setManagementController(AmbariManagementController controller) {
this.controller = controller;
}
@Override
public Resource.Type getType() {
return Resource.Type.Cluster;
}
@Override
public String getFKPropertyName() {
return CLUSTER_NAME;
}
@Override
public String getShortFKPropertyName() {
return "cluster";
}
@Override
public String toPersistId(String value) throws AmbariException {
return String.valueOf(controller.getClusters().getCluster(value).getClusterId());
}
@Override
public String fromPersistId(String value) throws AmbariException {
return controller.getClusters().getClusterById(Long.valueOf(value)).getClusterName();
}
@Override
public Map<Resource.Type, String> getForeignKeyInfo() {
return Collections.emptyMap();
}
@Override
public boolean instanceExists(Map<Resource.Type, String> keyMap,
Map<String, Object> properties) throws AmbariException {
try {
String clusterName = String.valueOf(properties.get(CLUSTER_NAME));
controller.getClusters().getCluster(clusterName);
return true;
} catch (ObjectNotFoundException e) {
// doesn't exist
}
return false;
}
}
/**
* Service resource registration.
*/
private static class ServiceTypeRegistration implements TypeRegistration {
/**
* management controller instance
*/
private AmbariManagementController controller = null;
/**
* service name property name
*/
private static final String SERVICE_NAME = PropertyHelper.getPropertyId("Artifacts", "service_name");
@Override
public void setManagementController(AmbariManagementController controller) {
this.controller = controller;
}
@Override
public Resource.Type getType() {
return Resource.Type.Service;
}
@Override
public String getFKPropertyName() {
return SERVICE_NAME;
}
@Override
public String getShortFKPropertyName() {
return "service";
}
@Override
public String toPersistId(String value) {
return value;
}
@Override
public String fromPersistId(String value) {
return value;
}
@Override
public Map<Resource.Type, String> getForeignKeyInfo() {
return Collections.singletonMap(Resource.Type.Cluster, "Artifacts/cluster_name");
}
@Override
public boolean instanceExists(Map<Resource.Type, String> keyMap,
Map<String, Object> properties) throws AmbariException {
String clusterName = String.valueOf(properties.get(keyMap.get(Resource.Type.Cluster)));
try {
Cluster cluster = controller.getClusters().getCluster(clusterName);
cluster.getService(String.valueOf(properties.get(SERVICE_NAME)));
return true;
} catch (ObjectNotFoundException e) {
// doesn't exist
}
return false;
}
}
}