org.geowebcache.sqlite.FileManager.java Source code

Java tutorial

Introduction

Here is the source code for org.geowebcache.sqlite.FileManager.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <p>
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Nuno Oliveira, GeoSolutions S.A.S., Copyright 2016
 */
package org.geowebcache.sqlite;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.filter.parameters.ParametersUtils;
import org.geowebcache.storage.TileObject;
import org.geowebcache.storage.TileRange;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.geowebcache.sqlite.Utils.Tuple;

/**
 * Class responsible to map GWC concepts (layer, tile, tile range, etc  ...)
 * to a filesystem file. The mapping is defined by a template that can use
 * the information associated with a tile.
 * <p>
 * <p/>
 * The template supported terms are:
 * <ul>
 * <li>params</li>
 * <li>x</li>
 * <li>y</li>
 * <li>z</li>
 * <li>layer</li>
 * <li>grid</li>
 * <li>format</li>
 * </ul>
 * It is also possible to use parameters referencing them by their name.
 * </p>
 * <p>
 * <p>
 * For example a template like the following:
 * <blockquote><pre>
 * {grid}/{layer}/{format}/{params}/{z}/tiles_{x}_{y}.sqlite
 * </pre></blockquote>
 * will produce paths similar to this one:
 * <blockquote><pre>
 * EPSG_4326/img_states/image_png/10/tiles_350_625.sqlite
 * </pre></blockquote>
 * </p>
 * <p>
 * <p>
 * Is possible to map all tiles to a single file by defining a static template (no terms).
 * Although, if a term is used it cannot be NULL otherwise an exception will be throw.
 * If a referenced parameter doesn't exists the string 'null' will be used.
 * </p>
 */
final class FileManager {

    private static Log LOGGER = LogFactory.getLog(FileManager.class);

    private final static Pattern PATH_TEMPLATE_ATTRIBUTE_PATTERN = Pattern.compile("\\{(.+?)\\}");

    private final File rootPath;

    private final long rowRangeCount;
    private final long columnRangeCount;

    // path builder extracted from the path template
    private final String[] pathBuilderOriginal;

    // keep track of which terms are used in the path template
    // the boolean tell us if the term was used in the path template
    // and the integer define the position of the term in the path builder
    private final Tuple<Boolean, Integer> replaceParametersId;
    private final Tuple<Boolean, Integer> replaceZoom;
    private final Tuple<Boolean, Integer> replaceRow;
    private final Tuple<Boolean, Integer> replaceColumn;
    private final Tuple<Boolean, Integer> replaceLayerName;
    private final Tuple<Boolean, Integer> replaceGridSetId;
    private final Tuple<Boolean, Integer> replaceFormat;

    // parameters used in the path template
    private final Set<Tuple<String, Integer>> replaceParameters;

