/*
* Copyright 2011-2015 the original author or authors.
*
* Licensed 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.springframework.xd.dirt.stream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.util.Assert;
import org.springframework.xd.dirt.core.BaseDefinition;
import org.springframework.xd.dirt.core.DeploymentUnitStatus;
import org.springframework.xd.dirt.core.ResourceDeployer;
import org.springframework.xd.dirt.job.dsl.ComposedJobUtil;
import org.springframework.xd.dirt.job.dsl.JobParser;
import org.springframework.xd.dirt.zookeeper.Paths;
import org.springframework.xd.dirt.zookeeper.ZooKeeperConnection;
import org.springframework.xd.dirt.zookeeper.ZooKeeperUtils;
import org.springframework.xd.module.ModuleDefinition;
import org.springframework.xd.module.ModuleDescriptor;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.rest.domain.support.DeploymentPropertiesFormat;
/**
* Abstract implementation of the @link {@link org.springframework.xd.dirt.core.ResourceDeployer} interface. It provides
* the basic support for calling CrudRepository methods and sending deployment messages.
*
* @author Luke Taylor
* @author Mark Pollack
* @author Eric Bottard
* @author Andy Clement
* @author David Turanski
*/
public abstract class AbstractDeployer<D extends BaseDefinition> implements ResourceDeployer<D>, DeploymentValidator {
private static final Logger logger = LoggerFactory.getLogger(AbstractDeployer.class);
/**
* Pattern used for parsing a single deployment property key. Group 1 is the module name, Group 2 is the
* deployment property name.
*/
private static final Pattern DEPLOYMENT_PROPERTY_PATTERN = Pattern.compile("module\\.([^\\.]+)\\.([^=]+)");
private final PagingAndSortingRepository<D, String> repository;
private final ZooKeeperConnection zkConnection;
protected final XDParser parser;
protected final JobParser composedJobParser;
/**
* Used in exception messages as well as indication to the parser.
*/
protected final ParsingContext definitionKind;
protected AbstractDeployer(ZooKeeperConnection zkConnection, PagingAndSortingRepository<D, String> repository,
XDParser parser, ParsingContext parsingContext) {
Assert.notNull(zkConnection, "ZooKeeper connection cannot be null");
Assert.notNull(repository, "Repository cannot be null");
Assert.notNull(parsingContext, "Entity type kind cannot be null");
this.zkConnection = zkConnection;
this.repository = repository;
this.definitionKind = parsingContext;
this.parser = parser;
this.composedJobParser = new JobParser();
}
@Override
public D save(D definition) {
Assert.notNull(definition, "Definition may not be null");
String name = definition.getName();
String def = definition.getDefinition();
validateBeforeSave(name, def);
if(!ComposedJobUtil.isComposedJobDefinition(def)){
List<ModuleDescriptor> moduleDescriptors =
parser.parse(name, def, definitionKind);
// todo: the result of parse() should already have correct (polymorphic) definitions
List<ModuleDefinition> moduleDefinitions = createModuleDefinitions(moduleDescriptors);
if (!moduleDefinitions.isEmpty()) {
definition.setModuleDefinitions(moduleDefinitions);
}
}
D savedDefinition = repository.save(definition);
return afterSave(savedDefinition);
}
@Override
public void validateBeforeSave(String name, String definition) {
Assert.hasText(name, "name cannot be blank or null");
D definitionFromRepo = getDefinitionRepository().findOne(name);
if (definitionFromRepo != null) {
throwDefinitionAlreadyExistsException(definitionFromRepo);
}
Assert.hasText(definition, "definition cannot be blank or null");
if (!ComposedJobUtil.isComposedJobDefinition(definition)){
parser.parse(name, definition, definitionKind);
} else {
composedJobParser.parse(definition);
}
}
/**
* Create a list of ModuleDefinitions given the results of parsing the definition.
*
* @param moduleDescriptors The list of ModuleDescriptors resulting from parsing the definition.
* @return a list of ModuleDefinitions
*/
protected List<ModuleDefinition> createModuleDefinitions(List<ModuleDescriptor> moduleDescriptors) {
List<ModuleDefinition> moduleDefinitions = new ArrayList<ModuleDefinition>(moduleDescriptors.size());
for (ModuleDescriptor moduleDescriptor : moduleDescriptors) {
moduleDefinitions.add(moduleDescriptor.getModuleDefinition());
}
return moduleDefinitions;
}
/**
* Return the ZooKeeper connection.
*
* @return the ZooKeeper connection
*/
protected ZooKeeperConnection getZooKeeperConnection() {
return zkConnection;
}
/**
* Callback method that subclasses may override to get a chance to act on newly saved definitions.
*/
protected D afterSave(D savedDefinition) {
return savedDefinition;
}
protected void throwDefinitionAlreadyExistsException(D definition) {
throw new DefinitionAlreadyExistsException(definition.getName(), String.format(
"There is already a %s named '%%s'", definitionKind));
}
protected void throwNoSuchDefinitionException(String name) {
throw new NoSuchDefinitionException(name,
String.format("There is no %s definition named '%%s'", definitionKind));
}
protected void throwDefinitionNotDeployable(String name) {
throw new NoSuchDefinitionException(name,
String.format("The %s named '%%s' cannot be deployed", definitionKind));
}
protected void throwNoSuchDefinitionException(String name, String definitionKind) {
throw new NoSuchDefinitionException(name,
String.format("There is no %s definition named '%%s'", definitionKind));
}
protected void throwNotDeployedException(String name) {
throw new NotDeployedException(name, String.format("The %s named '%%s' is not currently deployed",
definitionKind));
}
protected void throwAlreadyDeployedException(String name) {
throw new AlreadyDeployedException(name,
String.format("The %s named '%%s' is already deployed", definitionKind));
}
@Override
public D findOne(String name) {
return repository.findOne(name);
}
@Override
public Iterable<D> findAll() {
return repository.findAll();
}
@Override
public Page<D> findAll(Pageable pageable) {
return repository.findAll(pageable);
}
@Override
public void deleteAll() {
for (D d : findAll()) {
delete(d.getName());
}
}
protected CrudRepository<D, String> getDefinitionRepository() {
return repository;
}
/**
* Provides basic deployment behavior, whereby running state of deployed definitions is not persisted.
*
* @return the definition object for the given name
* @throws NoSuchDefinitionException if there is no definition by the given name
*/
protected D basicDeploy(String name, Map<String, String> properties) {
Assert.hasText(name, "name cannot be blank or null");
logger.trace("Deploying {}", name);
final D definition = getDefinitionRepository().findOne(name);
if (definition == null) {
throwNoSuchDefinitionException(name);
}
validateDeploymentProperties(definition, properties);
try {
String deploymentPath = getDeploymentPath(definition);
String statusPath = Paths.build(deploymentPath, Paths.STATUS);
byte[] propertyBytes = DeploymentPropertiesFormat.formatDeploymentProperties(properties).getBytes("UTF-8");
byte[] statusBytes = ZooKeeperUtils.mapToBytes(
new DeploymentUnitStatus(DeploymentUnitStatus.State.deploying).toMap());
zkConnection.getClient().inTransaction()
.create().forPath(deploymentPath, propertyBytes).and()
.create().withMode(CreateMode.EPHEMERAL).forPath(statusPath, statusBytes).and()
.commit();
}
catch (KeeperException.NodeExistsException e) {
throwAlreadyDeployedException(name);
}
catch (Exception e) {
throw ZooKeeperUtils.wrapThrowable(e);
}
return definition;
}
/**
* Validates that all deployment properties (of the form "module.<modulename>.<key>" do indeed
* reference module names that belong to the stream/job definition).
*/
private void validateDeploymentProperties(D definition, Map<String, String> properties) {
List<ModuleDescriptor> modules = null;
if(!ComposedJobUtil.isComposedJobDefinition(definition.getDefinition())){
modules = parser.parse(definition.getName(), definition.getDefinition(), definitionKind);
}
else {
modules = new ArrayList<ModuleDescriptor>();
ModuleDescriptor.Builder builder =
new ModuleDescriptor.Builder()
.setType(ModuleType.job)
.setGroup("job")
.setModuleName(ComposedJobUtil.getComposedJobModuleName(definition.getName()))
.setModuleLabel("")
.setIndex(0);
modules.add(builder.build());
}
Set<String> moduleLabels = new HashSet<String>(modules.size());
for (ModuleDescriptor md : modules) {
moduleLabels.add(md.getModuleLabel());
}
for (Map.Entry<String, String> pair : properties.entrySet()) {
Matcher matcher = DEPLOYMENT_PROPERTY_PATTERN.matcher(pair.getKey());
Assert.isTrue(matcher.matches(),
String.format("'%s' does not match '%s'", pair.getKey(), DEPLOYMENT_PROPERTY_PATTERN));
String moduleName = matcher.group(1);
Assert.isTrue("*".equals(moduleName) || moduleLabels.contains(moduleName),
String.format("'%s' refers to a module that is not in the list: %s", pair.getKey(), moduleLabels));
}
}
protected abstract D createDefinition(String name, String definition);
/**
* Return the ZooKeeper path used for deployment requests for the
* given definition.
*
* @param definition definition for which to obtain path
*
* @return ZooKeeper path for deployment requests
*/
protected abstract String getDeploymentPath(D definition);
@Override
public void validateBeforeDelete(String name) {
D def = getDefinitionRepository().findOne(name);
if (def == null) {
throwNoSuchDefinitionException(name);
}
}
@Override
public void delete(String name) {
D def = getDefinitionRepository().findOne(name);
if (def == null) {
throwNoSuchDefinitionException(name);
}
beforeDelete(def);
getDefinitionRepository().delete(def);
}
/**
* Callback method that subclasses may override to get a chance to act on definitions that are about to be deleted.
*/
protected void beforeDelete(D definition) {
}
}