/*
* Copyright 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.module;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.StringUtils;
import org.springframework.xd.dirt.core.RuntimeIOException;
import org.springframework.xd.module.ModuleDefinition;
import org.springframework.xd.module.ModuleDefinitions;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.module.SimpleModuleDefinition;
/**
* {@link Resource} based implementation of {@link ModuleRegistry}. <p>While not being coupled to {@code java.io.File}
* access, will try to sanitize file paths as much as possible.</p>
*
* @author Eric Bottard
* @author David Turanski
*/
public class ResourceModuleRegistry implements ModuleRegistry {
/**
* The extension the module 'File' must have if it's not in exploded dir format.
*/
public static final String ARCHIVE_AS_FILE_EXTENSION = ".jar";
/**
* The extension to use for storing an uploaded module hash.
*/
public static final String HASH_EXTENSION = ".md5";
private final static String[] SUFFIXES = new String[] {"", ARCHIVE_AS_FILE_EXTENSION};
/**
* Whether to require the presence of a hash file for archives. Helps preventing half-uploaded archives from being
* picked up.
*/
private boolean requireHashFiles;
protected ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
protected String root;
public ResourceModuleRegistry(String root) {
this.root = StringUtils.trimTrailingCharacter(root, '/');
}
protected Resource hashResource(Resource moduleResource) throws IOException {
// Yes, we'd like to use Resource.createRelative() but they don't all behave in the same way...
String location = moduleResource.getURI().toString() + HASH_EXTENSION;
return sanitize(resolver.getResources(location)).iterator().next();
}
@Override
public ModuleDefinition findDefinition(String name, ModuleType moduleType) {
List<ModuleDefinition> result = new ArrayList<ModuleDefinition>();
try {
for (String suffix : SUFFIXES) {
for (Resource resource : getResources(moduleType.name(), name, suffix)) {
collect(resource, result);
}
}
}
catch (IOException e) {
throw new RuntimeIOException(String.format("An error occurred trying to locate module '%s:%s'",
moduleType, name), e);
}
return result.size() == 1 ? result.iterator().next() : null;
}
@Override
public List<ModuleDefinition> findDefinitions(String name) {
List<ModuleDefinition> result = new ArrayList<ModuleDefinition>();
try {
for (String suffix : SUFFIXES) {
for (Resource resource : getResources("*", name, suffix)) {
collect(resource, result);
}
}
}
catch (IOException e) {
return Collections.emptyList();
}
return result;
}
@Override
public List<ModuleDefinition> findDefinitions(ModuleType type) {
List<ModuleDefinition> result = new ArrayList<>();
try {
for (Resource resource : getResources(type.name(), "*", "")) {
collect(resource, result);
}
}
catch (IOException e) {
return Collections.emptyList();
}
return result;
}
@Override
public List<ModuleDefinition> findDefinitions() {
List<ModuleDefinition> result = new ArrayList<>();
try {
for (Resource resource : getResources("*", "*", "")) {
collect(resource, result);
}
}
catch (IOException e) {
return Collections.emptyList();
}
return result;
}
protected Iterable<Resource> getResources(String moduleType,
String moduleName, String suffix) throws IOException {
String path = String.format("%s/%s/%s%s", this.root, moduleType, moduleName, suffix);
Resource[] resources = this.resolver.getResources(path);
List<Resource> filtered = sanitize(resources);
return filtered;
}
private List<Resource> sanitize(Resource[] resources) throws IOException {
List<Resource> filtered = new ArrayList<>();
for (Resource resource : resources) {
// Sanitize file paths (and force use of FSR, which is WritableResource)
if (resource instanceof UrlResource && resource.getURL().getProtocol().equals("file")
|| resource instanceof FileSystemResource) {
resource = new FileSystemResource(resource.getFile().getCanonicalFile());
}
String fileName = resource.getFilename();
if (!fileName.startsWith(".") &&
(!fileName.contains(".") ||
fileName.endsWith(ARCHIVE_AS_FILE_EXTENSION) ||
fileName.endsWith(HASH_EXTENSION))) {
filtered.add(resource);
}
}
return filtered;
}
protected void collect(Resource resource, List<ModuleDefinition> holder) throws IOException {
// ResourcePatternResolver.getResources() may return resources that don't exist when not using wildcards
if (!resource.exists()) {
return;
}
String path = resource.getURI().toString();
if (path.endsWith(HASH_EXTENSION)) {
return;
}
else if (path.endsWith(ARCHIVE_AS_FILE_EXTENSION) && requireHashFiles && !hashResource(resource).exists()) {
return;
}
// URI paths for directories include an extra slash, strip it for now
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
int lastSlash = path.lastIndexOf('/');
int nextToLastSlash = path.lastIndexOf('/', lastSlash - 1);
String name = path.substring(lastSlash + 1);
if (name.endsWith(ARCHIVE_AS_FILE_EXTENSION)) {
name = name.substring(0, name.length() - ARCHIVE_AS_FILE_EXTENSION.length());
}
String typeAsString = path.substring(nextToLastSlash + 1, lastSlash);
ModuleType type = null;
try {
type = ModuleType.valueOf(typeAsString);
}
catch (IllegalArgumentException e) {
// Not an actual type name, skip
return;
}
String locationToUse = resource.getURL().toString();
if (!locationToUse.endsWith(ARCHIVE_AS_FILE_EXTENSION) && !locationToUse.endsWith("/")) {
locationToUse += "/";
}
SimpleModuleDefinition found = ModuleDefinitions.simple(name, type, locationToUse);
if (holder.contains(found)) {
SimpleModuleDefinition other = (SimpleModuleDefinition) holder.get(holder.indexOf(found));
throw new IllegalStateException(String.format("Duplicate module definitions for '%s:%s' found at '%s' and" +
" " +
"'%s'",
found.getType(), found.getName(), found.getLocation(), other.getLocation()));
}
else {
holder.add(found);
}
}
public void setRequireHashFiles(boolean requireHashFiles) {
this.requireHashFiles = requireHashFiles;
}
}