/*
* Copyright 2011-2016 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.module.support;
import static org.springframework.xd.dirt.stream.ParsingContext.module;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.xd.dirt.job.dsl.ComposedJobUtil;
import org.springframework.xd.dirt.job.dsl.JobParser;
import org.springframework.xd.dirt.module.DependencyException;
import org.springframework.xd.dirt.module.ModuleAlreadyExistsException;
import org.springframework.xd.dirt.module.ModuleDependencyRepository;
import org.springframework.xd.dirt.module.NoSuchModuleException;
import org.springframework.xd.dirt.module.UploadedModuleDefinition;
import org.springframework.xd.dirt.module.WritableModuleRegistry;
import org.springframework.xd.dirt.stream.XDStreamParser;
import org.springframework.xd.dirt.util.PagingUtility;
import org.springframework.xd.module.ModuleDefinition;
import org.springframework.xd.module.ModuleDefinitions;
import org.springframework.xd.module.ModuleDescriptor;
import org.springframework.xd.module.ModuleType;
/**
* A service that knows how to handle registration of new module definitions, be it through composition or
* upload of actual 'bytecode'. Handles all bookkeeping (such as existence checking, dependency tracking, <i>etc.</i>)
* that is common to all registration scenarios.
*
* <p>Also adds pagination to {@code find*()} methods of {@code ModuleRegistry} after the fact.</p>
*
* @author Eric Bottard
* @author Gary Russell
*/
public class ModuleDefinitionService {
private final WritableModuleRegistry registry;
private final XDStreamParser parser;
private final ModuleDependencyRepository dependencyRepository;
private final PagingUtility<ModuleDefinition> pagingUtility = new PagingUtility<ModuleDefinition>();
private final JobParser composedJobParser;
@Autowired
public ModuleDefinitionService(WritableModuleRegistry registry, XDStreamParser parser, ModuleDependencyRepository dependencyRepository) {
this.registry = registry;
this.parser = parser;
this.dependencyRepository = dependencyRepository;
this.composedJobParser = new JobParser();
}
public ModuleDefinition findDefinition(String name, ModuleType type) {
return registry.findDefinition(name, type);
}
public Page<ModuleDefinition> findDefinitions(Pageable pageable, String name) {
List<ModuleDefinition> raw = registry.findDefinitions(name);
return pagingUtility.getPagedData(pageable, raw);
}
public Page<ModuleDefinition> findDefinitions(Pageable pageable, ModuleType type) {
List<ModuleDefinition> raw = registry.findDefinitions(type);
return pagingUtility.getPagedData(pageable, raw);
}
public Page<ModuleDefinition> findDefinitions(Pageable pageable) {
List<ModuleDefinition> raw = registry.findDefinitions();
return pagingUtility.getPagedData(pageable, raw);
}
public ModuleDefinition compose(String name, ModuleType typeHint, String dslDefinition, boolean force) {
ModuleDefinition moduleDefinition = null;
if(typeHint == ModuleType.job){
moduleDefinition = composeJob(name, typeHint, dslDefinition, force);
}
else{
moduleDefinition = composeStream(name, typeHint, dslDefinition, force);
}
return moduleDefinition;
}
private ModuleDefinition composeJob(String name, ModuleType typeHint, String dslDefinition, boolean force){
assertModuleUpdatability(name, typeHint, force);
String composedJobXml = composedJobParser.parse(dslDefinition).toXML(name);
byte bytes[] = createComposedJobJar(composedJobXml);
ModuleDefinition moduleDefinition = new UploadedModuleDefinition(name, typeHint, bytes) ;
Assert.isTrue(this.registry.registerNew(moduleDefinition), moduleDefinition + " could not be saved");
return moduleDefinition;
}
private byte[] createComposedJobJar(String xml) {
JarOutputStream target = null;
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
target = new JarOutputStream(outStream);
JarEntry entry = new JarEntry("config/");
target.putNextEntry(entry);
target.closeEntry();
writeXML(target, xml);
writeParameters(target, ComposedJobUtil.getPropertyDefinition());
//This needs to be explicitly closed before we get the bytes so that the
//compression is complete. target.flush() does not work for this.
target.close();
return outStream.toByteArray();
}
catch (Exception e) {
try{
if(target != null){
target.close();
}
}catch (IOException ie){
throw new IllegalStateException(e.getMessage(), e);
}
throw new IllegalStateException(e.getMessage(), e);
}
}
private void writeXML(JarOutputStream target, String xml) {
try (InputStream in = new ByteArrayInputStream(xml.getBytes(Charset.forName(StandardCharsets.UTF_8.name())))) {
JarEntry entry = new JarEntry("config/composedjob.xml");
target.putNextEntry(entry);
StreamUtils.copy(in, target);
target.closeEntry();
}
catch(IOException ioe){
throw new IllegalStateException(ioe.getMessage(), ioe);
}
}
private void writeParameters(JarOutputStream target, String parameters) {
try (InputStream in = new ByteArrayInputStream(parameters.getBytes(Charset.forName("UTF-8")))) {
JarEntry entry = new JarEntry("config/composedjob.properties");
target.putNextEntry(entry);
StreamUtils.copy(in, target);
target.closeEntry();
}
catch(IOException ioe){
throw new IllegalStateException(ioe.getMessage(), ioe);
}
}
private ModuleDefinition composeStream(String name, ModuleType typeHint, String dslDefinition, boolean force){
// TODO: pass typeHint to parser (XD-2343)
List<ModuleDescriptor> parseResult = this.parser.parse(name, dslDefinition, module);
ModuleType type = this.determineType(parseResult);
assertModuleUpdatability(name, type, force);
// TODO: XD-2284 need more than ModuleDefinitions (need to capture passed in options, etc)
List<ModuleDefinition> composedModuleDefinitions = createComposedModuleDefinitions(parseResult);
ModuleDefinition moduleDefinition = ModuleDefinitions.composed(name, type, dslDefinition, composedModuleDefinitions);
Assert.isTrue(this.registry.registerNew(moduleDefinition), moduleDefinition + " could not be saved");
return moduleDefinition;
}
public ModuleDefinition upload(String name, ModuleType type, byte[] bytes, boolean force) {
assertModuleUpdatability(name, type, force);
ModuleDefinition definition = new UploadedModuleDefinition(name, type, bytes);
Assert.isTrue(this.registry.registerNew(definition), definition + " could not be saved");
return definition;
}
/**
* Throws an exception if either one of the following is true:<ul>
* <li>a module with the given name and type already exists and force is {@code false}</li>
* <li>force if {@true} but the module is in use (by a stream or composed module)</li>
* </ul>
*
* <p>Also, will actually delete the already existing definition in case of an update,
* even though it would be overwritten by a registry, to cover the cases where a composed
* module is being replaced by a simple uploaded module, and <i>vice versa</i>.</p>
*
* @param name name of the module we're trying to update/create
* @param type type of the module we're trying to update/create
* @param force whether to attempt to force update if the module already exists
*/
private void assertModuleUpdatability(String name, ModuleType type, boolean force) {
ModuleDefinition definition = registry.findDefinition(name, type);
if (definition != null) {
if (!force) {
throw new ModuleAlreadyExistsException(name, type);
} else {
Set<String> dependents = this.dependencyRepository.find(name, type);
if (!dependents.isEmpty()) {
throw new DependencyException("Cannot force update module %2$s:%1$s because it is used by %3$s", name, type, dependents);
} else {
// Perform an eager deletion, taking care of module flavor change.
// Also, this catches cases when we're trying to replace a read-only module
if (!registry.delete(definition)) {
throw new ModuleAlreadyExistsException("There is already a module named '%s' with type '%s', and it cannot be updated", name, type);
}
}
}
}
}
public void delete(String name, ModuleType type) {
ModuleDefinition definition = registry.findDefinition(name, type);
if (definition == null) {
throw new NoSuchModuleException(name, type);
}
Set<String> dependents = this.dependencyRepository.find(name, type);
if (!dependents.isEmpty()) {
throw new DependencyException("Cannot delete module %2$s:%1$s because it is used by %3$s", name, type,
dependents);
}
boolean result = this.registry.delete(definition);
Assert.isTrue(result, String.format("Could not delete module '%s:%s'", type, name));
}
private List<ModuleDefinition> createComposedModuleDefinitions(
List<ModuleDescriptor> moduleDescriptors) {
List<ModuleDefinition> moduleDefinitions = new ArrayList<ModuleDefinition>(moduleDescriptors.size());
for (ModuleDescriptor moduleDescriptor : moduleDescriptors) {
moduleDefinitions.add(registry.findDefinition(moduleDescriptor.getModuleName(),
moduleDescriptor.getType()));
}
return moduleDefinitions;
}
private ModuleType determineType(List<ModuleDescriptor> modules) {
Assert.isTrue(modules != null && modules.size() > 0, "at least one module required");
if (modules.size() == 1) {
return modules.get(0).getType();
}
Collections.sort(modules);
ModuleType firstType = modules.get(0).getType();
ModuleType lastType = modules.get(modules.size() - 1).getType();
boolean hasInput = firstType != ModuleType.source;
boolean hasOutput = lastType != ModuleType.sink;
if (hasInput && hasOutput) {
return ModuleType.processor;
}
if (hasInput) {
return ModuleType.sink;
}
if (hasOutput) {
return ModuleType.source;
}
throw new IllegalArgumentException("invalid module composition; must expose input and/or output channel");
}
}