io.stallion.dataAccess.file.FilePersisterBase.java Source code

Java tutorial

Introduction

Here is the source code for io.stallion.dataAccess.file.FilePersisterBase.java

Source

/*
 * Stallion Core: A Modern Web Framework
 *
 * Copyright (C) 2015 - 2016 Stallion Software LLC.
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 2 of
 * the License, or (at your option) any later version. 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. You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
 *
 *
 *
 */

package io.stallion.dataAccess.file;

import io.stallion.dataAccess.*;
import io.stallion.dataAccess.db.DefaultSort;
import io.stallion.dataAccess.filtering.FilterChain;
import io.stallion.exceptions.ConfigException;
import io.stallion.exceptions.UsageException;
import io.stallion.fileSystem.FileSystemWatcherService;
import io.stallion.fileSystem.TreeVisitor;
import io.stallion.reflection.PropertyComparator;
import io.stallion.services.Log;
import io.stallion.settings.Settings;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import static io.stallion.utils.Literals.*;

/**
 * A base persister that handles retrieving and saving model objects to the file sytem.
 *
 * @param <T>
 */
public abstract class FilePersisterBase<T extends Model> extends BasePersister<T> {

    private String bucketFolderPath = "";
    private Map<String, Long> fileToIdMap = new HashMap<>();
    private Map<String, Long> fileToTimestampMap = new HashMap<>();
    private Map<Long, String> idToFileMap = new HashMap<>();
    private boolean manyItemsPerFile = false;
    private String itemArrayName = "";
    protected String sortField = "lastModifiedMillis";
    protected String sortDirection = "DESC";

    @Override
    public void init(DataAccessRegistration registration, ModelController<T> controller, Stash<T> stash) {
        super.init(registration, controller, stash);
        bucketFolderPath = registration.getAbsolutePath();
        manyItemsPerFile = registration.isMultiplePerFile();
        itemArrayName = registration.getItemArrayName();
        idToFileMap = new HashMap<>();
        fileToIdMap = new HashMap<>();

        if (!StringUtils.isEmpty(registration.getAbsolutePath())) {
            Boolean exists = new File(registration.getAbsolutePath()).isDirectory();
            Log.fine("DAL target {0} exists? {1}", registration.getAbsolutePath(), exists);
            if (!exists) {
                new File(registration.getAbsolutePath()).mkdirs();
            }
        }

        DefaultSort defaultSort = getModelClass().getAnnotation(DefaultSort.class);
        if (defaultSort != null) {
            sortField = defaultSort.field();
            sortDirection = defaultSort.direction();
        }

    }

    public abstract Set<String> getFileExtensions();

    public boolean matchesExtension(String path) {
        String extension = FilenameUtils.getExtension(path).toLowerCase();
        return getFileExtensions().contains(extension);
    }

    @Override
    public List<T> fetchAll() {
        File target = new File(Settings.instance().getTargetFolder());
        if (!target.isDirectory()) {
            if (getItemController().isWritable()) {
                target.mkdirs();
            } else {
                throw new ConfigException(String.format(
                        "The JSON bucket %s (path %s) is read-only, but does not exist in the file system. Either create the folder, make it writable, or remove it from the configuration.",
                        getItemController().getBucket(), getBucketFolderPath()));
            }
        }
        TreeVisitor visitor = new TreeVisitor();
        Path folderPath = FileSystems.getDefault().getPath(getBucketFolderPath());
        try {
            Files.walkFileTree(folderPath, visitor);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        List<T> objects = new ArrayList<>();
        for (Path path : visitor.getPaths()) {
            if (!matchesExtension(path.toString())) {
                continue;
            }

            if (path.toString().contains(".#")) {
                continue;
            }
            if (path.getFileName().startsWith(".")) {
                continue;
            }
            T o = fetchOne(path.toString());
            if (o != null) {
                objects.add(o);
            }

        }
        objects.sort(new PropertyComparator<T>(sortField));
        if (sortDirection.toLowerCase().equals("desc")) {
            Collections.reverse(objects);
        }

        return objects;
    }

    @Override
    public T fetchOne(T obj) {
        return fetchOne(fullFilePathForObj(obj));
    }

    @Override
    public T fetchOne(Long id) {
        return fetchOne(fullFilePathForId(id));
    }

    public T fetchOne(String filePath) {
        if (filePath.startsWith(".") || filePath.startsWith("#") || filePath.contains("..")) {
            return null;
        }
        if (!matchesExtension(filePath)) {
            return null;
        }
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            return null;
        }
        Log.finest("Fetch one from file path: {0}", filePath);
        T o = doFetchOne(file);
        if (o == null) {
            return null;
        }
        if (empty(o.getId())) {
            o.setId(makeIdFromFilePath(filePath));
        }

        Long ts = file.lastModified();
        o.setLastModifiedMillis(ts);

        handleFetchOne(o);
        onPostLoadFromFile(o, filePath);
        return o;
    }

    public abstract T doFetchOne(File file);

