LoadFromRelativeFile.java

package org.jbehave.core.io;

import java.io.File;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;

/**
 * <p>Loads story resources from relative file paths that are traversal to a given location.</p>
 * <code>
 * StoryLoader loader = new LoadFromRelativeFile(codeLocationFromClass(YourStory.class));
 * </code>
 * <p>By default, it uses traversal directory 'target/test-classes' with source dir in 'src/test/java'.</p>
 *
 * <p>Other traversal locations can be specified via the varargs constructor:</p>
 * <code>
 * StoryLoader loader = new LoadFromRelativeFile(codeLocationFromClass(YourStory.class),
 *         mavenModuleTestStoryFilePath("src/test/java"), intellijProjectTestStoryFilePath("src/test/java"));
 * </code>
 *
 * <p>Convenience methods : {@link LoadFromRelativeFile#mavenModuleStoryFilePath},
 * {@link LoadFromRelativeFile#mavenModuleTestStoryFilePath}, {@link LoadFromRelativeFile#intellijProjectStoryFilePath},
 * {@link LoadFromRelativeFile#intellijProjectTestStoryFilePath}</p>
 *
 * @see CodeLocations#codeLocationFromClass(Class)
 *
 */
public class LoadFromRelativeFile implements ResourceLoader, StoryLoader {

    private final Charset charset;
    private final StoryFilePath[] traversals;
    private final URL location;

    public LoadFromRelativeFile(URL location) {
        this(location, StandardCharsets.UTF_8);
    }

    public LoadFromRelativeFile(URL location, Charset charset) {
        this(location, charset, mavenModuleStoryFilePath("src/test/java"));
    }

    public LoadFromRelativeFile(URL location, StoryFilePath... traversals) {
        this(location, StandardCharsets.UTF_8, traversals);
    }

    public LoadFromRelativeFile(URL location, Charset charset, StoryFilePath... traversals) {
        this.charset = charset;
        this.traversals = traversals;
        this.location = location;
    }

    @Override
    public String loadResourceAsText(String resourcePath) {
        List<String> traversalPaths = new ArrayList<>();
        String locationPath = normalise(new File(CodeLocations.getPathFromURL(location)).getAbsolutePath());
        for (StoryFilePath traversal : traversals) {
            String filePath = locationPath.replace(traversal.toRemove, traversal.relativePath) + "/" + resourcePath;
            File file = new File(filePath);
            if (file.exists()) {
                return loadContent(filePath);
            } else {
                traversalPaths.add(filePath);
            }
        }
        throw new StoryResourceNotFound(resourcePath, traversalPaths);
    }

    @Override
    public String loadStoryAsText(String storyPath) {
        List<String> traversalPaths = new ArrayList<>();
        String locationPath = new File(CodeLocations.getPathFromURL(location)).getAbsolutePath();
        for (StoryFilePath traversal : traversals) {
            String filePath = locationPath.replace(traversal.toRemove, traversal.relativePath) + "/" + storyPath;
            File file = new File(filePath);
            if (file.exists()) {
                return loadContent(filePath);
            } else {
                traversalPaths.add(filePath);
            }
        }
        throw new StoryResourceNotFound(storyPath, traversalPaths);
    }

    protected String loadContent(String path) {
        try {
            return FileUtils.readFileToString(new File(path), charset);
        } catch (Exception e) {
            throw new InvalidStoryResource(path, e);
        }
    }

    private static String normalise(String path) {
        return path.replace('\\', '/');
    }

    /**
     * For use the the varargs constructor of {@link LoadFromRelativeFile}, to
     * allow a range of possibilities for locating Story file paths
     */
    public static class StoryFilePath {
        private final String toRemove;
        private final String relativePath;

        public StoryFilePath(String toRemove, String relativePath) {
            this.toRemove = normalise(toRemove);
            this.relativePath = normalise(relativePath);
        }

    }

    /**
     * Maven by default, has its PRODUCTION classes in target/classes. This
     * story file path is relative to that.
     *
     * @param relativePath
     *            the path to the stories' base-dir inside the module
     * @return the resulting StoryFilePath
     */
    public static StoryFilePath mavenModuleStoryFilePath(String relativePath) {
        return new StoryFilePath("target/classes", relativePath);
    }

    /**
     * Maven by default, has its TEST classes in target/test-classes. This story
     * file path is relative to that.
     *
     * @param relativePath
     *            the path to the stories' base-dir inside the module
     * @return the resulting StoryFilePath
     */
    public static StoryFilePath mavenModuleTestStoryFilePath(String relativePath) {
        return new StoryFilePath("target/test-classes", relativePath);
    }

    /**
     * Intellij by default, has its PRODUCTION classes in classes/production.
     * This story file path is relative to that.
     *
     * @param relativePath
     *            the path to the stories' base-dir inside the module
     * @return the resulting StoryFilePath
     */
    public static StoryFilePath intellijProjectStoryFilePath(String relativePath) {
        return new StoryFilePath("classes/production", relativePath);
    }

    /**
     * Intellij by default, has its TEST classes in classes/test. This story
     * file path is relative to that.
     *
     * @param relativePath
     *            the path to the stories' base-dir inside the module
     * @return the resulting StoryFilePath
     */
    public static StoryFilePath intellijProjectTestStoryFilePath(String relativePath) {
        return new StoryFilePath("classes/test", relativePath);
    }

}