/*
* Copyright 2015 Red Hat, Inc. 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 org.uberfire.java.nio.fs.jgit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.uberfire.java.nio.IOException;
import org.uberfire.java.nio.base.FileSystemId;
import org.uberfire.java.nio.base.FileSystemState;
import org.uberfire.java.nio.base.FileSystemStateAware;
import org.uberfire.java.nio.base.options.CommentedOption;
import org.uberfire.java.nio.file.ClosedWatchServiceException;
import org.uberfire.java.nio.file.FileStore;
import org.uberfire.java.nio.file.FileSystem;
import org.uberfire.java.nio.file.InterruptedException;
import org.uberfire.java.nio.file.InvalidPathException;
import org.uberfire.java.nio.file.Path;
import org.uberfire.java.nio.file.PathMatcher;
import org.uberfire.java.nio.file.PatternSyntaxException;
import org.uberfire.java.nio.file.WatchEvent;
import org.uberfire.java.nio.file.WatchKey;
import org.uberfire.java.nio.file.WatchService;
import org.uberfire.java.nio.file.Watchable;
import org.uberfire.java.nio.file.attribute.UserPrincipalLookupService;
import org.uberfire.java.nio.file.spi.FileSystemProvider;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static org.eclipse.jgit.lib.Repository.shortenRefName;
import static org.uberfire.commons.validation.PortablePreconditions.checkNotEmpty;
import static org.uberfire.commons.validation.PortablePreconditions.checkNotNull;
import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.branchList;
public class JGitFileSystem implements FileSystem,
FileSystemId,
FileSystemStateAware {
private static final Logger LOGGER = LoggerFactory.getLogger(JGitFileSystem.class);
private static final Set<String> SUPPORTED_ATTR_VIEWS = unmodifiableSet(new HashSet<String>(asList("basic",
"version")));
private final JGitFileSystemProvider provider;
private final Git gitRepo;
private final ListBranchCommand.ListMode listMode;
private final String toStringContent;
private final FileStore fileStore;
private final String name;
private final CredentialsProvider credential;
private final Map<WatchService, Queue<WatchKey>> events = new ConcurrentHashMap<WatchService, Queue<WatchKey>>();
private final Collection<WatchService> watchServices = new ArrayList<WatchService>();
private final AtomicInteger numberOfCommitsSinceLastGC = new AtomicInteger(0);
private final Lock lock = new Lock();
private boolean isClosed = false;
private FileSystemState state = FileSystemState.NORMAL;
private CommitInfo batchCommitInfo = null;
private Map<Path, Boolean> hadCommitOnBatchState = new ConcurrentHashMap<Path, Boolean>();
JGitFileSystem(final JGitFileSystemProvider provider,
final Map<String, String> fullHostNames,
final Git git,
final String name,
final CredentialsProvider credential) {
this(provider,
fullHostNames,
git,
name,
null,
credential);
}
JGitFileSystem(final JGitFileSystemProvider provider,
final Map<String, String> fullHostNames,
final Git git,
final String name,
final ListBranchCommand.ListMode listMode,
final CredentialsProvider credential) {
this.provider = checkNotNull("provider",
provider);
this.gitRepo = checkNotNull("git",
git);
this.name = checkNotEmpty("name",
name);
this.credential = checkNotNull("credential",
credential);
this.listMode = listMode;
this.fileStore = new JGitFileStore(gitRepo.getRepository());
if (fullHostNames != null && !fullHostNames.isEmpty()) {
final StringBuilder sb = new StringBuilder();
final Iterator<Map.Entry<String, String>> iterator = fullHostNames.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String, String> entry = iterator.next();
sb.append(entry.getKey()).append("://").append(entry.getValue()).append("/").append(name);
if (iterator.hasNext()) {
sb.append("\n");
}
}
toStringContent = sb.toString();
} else {
toStringContent = "git://" + name;
}
}
@Override
public String id() {
return name;
}
public String getName() {
return name;
}
public Git gitRepo() {
return gitRepo;
}
public CredentialsProvider getCredential() {
return credential;
}
@Override
public FileSystemProvider provider() {
return provider;
}
@Override
public boolean isOpen() {
return !isClosed;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public String getSeparator() {
return "/";
}
@Override
public Iterable<Path> getRootDirectories() {
checkClosed();
return new Iterable<Path>() {
@Override
public Iterator<Path> iterator() {
return new Iterator<Path>() {
Iterator<Ref> branches = null;
@Override
public boolean hasNext() {
if (branches == null) {
init();
}
return branches.hasNext();
}
private void init() {
branches = branchList(gitRepo,
listMode).iterator();
}
@Override
public Path next() {
if (branches == null) {
init();
}
try {
return JGitPathImpl.createRoot(JGitFileSystem.this,
"/",
shortenRefName(branches.next().getName()) + "@" + name,
false);
} catch (NoSuchElementException e) {
throw new IllegalStateException(
"The gitnio directory is in an invalid state. " +
"If you are an IntelliJ IDEA user, " +
"there is a known bug which requires specifying " +
"a custom directory for your git repository. " +
"You can specify a custom directory using '-Dorg.uberfire.nio.git.dir=/tmp/dir'. " +
"For more details please see https://issues.jboss.org/browse/UF-275.",
e);
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
@Override
public Iterable<FileStore> getFileStores() {
checkClosed();
return new Iterable<FileStore>() {
@Override
public Iterator<FileStore> iterator() {
return new Iterator<FileStore>() {
private int i = 0;
@Override
public boolean hasNext() {
return i < 1;
}
@Override
public FileStore next() {
if (i < 1) {
i++;
return fileStore;
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
@Override
public Set<String> supportedFileAttributeViews() {
checkClosed();
return SUPPORTED_ATTR_VIEWS;
}
@Override
public Path getPath(final String first,
final String... more)
throws InvalidPathException {
checkClosed();
if (first == null || first.trim().isEmpty()) {
return new JGitFSPath(this);
}
if (more == null || more.length == 0) {
return JGitPathImpl.create(this,
first,
JGitPathImpl.DEFAULT_REF_TREE + "@" + name,
false);
}
final StringBuilder sb = new StringBuilder();
for (final String segment : more) {
if (segment.length() > 0) {
if (sb.length() > 0) {
sb.append(getSeparator());
}
sb.append(segment);
}
}
return JGitPathImpl.create(this,
sb.toString(),
first + "@" + name,
false);
}
@Override
public PathMatcher getPathMatcher(final String syntaxAndPattern)
throws IllegalArgumentException, PatternSyntaxException, UnsupportedOperationException {
checkClosed();
checkNotEmpty("syntaxAndPattern",
syntaxAndPattern);
throw new UnsupportedOperationException();
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService()
throws UnsupportedOperationException {
checkClosed();
throw new UnsupportedOperationException();
}
@Override
public WatchService newWatchService()
throws UnsupportedOperationException, IOException {
checkClosed();
final WatchService ws = new WatchService() {
private boolean wsClose = false;
@Override
public WatchKey poll() throws ClosedWatchServiceException {
return events.get(this).poll();
}
@Override
public WatchKey poll(long timeout,
TimeUnit unit) throws ClosedWatchServiceException, org.uberfire.java.nio.file.InterruptedException {
return events.get(this).poll();
}
@Override
public synchronized WatchKey take() throws ClosedWatchServiceException, InterruptedException {
while (true) {
if (wsClose || isClosed) {
throw new ClosedWatchServiceException("This service is closed.");
} else if (events.get(this).size() > 0) {
return events.get(this).poll();
} else {
try {
this.wait();
} catch (final java.lang.InterruptedException e) {
}
}
}
}
@Override
public boolean isClose() {
return isClosed;
}
@Override
public synchronized void close() throws IOException {
wsClose = true;
notifyAll();
watchServices.remove(this);
}
@Override
public String toString() {
return "WatchService{" +
"FileSystem=" + JGitFileSystem.this.toString() +
'}';
}
};
events.put(ws,
new ConcurrentLinkedQueue<WatchKey>());
watchServices.add(ws);
return ws;
}
@Override
public void close() throws IOException {
if (isClosed) {
return;
}
gitRepo.getRepository().close();
isClosed = true;
try {
for (final WatchService ws : new ArrayList<WatchService>(watchServices)) {
try {
ws.close();
} catch (final Exception ex) {
LOGGER.error("Can't close watch service [" + toString() + "]",
ex);
}
}
watchServices.clear();
events.clear();
} catch (final Exception ex) {
LOGGER.error("Error during close of WatchServices [" + toString() + "]",
ex);
} finally {
provider.onCloseFileSystem(this);
}
}
private void checkClosed() throws IllegalStateException {
if (isClosed) {
throw new IllegalStateException("FileSystem is closed.");
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JGitFileSystem that = (JGitFileSystem) o;
if (fileStore != null ? !fileStore.equals(that.fileStore) : that.fileStore != null) {
return false;
}
if (!gitRepo.equals(that.gitRepo)) {
return false;
}
if (listMode != that.listMode) {
return false;
}
if (!name.equals(that.name)) {
return false;
}
if (!provider.equals(that.provider)) {
return false;
}
return true;
}
@Override
public String toString() {
return toStringContent;
}
@Override
public int hashCode() {
int result = provider.hashCode();
result = 31 * result + gitRepo.hashCode();
result = 31 * result + (listMode != null ? listMode.hashCode() : 0);
result = 31 * result + (fileStore != null ? fileStore.hashCode() : 0);
result = 31 * result + name.hashCode();
return result;
}
public void publishEvents(final Path watchable,
final List<WatchEvent<?>> elist) {
if (this.events.isEmpty()) {
return;
}
final WatchKey wk = new WatchKey() {
@Override
public boolean isValid() {
return true;
}
@Override
public List<WatchEvent<?>> pollEvents() {
return new ArrayList<WatchEvent<?>>(elist);
}
@Override
public boolean reset() {
return isOpen();
}
@Override
public void cancel() {
}
@Override
public Watchable watchable() {
return watchable;
}
};
for (final Map.Entry<WatchService, Queue<WatchKey>> watchServiceQueueEntry : events.entrySet()) {
watchServiceQueueEntry.getValue().add(wk);
final WatchService ws = watchServiceQueueEntry.getKey();
synchronized (ws) {
ws.notifyAll();
}
}
}
@Override
public void dispose() {
if (!isClosed) {
close();
}
provider.onDisposeFileSystem(this);
}
public boolean isOnBatch() {
return state.equals(FileSystemState.BATCH);
}
private CommitInfo buildCommitInfo(final String defaultMessage,
final CommentedOption op) {
String sessionId = null;
String name = null;
String email = null;
String message = defaultMessage;
TimeZone timeZone = null;
Date when = null;
if (op != null) {
sessionId = op.getSessionId();
name = op.getName();
email = op.getEmail();
if (op.getMessage() != null && !op.getMessage().trim().isEmpty()) {
message = op.getMessage();
}
timeZone = op.getTimeZone();
when = op.getWhen();
}
return new CommitInfo(sessionId,
name,
email,
message,
timeZone,
when);
}
public void setBatchCommitInfo(final String defaultMessage,
final CommentedOption op) {
this.batchCommitInfo = buildCommitInfo(defaultMessage,
op);
}
public void setHadCommitOnBatchState(final Path path,
final boolean hadCommitOnBatchState) {
final Path root = checkNotNull("path",
path).getRoot();
this.hadCommitOnBatchState.put(root.getRoot(),
hadCommitOnBatchState);
}
public void setHadCommitOnBatchState(final boolean value) {
for (Map.Entry<Path, Boolean> entry : hadCommitOnBatchState.entrySet()) {
entry.setValue(value);
}
}
public boolean isHadCommitOnBatchState(final Path path) {
final Path root = checkNotNull("path",
path).getRoot();
return hadCommitOnBatchState.containsKey(root) ? hadCommitOnBatchState.get(root) : false;
}
public CommitInfo getBatchCommitInfo() {
return batchCommitInfo;
}
public void setBatchCommitInfo(CommitInfo batchCommitInfo) {
this.batchCommitInfo = batchCommitInfo;
}
public int incrementAndGetCommitCount() {
return numberOfCommitsSinceLastGC.incrementAndGet();
}
public void resetCommitCount() {
numberOfCommitsSinceLastGC.set(0);
}
int getNumberOfCommitsSinceLastGC() {
return numberOfCommitsSinceLastGC.get();
}
@Override
public FileSystemState getState() {
return state;
}
public void setState(String state) {
try {
this.state = FileSystemState.valueOf(state);
} catch (final Exception ex) {
this.state = FileSystemState.NORMAL;
}
}
public void lock() {
try {
lock.lock();
} catch (java.lang.InterruptedException e) {
}
}
public void unlock() {
lock.unlock();
}
private static class Lock {
private final AtomicBoolean isLocked = new AtomicBoolean(false);
public synchronized void lock() throws java.lang.InterruptedException {
while (!isLocked.compareAndSet(false,
true)) {
wait();
}
}
public synchronized void unlock() {
isLocked.set(false);
notifyAll();
}
}
}