    public FilterChain<T> filterChain() {
        throw new UsageException(
                "File based persistence does not work with filter chains. You have to use a LocalStash, which will provide an in memory filter chain.");
    }

    @Override
    public void hardDelete(T obj) {
        String filePath = fullFilePathForObj(obj);
        File file = new File(filePath);
        file.delete();
    }

    public void onPostLoadFromFile(T obj, String path) {
        if (path.startsWith(getBucketFolderPath())) {
            path = path.replace(getBucketFolderPath(), "");
        }
        if (path.startsWith("/")) {
            path = StringUtils.stripStart(path, "/");
        }
        getIdToFileMap().put(obj.getId(), path);
        getFileToIdMap().put(path, obj.getId());
        if (obj instanceof ModelWithFilePath) {
            ((ModelWithFilePath) obj).setFilePath(path);
        }

    }

    public boolean reloadIfNewer(T obj) {
        String path = fullFilePathForObj(obj);
        File file = new File(path.toString());
        Long currentTs = file.lastModified();
        Long fileLastModified = or(obj.getLastModifiedMillis(), 0L);
        if (currentTs >= fileLastModified) {
            getStash().loadForId(obj.getId());
            fileToTimestampMap.put(path.toString(), currentTs);
            return true;
        }
        return false;
    }

    public String relativeFilePathForObj(T obj) {
        if (obj instanceof ModelWithFilePath) {
            if (!empty(((ModelWithFilePath) obj).getFilePath())) {
                return ((ModelWithFilePath) obj).getFilePath();
            } else {
                String path = ((ModelWithFilePath) obj).generateFilePath();
                ((ModelWithFilePath) obj).setFilePath(path);
                return path;
            }
        } else if (getIdToFileMap().containsKey(obj.getId())) {
            return getIdToFileMap().get(obj.getId());
        } else {
            return makePathForObject(obj);
        }
    }

    public String makePathForObject(T obj) {
        return obj.getId().toString() + ".json";
    }

    public String fullFilePathForObj(T obj) {
        String path = getBucketFolderPath();
        if (!path.endsWith("/")) {
            path += "/";
        }
        return path + relativeFilePathForObj(obj);
    }

    public String fullFilePathForId(Long id) {
        String path = getBucketFolderPath();
        if (!path.endsWith("/")) {
            path += "/";
        }
        if (!getIdToFileMap().containsKey(id)) {
            return null;
        }
        return path + getIdToFileMap().get(id);
    }

    public void watchEventCallback(String filePath) {
        if (getFileToIdMap().containsKey(filePath)) {
            getStash().loadForId(getFileToIdMap().get(filePath));
        } else {
            String fullPath = getBucketFolderPath() + "/" + filePath;
            T o = fetchOne(fullPath);
            if (o != null) {
                getStash().loadItem(o);
            }
        }
    }

    @Override
    public void attachWatcher() {
        FileSystemWatcherService.instance().registerWatcher(new ItemFileChangeEventHandler(this)
                .setWatchedFolder(this.getBucketFolderPath()).setWatchTree(true));
    }

    /**
     * Derives a Long id by hashing the file path and then taking the first 8 bytes
     * of the path.
     *
     * This is used if the model object doesn't have a defined id field.
     *
     * @param path
     * @return
     */
    public Long makeIdFromFilePath(String path) {
        path = path.toLowerCase();
        path = path.replace(getBucketFolderPath().toLowerCase(), "");
        path = StringUtils.stripStart(path, "/");
        path = getBucket() + "-----" + path;
        // Derive a long id by hashing the file path
        byte[] bs = Arrays.copyOfRange(DigestUtils.md5(path), 0, 6);
        bs = ArrayUtils.addAll(new byte[] { 0, 0 }, bs);
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.put(bs);
        buffer.flip();//need flip
        Long l = buffer.getLong();
        if (l < 0) {
            l = -l;
        }
        Log.finest("calculated id is {0}", l);
        return l;
    }

    public String getBucketFolderPath() {
        return bucketFolderPath;
    }

    public FilePersisterBase setBucketFolderPath(String bucketFolderPath) {
        this.bucketFolderPath = bucketFolderPath;
        return this;
    }

    public Map<String, Long> getFileToIdMap() {
        return fileToIdMap;
    }

    public FilePersisterBase setFileToIdMap(Map<String, Long> fileToIdMap) {
        this.fileToIdMap = fileToIdMap;
        return this;
    }

    public Map<Long, String> getIdToFileMap() {
        return idToFileMap;
    }

    public FilePersisterBase setIdToFileMap(Map<Long, String> idToFileMap) {
        this.idToFileMap = idToFileMap;
        return this;
    }

    public boolean isManyItemsPerFile() {
        return manyItemsPerFile;
    }

    public FilePersisterBase setManyItemsPerFile(boolean manyItemsPerFile) {
        this.manyItemsPerFile = manyItemsPerFile;
        return this;
    }

    public String getItemArrayName() {
        return itemArrayName;
    }

    public FilePersisterBase setItemArrayName(String itemArrayName) {
        this.itemArrayName = itemArrayName;
        return this;
    }
}