    FileManager(File rootDirectory, String pathTemplate, long rowRangeCount, long columnRangeCount) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info(String.format(
                    "Initiating file manager: [rootDirectory='%s', pathTemplate='%s', "
                            + "rowRangeCount='%d', columnRangeCount='%d'].",
                    rootDirectory, pathTemplate, rowRangeCount, columnRangeCount));
        }
        this.rootPath = rootDirectory;
        this.rowRangeCount = rowRangeCount;
        this.columnRangeCount = columnRangeCount;
        // parsing the path template and extracting the terms that are used
        Tuple<String[], Set<Tuple<String, Integer>>> parserResult = parsePathTemplate(rootDirectory.getPath(),
                pathTemplate);
        pathBuilderOriginal = parserResult.first;
        replaceParametersId = findAndRemove(parserResult.second, "params");
        replaceZoom = findAndRemove(parserResult.second, "z");
        replaceRow = findAndRemove(parserResult.second, "x");
        replaceColumn = findAndRemove(parserResult.second, "y");
        replaceLayerName = findAndRemove(parserResult.second, "layer");
        replaceGridSetId = findAndRemove(parserResult.second, "grid");
        replaceFormat = findAndRemove(parserResult.second, "format");
        replaceParameters = parserResult.second;
    }

    /**
     * Builds the complete file path associated to the provided tile.
     */
    File getFile(TileObject tile) {
        if (tile.getParametersId() == null && tile.getParameters() != null) {
            tile.setParametersId(ParametersUtils.getId(tile.getParameters()));
        }
        return getFile(tile.getParametersId(), tile.getXYZ(), tile.getLayerName(), tile.getGridSetId(),
                tile.getBlobFormat(), tile.getParameters());
    }

    /**
     * Build a complete file path using the provided terms.
     */
    File getFile(String parametersId, long[] xyz, String layerName, String gridSetId, String format,
            Map<String, String> parameters) {
        // init this local thread path builder
        String[] pathBuilderCopy = getPathBuilderCopy();
        // replace the terms used in the path template with the respective values
        if (replaceParametersId.first) {
            pathBuilderCopy[replaceParametersId.second] = normalizeAttributeValue("params",
                    handleParametersId(parametersId, parameters));
        }
        if (replaceZoom.first) {
            pathBuilderCopy[replaceZoom.second] = String.valueOf(getLongValue(xyz, 2));
        }
        if (replaceRow.first) {
            pathBuilderCopy[replaceRow.second] = String.valueOf(computeColumnRange(getLongValue(xyz, 0)));
        }
        if (replaceColumn.first) {
            pathBuilderCopy[replaceColumn.second] = String.valueOf(computeRowRange(getLongValue(xyz, 1)));
        }
        if (replaceLayerName.first) {
            pathBuilderCopy[replaceLayerName.second] = normalizeAttributeValue("layer", layerName);
        }
        if (replaceGridSetId.first) {
            pathBuilderCopy[replaceGridSetId.second] = normalizeAttributeValue("grid", gridSetId);
        }
        if (replaceFormat.first) {
            pathBuilderCopy[replaceFormat.second] = normalizeAttributeValue("format", format);
        }
        // replace the parameters used in the path template with the respective values
        for (Tuple<String, Integer> replaceParameter : replaceParameters) {
            // searching for the parameter value in a non case sensitive way
            String value = parameters.get(replaceParameter.first.toUpperCase());
            value = value == null ? parameters.get(replaceParameter.first.toLowerCase()) : value;
            // if the parameter doesn't exits we use string 'null' as value
            value = value == null ? "null" : normalizeAttributeValue(replaceParameter.first, value);
            pathBuilderCopy[replaceParameter.second] = value;
        }
        return new File(concatStringArray(pathBuilderCopy, 0));
    }

    /**
     * Return the files present in the root directory that correspond to a certain layer.
     */
    List<File> getFiles(String layerName) {
        // init the thread local path builder
        String[] pathBuilderCopy = getPathBuilderCopy();
        // we only need to replace the layer term
        if (replaceLayerName.first)
            pathBuilderCopy[replaceLayerName.second] = layerName;
        return getFiles(pathBuilderCopy);
    }

    /**
     * Return the files present in the root directory that correspond to a certain layer
     * and certain grid set.
     */
    List<File> getFiles(String layerName, String gridSetId) {
        // init the thread local path builder
        String[] pathBuilderCopy = getPathBuilderCopy();
        // we replace the layer and grid set terms
        if (replaceLayerName.first)
            pathBuilderCopy[replaceLayerName.second] = layerName;
        if (replaceGridSetId.first)
            pathBuilderCopy[replaceGridSetId.second] = gridSetId;
        return getFiles(pathBuilderCopy);
    }

    /**
     * Return the files present in the root directory that correspond to a certain layer
     * and certain grid set.
     */
    List<File> getParametersFiles(String layerName, String parametersId) {
        // init the thread local path builder
        String[] pathBuilderCopy = getPathBuilderCopy();
        // we replace the layer and grid set terms
        if (replaceLayerName.first)
            pathBuilderCopy[replaceLayerName.second] = layerName;
        if (replaceParametersId.first)
            pathBuilderCopy[replaceParametersId.second] = parametersId;
        return getFiles(pathBuilderCopy);
    }

    /**
     * Build the paths correspondent to a tile range. For each file we return the associated tiles range by zoom.
     */
    Map<File, List<long[]>> getFiles(TileRange tileRange) {
        Map<File, List<long[]>> files = new HashMap<>();
        // let's iterate of all the available zoom levels
        for (int z = tileRange.getZoomStart(); z <= tileRange.getZoomStop(); z++) {
            long[] range = tileRange.rangeBounds(z);
            if (range == null) {
                // this zoom level doesn't have any tiles associated
                continue;
            }
            // get the files and associated tiles for the current zoom level
            getFiles(files, tileRange.getParametersId(), tileRange.getLayerName(), tileRange.getGridSetId(),
                    tileRange.getMimeType().getFormat(), tileRange.getParameters(), z, range);
        }
        return files;
    }

    /**
     * This method will substitute any char that cannot be used in a file path with an underscore.
     */
    public static String normalizePathValue(String value) {
        return value.replaceAll("\\\\|/|:|(?:\\s+)", "_");
    }

    /**
     * Helper method that for a specific zoom level and a range of tiles will build all the files
     * paths need to contains those tiles.
     */
    private void getFiles(Map<File, List<long[]>> files, String parametersId, String layerName, String gridSetId,
            String format, Map<String, String> parameters, long z, long[] range) {
        long minRangeX = (range[0] / columnRangeCount) * columnRangeCount;
        long maxRangeX = (range[2] / columnRangeCount) * rowRangeCount;
        long minRangeY = (range[1] / rowRangeCount) * rowRangeCount;
        long maxRangeY = (range[3] / rowRangeCount) * rowRangeCount;
        for (long x = minRangeX; x <= maxRangeX; x += columnRangeCount) {
            long minx = Math.max(x, range[0]);
            long maxx = Math.min(x + columnRangeCount - 1, range[2]);
            for (long y = minRangeY; y <= maxRangeY; y += rowRangeCount) {
                long[] tile = new long[] { x, y, z };
                File file = getFile(parametersId, tile, layerName, gridSetId, format, parameters);
                long miny = Math.max(y, range[1]);
                long maxy = Math.min(y + rowRangeCount - 1, range[3]);
                List<long[]> ranges = files.get(file);
                if (ranges == null) {
                    ranges = new ArrayList<>();
                    files.put(file, ranges);
                }
                ranges.add(new long[] { minx, miny, maxx, maxy, z });
            }
        }
    }

    /**
     * If the provided parameters id is null a new one will be build based on the provided parameters.
     */
    private static String handleParametersId(String parametersId, Map<String, String> parameters) {
        if (parametersId != null) {
            // the provided parameters id is ok
            return parametersId;
        }
        // computing a new parameters id based on the provided parameters
        String computedParametersId = ParametersUtils.getId(parameters);
        if (computedParametersId == null) {
            // the provided parameter are null or empty let's use the string 'null' as parameter id
            return "null";
        }
        return computedParametersId;
    }

    private static String normalizeAttributeValue(String attribute, String value) {
        Utils.check(value != null, "Path template attribute '%s' value is NULL.", attribute);
        return normalizePathValue(value);
    }

    private static long getLongValue(long[] xyz, int index) {
        Utils.check(xyz != null, "Path template attribute 'xyz' is NULL.");
        Utils.check(xyz.length == 3, "Path template attribute 'xyz' doesn't have the correct length.");
        return xyz[index];
    }

    /**
     * Helper method that will find in the root directory the files that
     * match the provided path builder.
     */
    private List<File> getFiles(String[] pathBuilderCopy) {
        // build the concrete path with the embedded regex values (.*?)
        String pathRegex = concatStringArray(pathBuilderCopy, 1);
        // separate all the path parts, useful to walk in the path hierarchy
        String[] pathRegexParts = pathRegex.split(Utils.REGEX_FILE_SEPARATOR);
        // walk the directory tree to find all the files that match the builder path
        return walkFileTreeWithRegex(rootPath, 0, pathRegexParts);
    }

    /**
     * Helper method that will walk recursively the directory hierarchy based on
     * the provided path parts.
     */
    private static List<File> walkFileTreeWithRegex(File path, int level, String[] pathParts) {
        // filter the current directory files that match the current path part
        File[] files = path.listFiles((directory, name) -> {
            String pathPart = pathParts[level];
            // if need the current path will be interpreted as a regex (.*?)
            return pathPart.equals(name) || name.matches(pathPart);
        });
        if (Objects.isNull(files)) {
            return Collections.emptyList();
        }
        if (level != pathParts.length - 1) {
            // let's walk recursively in the matched files
            List<File> matchedFiles = new ArrayList<>();
            for (File file : files) {
                matchedFiles.addAll(walkFileTreeWithRegex(file, level + 1, pathParts));
            }
            return matchedFiles;
        }
        // we are in the last directory before the path end so we simply return the matched files
        return Arrays.asList(files);
    }

    private static String concatStringArray(String[] array, int startIndex) {
        StringBuilder result = new StringBuilder();
        for (int i = startIndex; i < array.length; i++) {
            result.append(array[i]);
        }
        return result.toString();
    }

    private String[] getPathBuilderCopy() {
        String[] pathBuilderCopy = new String[pathBuilderOriginal.length];
        System.arraycopy(pathBuilderOriginal, 0, pathBuilderCopy, 0, pathBuilderOriginal.length);
        return pathBuilderCopy;
    }

    private static Tuple<Boolean, Integer> findAndRemove(Set<Tuple<String, Integer>> attributes, String attribute) {
        Tuple<String, Integer> found = null;
        for (Tuple<String, Integer> candidateAttribute : attributes) {
            if (candidateAttribute.first.equals(attribute)) {
                if (found != null) {
                    throw Utils.exception("Term '%s' appears multiple times in the path template.", attribute);
                }
                found = candidateAttribute;
            }
        }
        if (found != null) {
            attributes.remove(found);
            return Tuple.tuple(true, found.second);
        }
        return Tuple.tuple(false, -1);
    }

    /**
     * Helper method that will parse a path template and return a path builder
     * and the found terms and the used parameters.
     */
    private static Tuple<String[], Set<Tuple<String, Integer>>> parsePathTemplate(String rootPath,
            String pathTemplate) {
        // replacing chars '\' and '/' with the current os path separator
        pathTemplate = pathTemplate.replaceAll("(\\\\)|/", Utils.REGEX_FILE_SEPARATOR);
        List<String> pathBuilder = new ArrayList<>();
        // the first element of the path builder is the root directory
        pathBuilder.add(rootPath + File.separator);
        Set<Tuple<String, Integer>> attributes = new HashSet<>();
        // matching all the available terms in the path template
        Matcher matcher = PATH_TEMPLATE_ATTRIBUTE_PATTERN.matcher(pathTemplate);
        int lastMatchIndex = 0;
        int pathBuilderIndex = 1;
        while (matcher.find()) {
            // keeping track of the found term and is position on the path builder
            pathBuilder.add(pathTemplate.substring(lastMatchIndex, matcher.start()));
            // adding the match all regex expression to the path builder (match all files at that level)
            pathBuilder.add(".*?");
            String attribute = matcher.group(1).toLowerCase();
            attributes.add(Tuple.tuple(attribute, pathBuilderIndex + 1));
            pathBuilderIndex += 2;
            lastMatchIndex = matcher.end();
        }
        pathBuilder.add(pathTemplate.substring(lastMatchIndex, pathTemplate.length()));
        return Tuple.tuple(pathBuilder.toArray(new String[pathBuilder.size()]), attributes);
    }

    private long computeRowRange(long tileRow) {
        return (tileRow / rowRangeCount) * rowRangeCount;
    }

    private long computeColumnRange(long tileRow) {
        return (tileRow / columnRangeCount) * columnRangeCount;
    }
}