rapture.series.file.FileSeriesStore.java Source code

Java tutorial

Introduction

Here is the source code for rapture.series.file.FileSeriesStore.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package rapture.series.file;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

// TODO Not convinced that rapture.series.mem is the best package name for this
// but if it moves then the class's full package name is hard coded as a String in a couple of places.

import rapture.common.RaptureFolderInfo;
import rapture.common.RaptureURI;
import rapture.common.RaptureURI.Parser;
import rapture.common.SeriesValue;
import rapture.common.exception.ExceptionToString;
import rapture.common.exception.RaptureExceptionFactory;
import rapture.dsl.serfun.DecimalSeriesValue;
import rapture.dsl.serfun.LongSeriesValue;
import rapture.dsl.serfun.SeriesValueCodec;
import rapture.dsl.serfun.StringSeriesValue;
import rapture.dsl.serfun.StructureSeriesValueImpl;
import rapture.kernel.file.FileRepoUtils;
import rapture.series.AbstractSeriesStore;
import rapture.series.SeriesPaginator;
import rapture.series.children.ChildrenRepo;

/**
 * A file based version of a series repo, for testing
 *
 * @author dtong
 */
public class FileSeriesStore extends AbstractSeriesStore {

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "FileSeriesStore [childrenRepo=" + childrenRepo + ", parentDir=" + parentDir + ", forwards="
                + forwards + ", backwards=" + backwards + ", instanceName=" + instanceName + "]";
    }

    public static final String PREFIX = "prefix";
    // Not sure what this is
    private final ChildrenRepo childrenRepo;
    private File parentDir = null;
    private static final Logger log = Logger.getLogger(FileSeriesStore.class);

    Comparator<SeriesValue> forwards = new Comparator<SeriesValue>() {
        @Override
        public int compare(SeriesValue o1, SeriesValue o2) {
            return o1.getColumn().compareTo(o2.getColumn());
        }
    };

    Comparator<SeriesValue> backwards = new Comparator<SeriesValue>() {
        @Override
        public int compare(SeriesValue o1, SeriesValue o2) {
            return o2.getColumn().compareTo(o1.getColumn());
        }
    };

    private String instanceName = "default";

    public FileSeriesStore() {
        this.childrenRepo = new ChildrenRepo() {

            @Override
            public List<SeriesValue> getPoints(String key) {
                return FileSeriesStore.this.getPoints(key);
            }

            @Override
            public boolean dropPoints(String key, List<String> points) {
                return FileSeriesStore.this.deletePointsFromSeriesByPointKey(key, points);
            }

            @Override
            public boolean addPoint(String key, SeriesValue value) {
                FileSeriesStore.this.addPointToSeries(key, value);
                return true;
            }

            @Override
            public boolean dropRow(String key) {
                return FileSeriesStore.this.deletePointsFromSeries(key);
            }
        };
    }

    @Override
    public void setConfig(Map<String, String> config) {
        // What happens if this is called twice?
        if (parentDir != null)
            throw RaptureExceptionFactory.create("Calling setConfig twice is currently not supported");
        String prefix = config.get(FileRepoUtils.PREFIX);
        if (StringUtils.trimToNull(prefix) == null)
            throw RaptureExceptionFactory.create("prefix must be specified");
        parentDir = FileRepoUtils.ensureDirectory(prefix + "_series");
    }

    @Override
    public void drop() {
        if (parentDir != null)
            FileUtils.deleteQuietly(parentDir);
        parentDir = null;
    }

    /**
     * Returns a sorted series
     *
     * @param direction
     *            true for verbatim, false for reverse order.
     * @return
     */
    protected Collection<SeriesValue> readSeriesSorted(String key, boolean direction) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key);
        if (!seriesFile.isFile())
            return new ArrayList<>();
        TreeSet<SeriesValue> series = new TreeSet<>((direction) ? forwards : backwards);
        try {
            List<String> lines = Files.readAllLines(seriesFile.toPath(), StandardCharsets.UTF_8);
            for (String line : lines) {
                char c = line.charAt(0);
                int i = line.indexOf(c, 1);
                String column = line.substring(1, i);
                byte[] bytes = line.substring(i + 1).getBytes();
                series.add(SeriesValueCodec.decode(column, bytes));
            }
            return series;
        } catch (IOException e) {
            log.debug(ExceptionToString.format(e));
        }
        return null;
    }

    /**
     * Writes an ordered series back to the file system
     *
     * @param key
     * @param values
     * @return
     */
    protected boolean writeSeries(String key, Collection<SeriesValue> values) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key + Parser.COLON_CHAR);
        if (!seriesFile.exists())
            try {
                if ((!seriesFile.getParentFile().isDirectory() && !seriesFile.getParentFile().mkdirs())
                        || !seriesFile.createNewFile())
                    throw RaptureExceptionFactory.create("Cannot create series " + seriesFile.getAbsolutePath());
            } catch (IOException ioe) {
                log.debug(ExceptionToString.format(ioe));
                throw RaptureExceptionFactory.create("Cannot create series " + key, ioe);
            }

        String path = seriesFile.getAbsolutePath();
        String backup = path + "_prev";
        File backupFile = new File(backup);
        try {
            seriesFile.renameTo(backupFile);
            try (FileOutputStream fos = new FileOutputStream(new File(path))) {
                for (SeriesValue value : values) {
                    int ch = 32;
                    // Find unique marker
                    String column = value.getColumn();
                    while (column.indexOf(ch) >= 0)
                        ch++;
                    fos.write(ch);
                    fos.write(column.getBytes());
                    fos.write(ch);
                    fos.write(SeriesValueCodec.encodeValue(value));
                    fos.write('\n');
                }
            }
        } catch (IOException e) {
            log.debug(ExceptionToString.format(e));
            // put it back
            seriesFile.renameTo(new File(path));
            return false;
        }
        backupFile.delete();
        return true;
    }

    @Override
    public void addDoubleToSeries(String key, String column, double value) {
        addPointToSeries(key, new DecimalSeriesValue(value, column));
    }

    @Override
    public void addDoublesToSeries(String key, List<String> columns, List<Double> values) {
        addPointsToSeries(key, DecimalSeriesValue.zip(columns, values));
    }

    @Override
    public void addLongToSeries(String key, String column, long value) {
        addPointToSeries(key, new LongSeriesValue(value, column));
    }

    @Override
    public void addLongsToSeries(String key, List<String> columns, List<Long> values) {
        addPointsToSeries(key, LongSeriesValue.zip(columns, values));
    }

    @Override
    public void addStringToSeries(String key, String column, String value) {
        addPointToSeries(key, new StringSeriesValue(value, column));
    }

    @Override
    public void addStringsToSeries(String key, List<String> columns, List<String> values) {
        addPointsToSeries(key, StringSeriesValue.zip(columns, values));
    }

    @Override
    public void addStructureToSeries(String key, String column, String jsonValue) {
        try {
            addPointToSeries(key, StructureSeriesValueImpl.unmarshal(jsonValue, column));
        } catch (IOException e) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    "Error parsing json value " + jsonValue, e);
        }
    }

    @Override
    public void addStructuresToSeries(String key, List<String> columns, List<String> jsonValues) {
        try {
            addPointsToSeries(key, StructureSeriesValueImpl.zip(columns, jsonValues));
        } catch (IOException e) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST, "Error parsing json values",
                    e);
        }
    }

    @Override
    public void addPointsToSeries(String key, List<SeriesValue> values) {
        boolean nullKey = false;
        for (SeriesValue value : values) {
            if (value.getColumn() == null)
                nullKey = true;
            else
                addPointToSeries(key, value);
        }
        if (nullKey)
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_BAD_REQUEST,
                    "Column Key may not be null, other values added");
    }

    @Override
    public void addPointToSeries(String key, SeriesValue value) {
        Collection<SeriesValue> series = readSeriesSorted(key, true);
        series.add(value);
        writeSeries(key, series);
    }

    @Override
    public boolean deletePointsFromSeriesByPointKey(String key, List<String> pointKeys) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key + Parser.COLON_CHAR);
        if ((pointKeys == null) || pointKeys.isEmpty() || !seriesFile.isFile())
            return true;
        List<SeriesValue> series = getPoints(key);
        if (series == null)
            return true;
        List<SeriesValue> newSeries = new ArrayList<SeriesValue>();
        for (SeriesValue value : series) {
            if (pointKeys.contains(value.getColumn())) {
                pointKeys.remove(value.getColumn());
            } else {
                newSeries.add(value);
            }
        }
        return writeSeries(key, newSeries);
    }

    @Override
    public boolean deletePointsFromSeries(String key) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key + Parser.COLON_CHAR);
        if (!seriesFile.isFile())
            return false;
        return seriesFile.delete();
    }

    @Override
    public List<SeriesValue> getPoints(String key) {
        return getPointsAfter(key, null, null, Integer.MAX_VALUE);
    }

    @Override
    public List<SeriesValue> getPointsAfter(String key, String startColumn, int maxNumber) {
        return getPointsAfter(key, startColumn, null, maxNumber);
    }

    @Override
    public List<SeriesValue> getPointsAfterReverse(String key, String startColumn, int maxNumber) {
        return getPointsAfterReverse(key, startColumn, null, maxNumber);
    }

    public List<SeriesValue> getPointsAfterReverse(String key, String startColumn, String endColumn,
            int maxNumber) {
        List<SeriesValue> series = new ArrayList<>(); // or ((maxNumber > values.size()) ? values.size() : maxNumber);
        boolean found = false;
        int limit = (maxNumber > overflowLimit) ? overflowLimit : maxNumber;
        for (SeriesValue value : readSeriesSorted(key, false)) {
            if (!found) {
                if (value.getColumn().equals(startColumn)) {
                    found = true;
                } else
                    continue;
            }
            series.add(value);
            if (series.size() == limit)
                break;
            if ((endColumn != null) && value.getColumn().equals(endColumn))
                break;
        }
        return series;
    }

    @Override
    public List<SeriesValue> getPointsAfter(String key, String startColumn, String endColumn, int maxNumber) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key + Parser.COLON_CHAR);
        if (!seriesFile.exists())
            return new ArrayList<>();
        if (!seriesFile.isFile())
            throw RaptureExceptionFactory
                    .create("For FILE implementation you can't have a Series with the same name as a Folder");
        List<SeriesValue> series = new ArrayList<>();
        int limit = (maxNumber > overflowLimit) ? overflowLimit : maxNumber;
        boolean found = (startColumn == null);
        try {
            List<String> lines = Files.readAllLines(seriesFile.toPath(), StandardCharsets.UTF_8);
            for (String line : lines) {
                char c = line.charAt(0);
                int i = line.indexOf(c, 1);
                String column = line.substring(1, i);
                byte[] bytes = line.substring(i + 1).getBytes();
                SeriesValue value = SeriesValueCodec.decode(column, bytes);
                if (!found) {
                    if (!value.getColumn().equals(startColumn))
                        continue;
                    found = true;
                }
                series.add(value);
                if (series.size() == limit)
                    break;
                if ((endColumn != null) && value.getColumn().equals(endColumn))
                    break;
            }
        } catch (IOException e) {
            log.debug(ExceptionToString.format(e));
            throw RaptureExceptionFactory.create("Cannot read Series " + key, e);
        }
        return series;
    }

    @Override
    public void setInstanceName(String instanceName) {
        // TODO what dis for?
        this.instanceName = instanceName;
    }

    @Override
    public List<String> getSeriesLike(String keyPrefix) {
        // TODO
        throw new RuntimeException("Yeah yeah, I'll get to it.");

        // List<String> ret = new ArrayList<>();
        // for (String key : seriesStore.keySet()) {
        // if (key.startsWith(keyPrefix)) {
        // ret.add(key);
        // }
        // }
        // return ret;

    }

    @Override
    public Iterable<SeriesValue> getRangeAsIteration(String key, String startCol, String endCol, int pageSize) {
        return new SeriesPaginator(key, startCol, endCol, pageSize, this);
    }

    @Override
    public List<SeriesValue> getRangeAsList(String key, String startCol, String endCol) {
        return getPointsAfter(key, startCol, endCol, Integer.MAX_VALUE);
    }

    @Override
    public List<RaptureFolderInfo> listSeriesByUriPrefix(String string) {
        File file = FileRepoUtils.makeGenericFile(parentDir, string);
        if (!file.isDirectory())
            return null;

        File[] children = file.listFiles();
        List<RaptureFolderInfo> info = new ArrayList<>((children == null) ? 0 : children.length);
        if ((children != null) && (children.length > 0)) {
            for (File kid : children) {
                RaptureFolderInfo inf = new RaptureFolderInfo();
                if (kid.isDirectory()) {
                    inf.setName(kid.getName());
                    inf.setFolder(true);
                } else {
                    inf.setName(kid.getName().substring(0, kid.getName().length() - 1)); // remove trailing colon
                    inf.setFolder(false);
                }
                info.add(inf);
            }
        }
        return info;
    }

    @Override
    public boolean unregisterKey(String key) {
        return childrenRepo.dropFileEntry(key);
    }

    @Override
    public boolean unregisterKey(String key, boolean isFolder) {
        if (isFolder)
            childrenRepo.dropFolderEntry(key);
        return unregisterKey(key);
    }

    @Override
    public SeriesValue getLastPoint(String key) {
        File seriesFile = FileRepoUtils.makeGenericFile(parentDir, key + Parser.COLON_CHAR);
        if (!seriesFile.isFile())
            return null;
        try {
            List<String> lines = Files.readAllLines(seriesFile.toPath(), StandardCharsets.UTF_8);
            if (!lines.isEmpty()) {
                String line = lines.get(lines.size() - 1);
                char c = line.charAt(0);
                int i = line.indexOf(c, 1);
                String column = line.substring(1, i);
                byte[] bytes = line.substring(i + 1).getBytes();
                return SeriesValueCodec.decode(column, bytes);
            }
        } catch (IOException e) {
            log.debug(ExceptionToString.format(e));
        }
        return null;
    }

    @Override
    public void createSeries(String key) {
        writeSeries(key, new ArrayList<SeriesValue>());
    }

    @Override
    public void deleteSeries(String key) {
        unregisterKey(key);
        deletePointsFromSeries(key);
    }

    public boolean deleteFolder(RaptureURI uri) {
        File f = FileRepoUtils.makeGenericFile(parentDir, uri.getDocPath());
        if (f.exists() && f.isDirectory()) {
            if (f.list().length == 0) {
                try {
                    java.nio.file.Files.delete(f.toPath());
                    return true;
                } catch (IOException e) {
                    log.error("Unable to delete " + uri.toString() + " because " + e.getMessage());
                }
            } else {
                log.debug("Folder is not empty " + uri.toString());
            }
        } else {
            log.debug("Not a folder " + uri.toString());
        }
        return false;
    }
}