am.ik.categolj3.api.git.GitStore.java Source code

Java tutorial

Introduction

Here is the source code for am.ik.categolj3.api.git.GitStore.java

Source

/*
 * Copyright (C) 2015 Toshiaki Maki <makingx@gmail.com>
 *
 * 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 am.ik.categolj3.api.git;

import am.ik.categolj3.api.entry.Author;
import am.ik.categolj3.api.entry.Entry;
import am.ik.categolj3.api.entry.EntryEventFiringCache;
import am.ik.categolj3.api.event.AppState;
import am.ik.categolj3.api.event.EventManager;
import com.google.common.collect.Iterables;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.FileSystemUtils;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

@Component
@Slf4j
public class GitStore {
    @Autowired
    GitProperties gitProperties;

    @Autowired
    CacheManager cacheManager;

    @Autowired
    EventManager eventManager;

    @Autowired
    ForceRefreshTask forceRefreshTask;

    @Autowired
    GitPullTask gitPullTask;

    @Autowired
    ApplicationContext applicationContext;

    Cache entryCache;

    Git git;

    AtomicReference<ObjectId> currentHead = new AtomicReference<>();

    public Entry get(Long entryId) {
        Entry entry = this.entryCache.get(entryId, Entry.class);
        if (entry == null) {
            entry = getContentFiles().stream().filter(f -> String.format("%05d.md", entryId).equals(f.getName()))
                    .map(File::toPath).map(Entry::loadFromFile).filter(Optional::isPresent).map(Optional::get)
                    .findAny().orElseThrow(() -> new IllegalArgumentException(
                            "The requested entry is not found [" + entryId + "]"));
            this.entryCache.put(entryId, entry);
        }
        return entry;
    }

    public CompletableFuture<PullResult> pull() {
        return this.gitPullTask.pull(this.git).thenApply(x -> {
            this.syncHead();
            return x;
        });
    }

    public List<Entry> loadEntries() {
        return getContentFiles().stream().map(File::toPath).map(this::loadEntry).filter(Optional::isPresent)
                .map(Optional::get).collect(Collectors.toList());
    }

    public void forceRefreshAll() {
        loadEntries().forEach(entry -> {
            entryCache.put(entry.getEntryId(), entry);
        });
    }

    void syncHead() {
        log.info("Syncing HEAD...");

        ObjectId oldHead = this.currentHead.get();
        ObjectId newHead = this.head();

        try (Repository repository = this.git.getRepository()) {
            DiffFormatter diffFormatter = new DiffFormatter(System.out);
            diffFormatter.setRepository(repository);
            RevWalk walk = new RevWalk(repository);
            try {
                RevCommit fromCommit = walk.parseCommit(oldHead);
                RevCommit toCommit = walk.parseCommit(newHead);
                List<DiffEntry> list = diffFormatter.scan(fromCommit.getTree(), toCommit.getTree());

                list.forEach(diff -> {
                    log.info("[{}]\tnew={}\told={}", diff.getChangeType(), diff.getNewPath(), diff.getOldPath());
                    if (diff.getOldPath() != null) {
                        Path path = Paths
                                .get(gitProperties.getBaseDir().getAbsolutePath() + "/" + diff.getOldPath());
                        if (Entry.isPublic(path)) {
                            Long entryId = Entry.parseEntryId(path);
                            log.info("evict Entry({})", entryId);
                            this.entryCache.evict(entryId);
                        }
                    }
                    if (diff.getNewPath() != null) {
                        Path path = Paths
                                .get(gitProperties.getBaseDir().getAbsolutePath() + "/" + diff.getNewPath());
                        this.loadEntry(path).ifPresent(entry -> {
                            log.info("put Entry({})", entry.getEntryId());
                            this.entryCache.put(entry.getEntryId(), entry);
                        });
                    }
                });
            } finally {
                walk.dispose();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        this.currentHead.set(newHead);
    }

    ObjectId head() {
        try (Repository repository = this.git.getRepository()) {
            return repository.resolve("HEAD");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    Optional<Entry> loadEntry(Path path) {
        return Entry.loadFromFile(path).map(e -> {
            Pair<Author, Author> author = getAuthor(path);
            if (e.getFrontMatter() != null && e.getFrontMatter().getDate() != null) {
                // ignore created.date if it is set by frontMatter
                e.setCreated(new Author(author.getKey().getName(), e.getFrontMatter().getDate()));
            } else {
                e.setCreated(author.getKey());
            }
            if (e.getFrontMatter() != null && e.getFrontMatter().getUpdated() != null) {
                // ignore updated.date if it is set by frontMatter
                e.setUpdated(new Author(author.getValue().getName(), e.getFrontMatter().getUpdated()));
            } else {
                e.setUpdated(author.getValue());
            }
            return e;
        });
    }

    List<File> getContentFiles() {
        String contentsDir = gitProperties.getBaseDir().getAbsolutePath() + "/" + gitProperties.getContentDir();
        File[] files = new File(contentsDir).listFiles(f -> Entry.isPublic(f.toPath()));
        return files == null ? Collections.emptyList() : Arrays.asList(files);
    }

    Pair<Author, Author> getAuthor(Path path) {
        Path p = gitProperties.getBaseDir().toPath().relativize(path);
        try {
            Iterable<RevCommit> commits = git.log().addPath(p.toString().replace("\\", "/")).call();
            RevCommit updated = Iterables.getFirst(commits, null);
            RevCommit created = Iterables.getLast(commits, updated);
            return new Pair<>(author(created), author(updated));
        } catch (GitAPIException e) {
            throw new IllegalStateException(e);
        }
    }

    Author author(RevCommit commit) {
        String name = commit != null ? commit.getAuthorIdent().getName() : "";
        Date date = commit != null ? commit.getAuthorIdent().getWhen() : new Date();
        OffsetDateTime o = OffsetDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        return new Author(name, o);
    }

    Git getGitDirectory() {
        try {
            if (gitProperties.getBaseDir().exists()) {
                if (gitProperties.isInit()) {
                    FileSystemUtils.deleteRecursively(gitProperties.getBaseDir());
                } else {
                    return Git.open(gitProperties.getBaseDir());
                }
            }
            CloneCommand clone = Git.cloneRepository().setURI(gitProperties.getUri())
                    .setDirectory(gitProperties.getBaseDir());
            gitProperties.credentialsProvider().ifPresent(clone::setCredentialsProvider);
            return clone.call();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (GitAPIException e) {
            throw new IllegalStateException(e);
        }
    }

    @PostConstruct
    void load() {
        this.entryCache = new EntryEventFiringCache(this.cacheManager.getCache("entry"), this.eventManager);
        this.git = this.getGitDirectory();
        this.currentHead.set(this.head());
        this.pull().thenCompose(r -> this.forceRefreshTask.forceRefresh(this))
                .thenAccept(v -> this.eventManager.setState(AppState.INITIALIZED)).exceptionally(e -> {
                    log.error("error!", e);
                    return null;
                });
    }

    @PreDestroy
    void destroy() {
        this.git.close();
    }

    @Component
    @Slf4j
    public static class ForceRefreshTask {
        @Async
        public CompletableFuture<Void> forceRefresh(GitStore gitStore) {
            try {
                gitStore.forceRefreshAll();
                return CompletableFuture.completedFuture(null);
            } catch (Exception e) {
                CompletableFuture<Void> f = new CompletableFuture<>();
                f.completeExceptionally(e);
                return f;
            }
        }
    }

    @Component
    @Slf4j
    public static class GitPullTask {
        @Autowired
        GitProperties gitProperties;

        @Async
        public CompletableFuture<PullResult> pull(Git git) {
            log.info("git pull {}", gitProperties.getUri());
            try {
                PullCommand pull = git.pull();
                gitProperties.credentialsProvider().ifPresent(pull::setCredentialsProvider);
                return CompletableFuture.completedFuture(pull.call());
            } catch (GitAPIException e) {
                CompletableFuture<PullResult> f = new CompletableFuture<>();
                f.completeExceptionally(e);
                return f;
            }
        }
    }

    @Data
    public static class Pair<K, V> {
        private final K key;
        private final V value;
    }
}