diff --git a/pom.xml b/pom.xml
index 5e67c117db..3295e732db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -305,7 +305,7 @@
org.mockito
- mockito-all
+ mockito-core
${mockito.version}
test
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
index f7b4d75fbd..270b996635 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/CatCommandBuilder.java
@@ -33,24 +33,19 @@
package sonia.scm.repository.api;
-//~--- non-JDK imports --------------------------------------------------------
-
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.spi.CatCommand;
import sonia.scm.repository.spi.CatCommandRequest;
import sonia.scm.util.IOUtil;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
/**
@@ -106,23 +101,31 @@ public final class CatCommandBuilder
}
/**
- * Passes the content of the given file to the outputstream.
+ * Passes the content of the given file to the output stream.
*
- * @param outputStream outputstream for the content
+ * @param outputStream output stream for the content
* @param path file path
- *
- * @return {@code this}
- *
- * @throws IOException
- * @throws RepositoryException
*/
- public CatCommandBuilder retriveContent(OutputStream outputStream,
- String path)
- throws IOException, RepositoryException
- {
+ public void retriveContent(OutputStream outputStream, String path) throws IOException, RepositoryException {
getCatResult(outputStream, path);
+ }
- return this;
+ /**
+ * Returns an output stream with the file content.
+ *
+ * @param path file path
+ */
+ public InputStream getStream(String path) throws IOException, RepositoryException {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
+ "path is required");
+
+ CatCommandRequest requestClone = request.clone();
+
+ requestClone.setPath(path);
+
+ logger.debug("create cat stream for {}", requestClone);
+
+ return catCommand.getCatResultStream(requestClone);
}
//~--- get methods ----------------------------------------------------------
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
index b9a6f52ab0..e0e336f2ff 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/CatCommand.java
@@ -33,13 +33,10 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import sonia.scm.repository.RepositoryException;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
/**
@@ -47,19 +44,9 @@ import java.io.OutputStream;
* @author Sebastian Sdorra
* @since 1.17
*/
-public interface CatCommand
-{
+public interface CatCommand {
- /**
- * Method description
- *
- *
- * @param request
- * @param output
- *
- * @throws IOException
- * @throws RepositoryException
- */
- public void getCatResult(CatCommandRequest request, OutputStream output)
- throws IOException, RepositoryException;
+ void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException;
+
+ InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException;
}
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
new file mode 100644
index 0000000000..a461e40dea
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java
@@ -0,0 +1,67 @@
+package sonia.scm.it;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeFalse;
+import static sonia.scm.it.RestUtil.given;
+import static sonia.scm.it.ScmTypes.availableScmTypes;
+
+@RunWith(Parameterized.class)
+public class RepositoryAccessITCase {
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private final String repositoryType;
+ private RepositoryUtil repositoryUtil;
+
+ public RepositoryAccessITCase(String repositoryType) {
+ this.repositoryType = repositoryType;
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection createParameters() {
+ return availableScmTypes();
+ }
+
+ @Before
+ public void initClient() throws IOException {
+ TestData.createDefault();
+ repositoryUtil = new RepositoryUtil(repositoryType, tempFolder.getRoot());
+ }
+
+ @Test
+ public void shouldFindBranches() throws IOException {
+ assumeFalse("There are no branches for SVN", repositoryType.equals("svn"));
+
+ repositoryUtil.createAndCommitFile("a.txt", "a");
+
+ String branchesUrl = given()
+ .when()
+ .get(TestData.getDefaultRepositoryUrl(repositoryType))
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .path("_links.branches.href");
+
+ Object branchName = given()
+ .when()
+ .get(branchesUrl)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .path("_embedded.branches[0].name");
+
+ assertNotNull(branchName);
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java
new file mode 100644
index 0000000000..98d4c8cdab
--- /dev/null
+++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java
@@ -0,0 +1,62 @@
+package sonia.scm.it;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.apache.http.HttpStatus;
+import sonia.scm.repository.Changeset;
+import sonia.scm.repository.Person;
+import sonia.scm.repository.client.api.ClientCommand;
+import sonia.scm.repository.client.api.RepositoryClient;
+import sonia.scm.repository.client.api.RepositoryClientFactory;
+import sonia.scm.web.VndMediaType;
+
+import java.io.File;
+import java.io.IOException;
+
+import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
+import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
+import static sonia.scm.it.RestUtil.given;
+
+public class RepositoryUtil {
+
+ private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
+
+ private final RepositoryClient repositoryClient;
+ private final File folder;
+
+ RepositoryUtil(String repositoryType, File folder) throws IOException {
+ this.repositoryClient = createRepositoryClient(repositoryType, folder);
+ this.folder = folder;
+ }
+
+ static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
+ String httpProtocolUrl = given(VndMediaType.REPOSITORY)
+
+ .when()
+ .get(TestData.getDefaultRepositoryUrl(repositoryType))
+
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract()
+ .path("_links.httpProtocol.href");
+
+
+ return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, ADMIN_USERNAME, ADMIN_PASSWORD, folder);
+ }
+
+ void createAndCommitFile(String fileName, String content) throws IOException {
+ Files.write(content, new File(folder, fileName), Charsets.UTF_8);
+ repositoryClient.getAddCommand().add(fileName);
+ commit("added " + fileName);
+ }
+
+ Changeset commit(String message) throws IOException {
+ Changeset changeset = repositoryClient.getCommitCommand().commit(
+ new Person("scmadmin", "scmadmin@scm-manager.org"), message
+ );
+ if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
+ repositoryClient.getPushCommand().push();
+ }
+ return changeset;
+ }
+}
diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java
index 1458ab6d0b..76d2461637 100644
--- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java
+++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java
@@ -16,10 +16,18 @@ public class RestUtil {
return REST_BASE_URL.resolve(path);
}
+ public static final String ADMIN_USERNAME = "scmadmin";
+ public static final String ADMIN_PASSWORD = "scmadmin";
+
public static RequestSpecification given(String mediaType) {
return RestAssured.given()
.contentType(mediaType)
.accept(mediaType)
- .auth().preemptive().basic("scmadmin", "scmadmin");
+ .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD);
+ }
+
+ public static RequestSpecification given() {
+ return RestAssured.given()
+ .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD);
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
index 4420b9a22a..c451dd8614 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java
@@ -32,158 +32,133 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Strings;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
+import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.util.Util;
-//~--- JDK imports ------------------------------------------------------------
-
+import java.io.Closeable;
+import java.io.FilterInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class GitCatCommand extends AbstractGitCommand implements CatCommand
-{
- /**
- * the logger for GitCatCommand
- */
- private static final Logger logger =
- LoggerFactory.getLogger(GitCatCommand.class);
+public class GitCatCommand extends AbstractGitCommand implements CatCommand {
- //~--- constructors ---------------------------------------------------------
+ private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class);
- /**
- * Constructs ...
- *
- *
- *
- * @param context
- * @param repository
- */
- public GitCatCommand(GitContext context,
- sonia.scm.repository.Repository repository)
- {
+ public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) {
super(context, repository);
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param request
- * @param output
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Override
- public void getCatResult(CatCommandRequest request, OutputStream output)
- throws IOException, RepositoryException
- {
+ public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException {
logger.debug("try to read content for {}", request);
-
- org.eclipse.jgit.lib.Repository repo = open();
-
- ObjectId revId = getCommitOrDefault(repo, request.getRevision());
- getContent(repo, revId, request.getPath(), output);
+ try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) {
+ closableObjectLoaderContainer.objectLoader.copyTo(output);
+ }
}
- /**
- * Method description
- *
- *
- *
- * @param repo
- * @param revId
- * @param path
- * @param output
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
- void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId,
- String path, OutputStream output)
- throws IOException, RepositoryException
- {
- TreeWalk treeWalk = null;
- RevWalk revWalk = null;
+ @Override
+ public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException {
+ logger.debug("try to read content for {}", request);
+ return new InputStreamWrapper(getLoader(request));
+ }
- try
- {
- treeWalk = new TreeWalk(repo);
- treeWalk.setRecursive(Util.nonNull(path).contains("/"));
-
- if (logger.isDebugEnabled())
- {
- logger.debug("load content for {} at {}", path, revId.name());
- }
-
- revWalk = new RevWalk(repo);
-
- RevCommit entry = revWalk.parseCommit(revId);
- RevTree revTree = entry.getTree();
-
- if (revTree != null)
- {
- treeWalk.addTree(revTree);
- }
- else
- {
- logger.error("could not find tree for {}", revId.name());
- }
-
- treeWalk.setFilter(PathFilter.create(path));
-
- if (treeWalk.next())
- {
-
- // Path exists
- if (treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB)
- {
- ObjectId blobId = treeWalk.getObjectId(0);
- ObjectLoader loader = repo.open(blobId);
-
- loader.copyTo(output);
- }
- else
- {
-
- // Not a blob, its something else (tree, gitlink)
- throw new PathNotFoundException(path);
- }
- }
- else
- {
- throw new PathNotFoundException(path);
- }
+ void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException, RepositoryException {
+ try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) {
+ closableObjectLoaderContainer.objectLoader.copyTo(output);
}
- finally
- {
+ }
+
+ private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException, RepositoryException {
+ org.eclipse.jgit.lib.Repository repo = open();
+ ObjectId revId = getCommitOrDefault(repo, request.getRevision());
+ return getLoader(repo, revId, request.getPath());
+ }
+
+ private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException, RepositoryException {
+ TreeWalk treeWalk = new TreeWalk(repo);
+ treeWalk.setRecursive(Util.nonNull(path).contains("/"));
+
+ logger.debug("load content for {} at {}", path, revId.name());
+
+ RevWalk revWalk = new RevWalk(repo);
+
+ RevCommit entry = null;
+ try {
+ entry = revWalk.parseCommit(revId);
+ } catch (MissingObjectException e) {
+ throw new RevisionNotFoundException(revId.getName());
+ }
+ RevTree revTree = entry.getTree();
+
+ if (revTree != null) {
+ treeWalk.addTree(revTree);
+ } else {
+ logger.error("could not find tree for {}", revId.name());
+ }
+
+ treeWalk.setFilter(PathFilter.create(path));
+
+ if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
+ ObjectId blobId = treeWalk.getObjectId(0);
+ ObjectLoader loader = repo.open(blobId);
+
+ return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk);
+ } else {
+ throw new PathNotFoundException(path);
+ }
+ }
+
+ private static class ClosableObjectLoaderContainer implements Closeable {
+ private final ObjectLoader objectLoader;
+ private final TreeWalk treeWalk;
+ private final RevWalk revWalk;
+
+ private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) {
+ this.objectLoader = objectLoader;
+ this.treeWalk = treeWalk;
+ this.revWalk = revWalk;
+ }
+
+ @Override
+ public void close() {
GitUtil.release(revWalk);
GitUtil.release(treeWalk);
}
}
+
+ private static class InputStreamWrapper extends FilterInputStream {
+
+ private final ClosableObjectLoaderContainer container;
+
+ private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException {
+ super(container.objectLoader.openStream());
+ this.container = container;
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ } finally {
+ container.close();
+ }
+ }
+ }
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
index ede6a53429..c23db0873b 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java
@@ -32,19 +32,17 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
-
+import sonia.scm.repository.GitConstants;
+import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
-
-import static org.junit.Assert.*;
-
-//~--- JDK imports ------------------------------------------------------------
+import sonia.scm.repository.RevisionNotFoundException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import sonia.scm.repository.GitConstants;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
/**
* Unit tests for {@link GitCatCommand}.
@@ -53,15 +51,8 @@ import sonia.scm.repository.GitConstants;
*
* @author Sebastian Sdorra
*/
-public class GitCatCommandTest extends AbstractGitCommandTestBase
-{
+public class GitCatCommandTest extends AbstractGitCommandTestBase {
- /**
- * Tests cat command with default branch.
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
public void testDefaultBranch() throws IOException, RepositoryException {
// without default branch, the repository head should be used
@@ -75,16 +66,8 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase
assertEquals("a and b", execute(request));
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testCat() throws IOException, RepositoryException
- {
+ public void testCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt");
@@ -92,36 +75,46 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase
assertEquals("a and b", execute(request));
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testSimpleCat() throws IOException, RepositoryException
- {
+ public void testSimpleCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt");
assertEquals("b", execute(request));
}
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- *
- * @throws IOException
- * @throws RepositoryException
- */
- private String execute(CatCommandRequest request)
- throws IOException, RepositoryException
- {
+ @Test(expected = PathNotFoundException.class)
+ public void testUnknownFile() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setPath("unknown");
+ execute(request);
+ }
+
+ @Test(expected = RevisionNotFoundException.class)
+ public void testUnknownRevision() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setRevision("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ request.setPath("a.txt");
+ execute(request);
+ }
+
+ @Test
+ public void testSimpleStream() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+ request.setPath("b.txt");
+
+ InputStream catResultStream = new GitCatCommand(createContext(), repository).getCatResultStream(request);
+
+ assertEquals('b', catResultStream.read());
+ assertEquals('\n', catResultStream.read());
+ assertEquals(-1, catResultStream.read());
+
+ catResultStream.close();
+ }
+
+ private String execute(CatCommandRequest request) throws IOException, RepositoryException {
String content = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java
index 76438c1f1e..c0ae0b51b5 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgCatCommand.java
@@ -33,71 +33,44 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
+import com.aragost.javahg.commands.ExecutionException;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
-
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.web.HgUtil;
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class HgCatCommand extends AbstractCommand implements CatCommand
-{
+public class HgCatCommand extends AbstractCommand implements CatCommand {
- /**
- * Constructs ...
- *
- *
- * @param context
- * @param repository
- */
- HgCatCommand(HgCommandContext context, Repository repository)
- {
+ HgCatCommand(HgCommandContext context, Repository repository) {
super(context, repository);
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param request
- * @param output
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Override
- public void getCatResult(CatCommandRequest request, OutputStream output)
- throws IOException, RepositoryException
- {
+ public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException, RepositoryException {
+ InputStream input = getCatResultStream(request);
+ try {
+ ByteStreams.copy(input, output);
+ } finally {
+ Closeables.close(input, true);
+ }
+ }
+
+ @Override
+ public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException {
com.aragost.javahg.commands.CatCommand cmd =
com.aragost.javahg.commands.CatCommand.on(open());
cmd.rev(HgUtil.getRevision(request.getRevision()));
- InputStream input = null;
-
- try
- {
- input = cmd.execute(request.getPath());
- ByteStreams.copy(input, output);
- }
- finally
- {
- Closeables.close(input, true);
+ try {
+ return cmd.execute(request.getPath());
+ } catch (ExecutionException e) {
+ throw new RepositoryException(e);
}
}
}
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java
index 49ef283c1a..afb5249b25 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgCatCommandTest.java
@@ -33,36 +33,19 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
-
import sonia.scm.repository.RepositoryException;
-import static org.junit.Assert.*;
-
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class HgCatCommandTest extends AbstractHgCommandTestBase
-{
+import static org.junit.Assert.assertEquals;
+
+public class HgCatCommandTest extends AbstractHgCommandTestBase {
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testCat() throws IOException, RepositoryException
- {
+ public void testCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt");
@@ -70,48 +53,48 @@ public class HgCatCommandTest extends AbstractHgCommandTestBase
assertEquals("a", execute(request));
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testSimpleCat() throws IOException, RepositoryException
- {
+ public void testSimpleCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("b.txt");
assertEquals("b", execute(request));
}
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- *
- * @throws IOException
- * @throws RepositoryException
- */
- private String execute(CatCommandRequest request)
- throws IOException, RepositoryException
- {
- String content = null;
+ @Test(expected = RepositoryException.class)
+ public void testUnknownFile() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setPath("unknown");
+ execute(request);
+ }
+
+ @Test(expected = RepositoryException.class)
+ public void testUnknownRevision() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setRevision("abc");
+ request.setPath("a.txt");
+ execute(request);
+ }
+
+ @Test
+ public void testSimpleStream() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+ request.setPath("b.txt");
+
+ InputStream catResultStream = new HgCatCommand(cmdContext, repository).getCatResultStream(request);
+
+ assertEquals('b', catResultStream.read());
+ assertEquals('\n', catResultStream.read());
+ assertEquals(-1, catResultStream.read());
+
+ catResultStream.close();
+ }
+
+ private String execute(CatCommandRequest request) throws IOException, RepositoryException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
- try
- {
- new HgCatCommand(cmdContext, repository).getCatResult(request, baos);
- }
- finally
- {
- content = baos.toString().trim();
- }
-
- return content;
+ new HgCatCommand(cmdContext, repository).getCatResult(request, baos);
+ return baos.toString().trim();
}
}
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java
index af08ea929e..4a8b907c29 100644
--- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnCatCommand.java
@@ -37,22 +37,26 @@ package sonia.scm.repository.spi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
+import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.admin.SVNLookClient;
-
+import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
+import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.SvnUtil;
-//~--- JDK imports ------------------------------------------------------------
-
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -120,6 +124,16 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand
}
}
+ @Override
+ public InputStream getCatResultStream(CatCommandRequest request) throws IOException, RepositoryException {
+ // There seems to be no method creating an input stream as a result, so
+ // we have no other possibility then to copy the content into a buffer and
+ // stream it from there.
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ getCatResult(request, output);
+ return new ByteArrayInputStream(output.toByteArray());
+ }
+
/**
* Method description
*
@@ -146,6 +160,17 @@ public class SvnCatCommand extends AbstractSvnCommand implements CatCommand
}
catch (SVNException ex)
{
+ handleSvnException(request, ex);
+ }
+ }
+
+ private void handleSvnException(CatCommandRequest request, SVNException ex) throws RepositoryException {
+ int svnErrorCode = ex.getErrorMessage().getErrorCode().getCode();
+ if (SVNErrorCode.FS_NOT_FOUND.getCode() == svnErrorCode) {
+ throw new PathNotFoundException(request.getPath());
+ } else if (SVNErrorCode.FS_NO_SUCH_REVISION.getCode() == svnErrorCode) {
+ throw new RevisionNotFoundException(request.getRevision());
+ } else {
throw new RepositoryException("could not get content from revision", ex);
}
}
diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java
index 198a55edf3..f7e2123bad 100644
--- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java
+++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnCatCommandTest.java
@@ -32,36 +32,23 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.junit.Test;
-
+import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
-
-import static org.junit.Assert.*;
-
-//~--- JDK imports ------------------------------------------------------------
+import sonia.scm.repository.RevisionNotFoundException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
-/**
- *
- * @author Sebastian Sdorra
- */
-public class SvnCatCommandTest extends AbstractSvnCommandTestBase
-{
+import static org.junit.Assert.assertEquals;
+
+//~--- JDK imports ------------------------------------------------------------
+
+public class SvnCatCommandTest extends AbstractSvnCommandTestBase {
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testCat() throws IOException, RepositoryException
- {
+ public void testCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("a.txt");
@@ -69,36 +56,50 @@ public class SvnCatCommandTest extends AbstractSvnCommandTestBase
assertEquals("a", execute(request));
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws RepositoryException
- */
@Test
- public void testSimpleCat() throws IOException, RepositoryException
- {
+ public void testSimpleCat() throws IOException, RepositoryException {
CatCommandRequest request = new CatCommandRequest();
request.setPath("c/d.txt");
assertEquals("d", execute(request));
}
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- *
- * @throws IOException
- * @throws RepositoryException
- */
- private String execute(CatCommandRequest request)
- throws IOException, RepositoryException
- {
+ @Test(expected = PathNotFoundException.class)
+ public void testUnknownFile() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setPath("unknown");
+ request.setRevision("1");
+
+ execute(request);
+ }
+
+ @Test(expected = RevisionNotFoundException.class)
+ public void testUnknownRevision() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+
+ request.setPath("a.txt");
+ request.setRevision("42");
+
+ execute(request);
+ }
+
+ @Test
+ public void testSimpleStream() throws IOException, RepositoryException {
+ CatCommandRequest request = new CatCommandRequest();
+ request.setPath("a.txt");
+ request.setRevision("1");
+
+ InputStream catResultStream = new SvnCatCommand(createContext(), repository).getCatResultStream(request);
+
+ assertEquals('a', catResultStream.read());
+ assertEquals('\n', catResultStream.read());
+ assertEquals(-1, catResultStream.read());
+
+ catResultStream.close();
+ }
+
+ private String execute(CatCommandRequest request) throws IOException, RepositoryException {
String content = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index c622042061..0b0ad145c6 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -226,7 +226,18 @@
compiler
${mustache.version}
-
+
+
+ com.github.sdorra
+ spotter-core
+ 1.1.0
+
+
+
+ org.apache.tika
+ tika-core
+ 1.18
+
diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java
index 234aae4015..73f59f14da 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java
@@ -35,26 +35,20 @@ package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
-import com.google.common.io.Closeables;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.repository.PathNotFoundException;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RevisionNotFoundException;
import sonia.scm.repository.api.CatCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.io.IOException;
-import java.io.OutputStream;
+import sonia.scm.util.IOUtil;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
-import sonia.scm.util.IOUtil;
+import java.io.IOException;
+import java.io.OutputStream;
/**
*
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java
new file mode 100644
index 0000000000..0936a5b4a0
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java
@@ -0,0 +1,154 @@
+package sonia.scm.api.v2.resources;
+
+import com.github.sdorra.spotter.ContentType;
+import com.github.sdorra.spotter.ContentTypes;
+import com.webcohesion.enunciate.metadata.rs.ResponseCode;
+import com.webcohesion.enunciate.metadata.rs.StatusCodes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.PathNotFoundException;
+import sonia.scm.repository.RepositoryException;
+import sonia.scm.repository.RepositoryNotFoundException;
+import sonia.scm.repository.RevisionNotFoundException;
+import sonia.scm.repository.api.RepositoryService;
+import sonia.scm.repository.api.RepositoryServiceFactory;
+import sonia.scm.util.IOUtil;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+public class ContentResource {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ContentResource.class);
+ private static final int HEAD_BUFFER_SIZE = 1024;
+
+ private final RepositoryServiceFactory serviceFactory;
+
+ @Inject
+ public ContentResource(RepositoryServiceFactory serviceFactory) {
+ this.serviceFactory = serviceFactory;
+ }
+
+ /**
+ * Returns the content of a file for the given revision in the repository. The content type depends on the file
+ * content and can be discovered calling HEAD on the same URL. If a programming languge could be
+ * recognized, this will be given in the header Language.
+ *
+ * @param namespace the namespace of the repository
+ * @param name the name of the repository
+ * @param revision the revision
+ * @param path The path of the file
+ *
+ */
+ @GET
+ @Path("{revision}/{path: .*}")
+ @StatusCodes({
+ @ResponseCode(code = 200, condition = "success"),
+ @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
+ @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"),
+ @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"),
+ @ResponseCode(code = 500, condition = "internal server error")
+ })
+ public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
+ try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
+ StreamingOutput stream = createStreamingOutput(namespace, name, revision, path, repositoryService);
+ Response.ResponseBuilder responseBuilder = Response.ok(stream);
+ return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
+ } catch (RepositoryNotFoundException e) {
+ LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ }
+
+ private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path, RepositoryService repositoryService) {
+ return os -> {
+ try {
+ repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path);
+ os.close();
+ } catch (PathNotFoundException e) {
+ LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
+ throw new WebApplicationException(Status.NOT_FOUND);
+ } catch (RepositoryException e) {
+ LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e);
+ throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
+ }
+ };
+ }
+
+ /**
+ * Returns the content type and the programming language (if it can be detected) of a file for the given revision in
+ * the repository. The programming language will be given in the header Language.
+ *
+ * @param namespace the namespace of the repository
+ * @param name the name of the repository
+ * @param revision the revision
+ * @param path The path of the file
+ *
+ */
+ @HEAD
+ @Path("{revision}/{path: .*}")
+ @StatusCodes({
+ @ResponseCode(code = 200, condition = "success"),
+ @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
+ @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"),
+ @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"),
+ @ResponseCode(code = 500, condition = "internal server error")
+ })
+ public Response metadata(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
+ try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
+ Response.ResponseBuilder responseBuilder = Response.ok();
+ return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
+ } catch (RepositoryNotFoundException e) {
+ LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ }
+
+ private Response createContentHeader(String namespace, String name, String revision, String path, RepositoryService repositoryService, Response.ResponseBuilder responseBuilder) {
+ try {
+ appendContentHeader(path, getHead(revision, path, repositoryService), responseBuilder);
+ } catch (PathNotFoundException e) {
+ LOG.debug("path '{}' not found in repository {}/{}", path, namespace, name, e);
+ return Response.status(Status.NOT_FOUND).build();
+ } catch (RevisionNotFoundException e) {
+ LOG.debug("revision '{}' not found in repository {}/{}", revision, namespace, name, e);
+ return Response.status(Status.NOT_FOUND).build();
+ } catch (IOException | RepositoryException e) {
+ LOG.info("error reading repository resource {} from {}/{}", path, namespace, name, e);
+ return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
+ }
+ return responseBuilder.build();
+ }
+
+ private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) {
+ ContentType contentType = ContentTypes.detect(path, head);
+ responseBuilder.header("Content-Type", contentType.getRaw());
+ contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language));
+ }
+
+ private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, RepositoryException {
+ InputStream stream = repositoryService.getCatCommand().setRevision(revision).getStream(path);
+ try {
+ byte[] buffer = new byte[HEAD_BUFFER_SIZE];
+ int length = stream.read(buffer);
+ if (length < buffer.length) {
+ return Arrays.copyOf(buffer, length);
+ } else {
+ return buffer;
+ }
+ } finally {
+ IOUtil.close(stream);
+ }
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
index 43aa6de608..817eb29f11 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java
@@ -35,6 +35,7 @@ public class RepositoryResource {
private final Provider branchRootResource;
private final Provider changesetRootResource;
private final Provider sourceRootResource;
+ private final Provider contentResource;
private final Provider permissionRootResource;
@Inject
@@ -44,7 +45,7 @@ public class RepositoryResource {
Provider tagRootResource,
Provider branchRootResource,
Provider changesetRootResource,
- Provider sourceRootResource, Provider permissionRootResource) {
+ Provider sourceRootResource, Provider contentResource, Provider permissionRootResource) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -53,6 +54,7 @@ public class RepositoryResource {
this.branchRootResource = branchRootResource;
this.changesetRootResource = changesetRootResource;
this.sourceRootResource = sourceRootResource;
+ this.contentResource = contentResource;
this.permissionRootResource = permissionRootResource;
}
@@ -151,6 +153,11 @@ public class RepositoryResource {
return sourceRootResource.get();
}
+ @Path("content/")
+ public ContentResource content() {
+ return contentResource.get();
+ }
+
@Path("permissions/")
public PermissionRootResource permissions() {
return permissionRootResource.get();
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
index 711a5cf196..1b23b18f27 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java
@@ -45,7 +45,7 @@ public class BranchRootResourceTest {
public void prepareEnvironment() throws Exception {
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
BranchRootResource branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper);
- RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null)), null);
+ RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null)), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java
new file mode 100644
index 0000000000..9302a4fcfd
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java
@@ -0,0 +1,186 @@
+package sonia.scm.api.v2.resources;
+
+import com.google.common.io.Resources;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.PathNotFoundException;
+import sonia.scm.repository.RepositoryNotFoundException;
+import sonia.scm.repository.api.CatCommandBuilder;
+import sonia.scm.repository.api.RepositoryService;
+import sonia.scm.repository.api.RepositoryServiceFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ContentResourceTest {
+
+ private static final String NAMESPACE = "space";
+ private static final String REPO_NAME = "name";
+ private static final String REV = "rev";
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private RepositoryServiceFactory repositoryServiceFactory;
+
+ @InjectMocks
+ private ContentResource contentResource;
+
+ private CatCommandBuilder catCommand;
+
+ @Before
+ public void initService() throws Exception {
+ NamespaceAndName existingNamespaceAndName = new NamespaceAndName(NAMESPACE, REPO_NAME);
+ RepositoryService repositoryService = repositoryServiceFactory.create(existingNamespaceAndName);
+ catCommand = repositoryService.getCatCommand();
+ when(catCommand.setRevision(REV)).thenReturn(catCommand);
+
+ // defaults for unknown things
+ doThrow(new RepositoryNotFoundException("x")).when(repositoryServiceFactory).create(not(eq(existingNamespaceAndName)));
+ doThrow(new PathNotFoundException("x")).when(catCommand).getStream(any());
+ }
+
+ @Test
+ public void shouldReadSimpleFile() throws Exception {
+ mockContent("file", "Hello".getBytes());
+
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file");
+ assertEquals(200, response.getStatus());
+
+ ByteArrayOutputStream baos = readOutputStream(response);
+
+ assertEquals("Hello", baos.toString());
+ }
+
+ @Test
+ public void shouldHandleMissingFile() {
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist");
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void shouldHandleMissingRepository() {
+ Response response = contentResource.get("no", "repo", REV, "anything");
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void shouldRecognizeTikaSourceCode() throws Exception {
+ mockContentFromResource("SomeGoCode.go");
+
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go");
+ assertEquals(200, response.getStatus());
+
+ assertEquals("GO", response.getHeaderString("Language"));
+ assertEquals("text/x-go", response.getHeaderString("Content-Type"));
+ }
+
+ @Test
+ public void shouldRecognizeSpecialSourceCode() throws Exception {
+ mockContentFromResource("Dockerfile");
+
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile");
+ assertEquals(200, response.getStatus());
+
+ assertEquals("DOCKERFILE", response.getHeaderString("Language"));
+ assertEquals("text/plain", response.getHeaderString("Content-Type"));
+ }
+
+ @Test
+ public void shouldRecognizeShebangSourceCode() throws Exception {
+ mockContentFromResource("someScript.sh");
+
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "someScript.sh");
+ assertEquals(200, response.getStatus());
+
+ assertEquals("PYTHON", response.getHeaderString("Language"));
+ assertEquals("application/x-sh", response.getHeaderString("Content-Type"));
+ }
+
+ @Test
+ public void shouldHandleRandomByteFile() throws Exception {
+ mockContentFromResource("JustBytes");
+
+ Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes");
+ assertEquals(200, response.getStatus());
+
+ assertFalse(response.getHeaders().containsKey("Language"));
+ assertEquals("application/octet-stream", response.getHeaderString("Content-Type"));
+ }
+
+ @Test
+ public void shouldNotReadCompleteFileForHead() throws Exception {
+ FailingAfterSomeBytesStream stream = new FailingAfterSomeBytesStream();
+ doAnswer(invocation -> stream).when(catCommand).getStream(eq("readHeadOnly"));
+
+ Response response = contentResource.metadata(NAMESPACE, REPO_NAME, REV, "readHeadOnly");
+ assertEquals(200, response.getStatus());
+
+ assertEquals("application/octet-stream", response.getHeaderString("Content-Type"));
+ assertTrue("stream has to be closed after reading head", stream.isClosed());
+ }
+
+ private void mockContentFromResource(String fileName) throws Exception {
+ URL url = Resources.getResource(fileName);
+ mockContent(fileName, Resources.toByteArray(url));
+ }
+
+ private void mockContent(String path, byte[] content) throws Exception {
+ doAnswer(invocation -> {
+ OutputStream outputStream = (OutputStream) invocation.getArguments()[0];
+ outputStream.write(content);
+ outputStream.close();
+ return null;
+ }).when(catCommand).retriveContent(any(), eq(path));
+ doAnswer(invocation -> new ByteArrayInputStream(content)).when(catCommand).getStream(eq(path));
+ }
+
+ private ByteArrayOutputStream readOutputStream(Response response) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ((StreamingOutput) response.getEntity()).write(baos);
+ return baos;
+ }
+
+ private static class FailingAfterSomeBytesStream extends InputStream {
+ private int bytesRead = 0;
+ private boolean closed = false;
+ @Override
+ public int read() {
+ if (++bytesRead > 1024) {
+ fail("read too many bytes");
+ }
+ return 0;
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
index 5de6d916ab..23cd51c9f9 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
@@ -53,11 +53,9 @@ import static org.mockito.MockitoAnnotations.initMocks;
@RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j
public class PermissionRootResourceTest {
- public static final String REPOSITORY_NAMESPACE = "repo_namespace";
- public static final String REPOSITORY_NAME = "repo";
+ private static final String REPOSITORY_NAMESPACE = "repo_namespace";
+ private static final String REPOSITORY_NAME = "repo";
private static final String PERMISSION_NAME = "perm";
-
-
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/";
private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME;
private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }";
@@ -93,7 +91,6 @@ public class PermissionRootResourceTest {
.content(PERMISSION_TEST_PAYLOAD)
.path(PATH_OF_ONE_PERMISSION);
-
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@Rule
@@ -102,7 +99,6 @@ public class PermissionRootResourceTest {
@Mock
private RepositoryManager repositoryManager;
-
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -112,17 +108,15 @@ public class PermissionRootResourceTest {
@InjectMocks
private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper;
-
private PermissionRootResource permissionRootResource;
-
@BeforeEach
@Before
public void prepareEnvironment() {
initMocks(this);
permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager));
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider
- .of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null);
+ .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class);
@@ -130,32 +124,50 @@ public class PermissionRootResourceTest {
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
}
+ @TestFactory
+ @DisplayName("test endpoints on missing repository and user is Admin")
+ Stream missedRepositoryTestFactory() {
+ return createDynamicTestsToAssertResponses(
+ requestGETAllPermissions.expectedResponseStatus(404),
+ requestGETPermission.expectedResponseStatus(404),
+ requestPOSTPermission.expectedResponseStatus(404),
+ requestDELETEPermission.expectedResponseStatus(404),
+ requestPUTPermission.expectedResponseStatus(404));
+ }
+
+ @TestFactory
+ @DisplayName("test endpoints on missing permission and user is Admin")
+ Stream missedPermissionTestFactory() {
+ authorizedUserHasARepository();
+ return createDynamicTestsToAssertResponses(
+ requestGETPermission.expectedResponseStatus(404),
+ requestPOSTPermission.expectedResponseStatus(201),
+ requestGETAllPermissions.expectedResponseStatus(200),
+ requestDELETEPermission.expectedResponseStatus(204),
+ requestPUTPermission.expectedResponseStatus(404));
+ }
+
+ @TestFactory
+ @DisplayName("test endpoints on missing permission and user is not Admin")
+ Stream missedPermissionUserForbiddenTestFactory() {
+ Repository mockRepository = mock(Repository.class);
+ when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
+ doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository);
+ when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
+ return createDynamicTestsToAssertResponses(
+ requestGETPermission.expectedResponseStatus(401),
+ requestPOSTPermission.expectedResponseStatus(401),
+ requestGETAllPermissions.expectedResponseStatus(401),
+ requestDELETEPermission.expectedResponseStatus(401),
+ requestPUTPermission.expectedResponseStatus(401));
+ }
@Test
public void shouldGetAllPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
- assertExpectedRequest(requestGETAllPermissions
- .expectedResponseStatus(200)
- .responseValidator((response) -> {
- String body = response.getContentAsString();
- ObjectMapper mapper = new ObjectMapper();
- try {
- List actualPermissionDtos = mapper.readValue(body, new TypeReference>() {
- });
- assertThat(actualPermissionDtos)
- .as("response payload match permission object models")
- .hasSize(TEST_PERMISSIONS.size())
- .usingRecursiveFieldByFieldElementComparator()
- .containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS))
- ;
- } catch (IOException e) {
- fail();
- }
- })
- );
+ assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS));
}
-
@Test
public void shouldGetPermissionByName() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
@@ -179,7 +191,6 @@ public class PermissionRootResourceTest {
);
}
-
@Test
public void shouldGetCreatedPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
@@ -207,7 +218,6 @@ public class PermissionRootResourceTest {
);
}
-
@Test
public void shouldGetUpdatedPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
@@ -287,7 +297,6 @@ public class PermissionRootResourceTest {
);
}
-
private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions) {
return permissions
.stream()
@@ -309,30 +318,6 @@ public class PermissionRootResourceTest {
return result;
}
- @TestFactory
- @DisplayName("test endpoints on missing repository and user is Admin")
- Stream missedRepositoryTestFactory() {
- return createDynamicTestsToAssertResponses(
- requestGETAllPermissions.expectedResponseStatus(404),
- requestGETPermission.expectedResponseStatus(404),
- requestPOSTPermission.expectedResponseStatus(404),
- requestDELETEPermission.expectedResponseStatus(404),
- requestPUTPermission.expectedResponseStatus(404));
- }
-
-
- @TestFactory
- @DisplayName("test endpoints on missing permission and user is Admin")
- Stream missedPermissionTestFactory() {
- authorizedUserHasARepository();
- return createDynamicTestsToAssertResponses(
- requestGETPermission.expectedResponseStatus(404),
- requestPOSTPermission.expectedResponseStatus(201),
- requestGETAllPermissions.expectedResponseStatus(200),
- requestDELETEPermission.expectedResponseStatus(204),
- requestPUTPermission.expectedResponseStatus(404));
- }
-
private Repository authorizedUserHasARepository() {
Repository mockRepository = mock(Repository.class);
when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
@@ -347,25 +332,7 @@ public class PermissionRootResourceTest {
when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions);
}
-
- @TestFactory
- @DisplayName("test endpoints on missing permission and user is not Admin")
- Stream missedPermissionUserForbiddenTestFactory() {
- Repository mockRepository = mock(Repository.class);
- when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
- doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository);
- when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
- return createDynamicTestsToAssertResponses(
- requestGETPermission.expectedResponseStatus(401),
- requestPOSTPermission.expectedResponseStatus(401),
- requestGETAllPermissions.expectedResponseStatus(401),
- requestDELETEPermission.expectedResponseStatus(401),
- requestPUTPermission.expectedResponseStatus(401));
- }
-
-
private Stream createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) {
-
return Stream.of(expectedRequests)
.map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry)));
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
index 75d09a2414..d47b26e5eb 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java
@@ -74,7 +74,7 @@ public class RepositoryRootResourceTest {
@Before
public void prepareEnvironment() {
initMocks(this);
- RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null);
+ RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null);
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource));
diff --git a/scm-webapp/src/test/resources/Dockerfile b/scm-webapp/src/test/resources/Dockerfile
new file mode 100644
index 0000000000..d4b48dfd45
--- /dev/null
+++ b/scm-webapp/src/test/resources/Dockerfile
@@ -0,0 +1,8 @@
+FROM ubuntu
+
+COPY nothing /nowhere
+
+RUN apt-get update
+
+EXEC shutdhown -h now
+
diff --git a/scm-webapp/src/test/resources/JustBytes b/scm-webapp/src/test/resources/JustBytes
new file mode 100644
index 0000000000..f455a92b47
Binary files /dev/null and b/scm-webapp/src/test/resources/JustBytes differ
diff --git a/scm-webapp/src/test/resources/SomeGoCode.go b/scm-webapp/src/test/resources/SomeGoCode.go
new file mode 100644
index 0000000000..18d6395a74
--- /dev/null
+++ b/scm-webapp/src/test/resources/SomeGoCode.go
@@ -0,0 +1 @@
+package resources
diff --git a/scm-webapp/src/test/resources/someScript.sh b/scm-webapp/src/test/resources/someScript.sh
new file mode 100644
index 0000000000..0bcecb189d
--- /dev/null
+++ b/scm-webapp/src/test/resources/someScript.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+
+for f in * ; do
+ ls $f
+done
+