All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.config.git.GitConfigSource Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2017, 2023 Oracle and/or its affiliates.
 *
 * 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 io.helidon.config.git;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import io.helidon.common.media.type.MediaType;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.AbstractConfigSource;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.FileSourceHelper;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParser.Content;
import io.helidon.config.spi.ParsableSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;

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.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;

import static java.util.Collections.singleton;

/**
 * A config source which loads a configuration document from Git repository.
 * 

* Config source is initialized by {@link GitConfigSourceBuilder}. */ public class GitConfigSource extends AbstractConfigSource implements ParsableSource, PollableSource, AutoCloseable { private static final System.Logger LOGGER = System.getLogger(GitConfigSource.class.getName()); private final URI uri; private final String branch; private Path directory; private Path targetPath; private Repository repository; private GitConfigSourceBuilder.GitEndpoint endpoint; private boolean isTempDirectory = false; private boolean isClosed = false; private final List gits = Collections.synchronizedList(new ArrayList<>()); /** * Initializes config source from builder. * * @param builder builder to be initialized from */ GitConfigSource(GitConfigSourceBuilder builder, GitConfigSourceBuilder.GitEndpoint endpoint) { super(builder); this.endpoint = endpoint; this.uri = endpoint.uri(); this.branch = endpoint.branch(); if (endpoint.directory() == null) { if (uri == null) { throw new ConfigException("Directory or Uri must be set."); } try { this.directory = Files.createTempDirectory("helidon-config-git-source-"); isTempDirectory = true; } catch (IOException e) { throw new ConfigException("Cannot create temporary directory.", e); } } else { this.directory = endpoint.directory(); } } @Override public void init(ConfigContext context) { try { init(); targetPath = directory.resolve(endpoint.path()); } catch (IOException | GitAPIException | JGitInternalException e) { throw new ConfigException(String.format("Cannot initialize repository '%s' in local temp dir %s", uri.toASCIIString(), directory.toString()), e); } } /** * Create an instance from meta configuration. * * @param metaConfig meta configuration of this source * @return config source configured from the meta configuration */ public static GitConfigSource create(Config metaConfig) { return builder().config(metaConfig).build(); } /** * Create a fluent API builder for GIT config source. * * @return a new builder instance */ public static GitConfigSourceBuilder builder() { return new GitConfigSourceBuilder(); } @Override protected String uid() { StringBuilder sb = new StringBuilder(); if (endpoint.directory() != null) { sb.append(endpoint.directory()); } if (endpoint.uri() != null && endpoint.directory() != null) { sb.append('|'); } if (endpoint.uri() != null) { sb.append(endpoint.uri().toASCIIString()); } sb.append('#'); sb.append(endpoint.path()); return sb.toString(); } @Override public Optional parser() { return super.parser(); } @Override public Optional pollingStrategy() { return super.pollingStrategy(); } @Override public boolean isModified(byte[] stamp) { try { pull(); } catch (GitAPIException e) { LOGGER.log(Level.WARNING, "Pull failed.", e); } return FileSourceHelper.isModified(targetPath, stamp); } @Override public Optional load() throws ConfigException { if (!Files.exists(targetPath)) { return Optional.empty(); } return FileSourceHelper.readDataAndDigest(targetPath) .map(dad -> Content.builder() .data(new ByteArrayInputStream(dad.data())) .stamp(dad.digest()) .mediaType(MediaTypes.detectType(targetPath)) .build()); } @Override public Function> relativeResolver() { return it -> { Path path = targetPath.getParent().resolve(it); if (Files.exists(path) && Files.isReadable(path) && !Files.isDirectory(path)) { try { return Optional.of(Files.newInputStream(path)); } catch (IOException e) { throw new ConfigException("Failed to read configuration from path: " + path.toAbsolutePath(), e); } } else { return Optional.empty(); } }; } @Override public Optional mediaType() { return super.mediaType(); } private void init() throws IOException, GitAPIException { if (!Files.exists(directory)) { throw new ConfigException(String.format("Directory '%s' does not exist.", directory.toString())); } if (!Files.isDirectory(directory)) { throw new ConfigException(String.format("'%s' is not a directory.", directory.toString())); } if (!Files.isReadable(directory) || !Files.isWritable(directory)) { throw new ConfigException(String.format("Directory '%s' is not accessible.", directory.toString())); } try (DirectoryStream dirStream = Files.newDirectoryStream(directory)) { if (dirStream.iterator().hasNext()) { try { recordGit(Git.open(directory.toFile())); } catch (IOException e) { throw new ConfigException( String.format("Directory '%s' is not empty and it is not a valid repository.", directory.toString())); } } else if (uri != null) { CloneCommand cloneCommand = Git.cloneRepository() .setCredentialsProvider(endpoint.credentialsProvider()) .setURI(uri.toASCIIString()) .setBranchesToClone(singleton("refs/heads/" + branch)) .setBranch("refs/heads/" + branch) .setDirectory(directory.toFile()); Git cloneResult = recordGit(cloneCommand.call()); LOGGER.log(Level.DEBUG, () -> String.format("git clone result: %s", cloneResult.toString())); } } repository = new FileRepositoryBuilder() .setGitDir(directory.resolve(".git").toFile()) .build(); // make sure we have the latest data before we start using this config source pull(); } private void pull() throws GitAPIException { Git git = recordGit(Git.wrap(repository)); PullCommand pull = git.pull() .setCredentialsProvider(endpoint.credentialsProvider()) .setRebase(true); PullResult result = pull.call(); if (!result.isSuccessful()) { LOGGER.log(Level.WARNING, () -> String.format("Cannot pull from git '%s', branch '%s'", uri.toASCIIString(), branch)); if (LOGGER.isLoggable(Level.TRACE)) { Status status = git.status().call(); LOGGER.log(Level.TRACE, () -> "git status cleanliness: " + status.isClean()); if (!status.isClean()) { LOGGER.log(Level.TRACE, () -> "git status uncommitted changes: " + status.getUncommittedChanges()); LOGGER.log(Level.TRACE, () -> "git status untracked: " + status.getUntracked()); } } } else { LOGGER.log(Level.DEBUG, "Pull was successful."); } LOGGER.log(Level.TRACE, () -> "git rebase result: " + result.getRebaseResult().getStatus().name()); LOGGER.log(Level.TRACE, () -> "git fetch result: " + result.getFetchResult().getMessages()); } GitConfigSourceBuilder.GitEndpoint gitEndpoint() { return endpoint; } private Git recordGit(Git git) { gits.add(git); return git; } @Override public void close() throws IOException { if (!isClosed) { try { if (repository != null) { repository.close(); } closeGits(); if (isTempDirectory) { deleteTempDirectory(); } } finally { isClosed = true; } } } private void closeGits() { gits.forEach(Git::close); } private void deleteTempDirectory() throws IOException { LOGGER.log(Level.DEBUG, () -> String.format("GitConfigSource deleting temp directory %s", directory.toString())); Files.walkFileTree(directory, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (!Files.isWritable(file)) { //When you try to delete the file on Windows and it is marked as read-only //it would fail unless this change file.toFile().setWritable(true); } Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy