+ * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files.
+ *
+ * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
+ * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
+ * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations
+ *
+ * @author Mohamed Karray
+ * @since 2.0.0
+ */
+public class InitialRepositoryLocationResolver {
+
+ private static final String DEFAULT_REPOSITORY_PATH = "repositories";
+
+ private static final CharMatcher ID_MATCHER = CharMatcher.anyOf("/\\.");
+
+ /**
+ * Returns the initial path to repository.
+ *
+ * @param repositoryId id of the repository
+ *
+ * @return initial path of repository
+ */
+ @SuppressWarnings("squid:S2083") // path traversal is prevented with ID_MATCHER
+ public Path getPath(String repositoryId) {
+ // avoid path traversal attacks
+ checkArgument(ID_MATCHER.matchesNoneOf(repositoryId), "repository id contains invalid characters");
+ return Paths.get(DEFAULT_REPOSITORY_PATH, repositoryId);
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java b/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java
new file mode 100644
index 0000000000..35a47af233
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/PathBasedRepositoryDAO.java
@@ -0,0 +1,18 @@
+package sonia.scm.repository;
+
+import java.nio.file.Path;
+
+/**
+ * A DAO used for Repositories accessible by a path
+ *
+ * @author Mohamed Karray
+ * @since 2.0.0
+ */
+public interface PathBasedRepositoryDAO extends RepositoryDAO {
+
+ /**
+ * Get the current path of the repository for the given id.
+ * This works for existing repositories only, not for repositories that should be created.
+ */
+ Path getPath(String repositoryId) ;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index fd8f07df8d..18613c1a12 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -62,13 +62,13 @@ import java.util.Set;
*/
@StaticPermissions(
value = "repository",
- permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}
+ permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"},
+ custom = true, customGlobal = true
)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
-
private static final long serialVersionUID = 3486560714961909711L;
private String contact;
@@ -81,7 +81,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified;
private String namespace;
private String name;
- private final Set permissions = new HashSet<>();
+ @XmlElement(name = "permission")
+ private final Set permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
@@ -122,7 +123,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
* @param permissions permissions for specific users and groups.
*/
public Repository(String id, String type, String namespace, String name, String contact,
- String description, Permission... permissions) {
+ String description, RepositoryPermission... permissions) {
this.id = id;
this.type = type;
this.namespace = namespace;
@@ -201,7 +202,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return new NamespaceAndName(getNamespace(), getName());
}
- public Collection getPermissions() {
+ public Collection getPermissions() {
return Collections.unmodifiableCollection(permissions);
}
@@ -297,16 +298,16 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
}
- public void setPermissions(Collection permissions) {
+ public void setPermissions(Collection permissions) {
this.permissions.clear();
this.permissions.addAll(permissions);
}
- public void addPermission(Permission newPermission) {
+ public void addPermission(RepositoryPermission newPermission) {
this.permissions.add(newPermission);
}
- public void removePermission(Permission permission) {
+ public void removePermission(RepositoryPermission permission) {
this.permissions.remove(permission);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java
index 3db13e5618..909d95dce2 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryConfig.java
@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
- *
+ *
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- *
+ *
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,13 +24,11 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
+ *
* http://bitbucket.org/sdorra/scm-manager
- *
*/
-
package sonia.scm.repository;
import sonia.scm.Validateable;
@@ -38,7 +36,6 @@ import sonia.scm.config.Configuration;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
-import java.io.File;
/**
* Basic {@link Repository} configuration class.
@@ -46,20 +43,10 @@ import java.io.File;
* @author Sebastian Sdorra
*/
@XmlRootElement
-public abstract class RepositoryConfig implements Validateable, Configuration
-{
-
- /**
- * Returns the directory for the repositories.
- *
- *
- * @return directory for the repositories
- */
- public File getRepositoryDirectory()
- {
- return repositoryDirectory;
- }
+public abstract class RepositoryConfig implements Validateable, Configuration {
+ /** true if the plugin is disabled */
+ private boolean disabled = false;
/**
* Returns true if the plugin is disabled.
*
@@ -67,8 +54,7 @@ public abstract class RepositoryConfig implements Validateable, Configuration
* @return true if the plugin is disabled
* @since 1.13
*/
- public boolean isDisabled()
- {
+ public boolean isDisabled() {
return disabled;
}
@@ -79,9 +65,8 @@ public abstract class RepositoryConfig implements Validateable, Configuration
* @return true if the configuration object is valid
*/
@Override
- public boolean isValid()
- {
- return repositoryDirectory != null;
+ public boolean isValid() {
+ return true;
}
//~--- set methods ----------------------------------------------------------
@@ -93,29 +78,11 @@ public abstract class RepositoryConfig implements Validateable, Configuration
* @param disabled
* @since 1.13
*/
- public void setDisabled(boolean disabled)
- {
+ public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
- /**
- * Sets the directory for the repositories
- *
- *
- * @param repositoryDirectory directory for repositories
- */
- public void setRepositoryDirectory(File repositoryDirectory)
- {
- this.repositoryDirectory = repositoryDirectory;
- }
- //~--- fields ---------------------------------------------------------------
-
- /** true if the plugin is disabled */
- private boolean disabled = false;
-
- /** directory for repositories */
- private File repositoryDirectory;
/**
* Specifies the identifier of the concrete {@link RepositoryConfig} when checking permissions of an object.
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java
index 2f766fb1f6..fd8e8a25f3 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryDirectoryHandler.java
@@ -40,8 +40,11 @@ import java.io.File;
* @author Sebastian Sdorra
* @since 1.36
*/
-public interface RepositoryDirectoryHandler extends RepositoryHandler
-{
+public interface RepositoryDirectoryHandler extends RepositoryHandler {
- public File getDirectory(Repository repository);
+ /**
+ * Get the current directory of the repository for the given id.
+ * @return the current directory of the given repository
+ */
+ File getDirectory(String repositoryId);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
index 79c06d03f9..aaa090827a 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryHandler.java
@@ -36,7 +36,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.Handler;
-import sonia.scm.NotSupportedFeatureException;
+import sonia.scm.FeatureNotSupportedException;
import sonia.scm.plugin.ExtensionPoint;
/**
@@ -50,17 +50,6 @@ public interface RepositoryHandler
extends Handler
{
- /**
- * Returns the resource path for the given {@link Repository}.
- * The resource path is part of the {@link Repository} url.
- *
- *
- *
- * @param repository given {@link Repository}
- * @return resource path of the {@link Repository}
- */
- public String createResourcePath(Repository repository);
-
//~--- get methods ----------------------------------------------------------
/**
@@ -70,9 +59,9 @@ public interface RepositoryHandler
* @return {@link ImportHandler} for the repository type of this handler
* @since 1.12
*
- * @throws NotSupportedFeatureException
+ * @throws FeatureNotSupportedException
*/
- public ImportHandler getImportHandler() throws NotSupportedFeatureException;
+ public ImportHandler getImportHandler() throws FeatureNotSupportedException;
/**
* Returns informations about the version of the RepositoryHandler.
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
new file mode 100644
index 0000000000..737374025d
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
@@ -0,0 +1,51 @@
+package sonia.scm.repository;
+
+import sonia.scm.SCMContextProvider;
+
+import javax.inject.Inject;
+import java.nio.file.Path;
+
+/**
+ * A Location Resolver for File based Repository Storage.
+ *
+ * WARNING: The Locations provided with this class may not be used from the plugins to store any plugin specific files.
+ *
+ * Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data
+ * Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files
+ * Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations
+ *
+ * @author Mohamed Karray
+ * @since 2.0.0
+ */
+public class RepositoryLocationResolver {
+
+ private final SCMContextProvider contextProvider;
+ private final RepositoryDAO repositoryDAO;
+ private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
+
+ @Inject
+ public RepositoryLocationResolver(SCMContextProvider contextProvider, RepositoryDAO repositoryDAO, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
+ this.contextProvider = contextProvider;
+ this.repositoryDAO = repositoryDAO;
+ this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
+ }
+
+ /**
+ * Returns the path to the repository.
+ *
+ * @param repositoryId repository id
+ *
+ * @return path of repository
+ */
+ public Path getPath(String repositoryId) {
+ Path path;
+
+ if (repositoryDAO instanceof PathBasedRepositoryDAO) {
+ path = ((PathBasedRepositoryDAO) repositoryDAO).getPath(repositoryId);
+ } else {
+ path = initialRepositoryLocationResolver.getPath(repositoryId);
+ }
+
+ return contextProvider.resolve(path);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java
new file mode 100644
index 0000000000..96f76346b3
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPathNotFoundException.java
@@ -0,0 +1,12 @@
+
+package sonia.scm.repository;
+
+public class RepositoryPathNotFoundException extends Exception {
+
+ public static final String REPOSITORY_PATH_NOT_FOUND = "Repository path not found";
+
+ public RepositoryPathNotFoundException() {
+ super(REPOSITORY_PATH_NOT_FOUND);
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java
similarity index 69%
rename from scm-core/src/main/java/sonia/scm/repository/Permission.java
rename to scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java
index 20cdc83cef..54163e0393 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Permission.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java
@@ -41,8 +41,15 @@ import sonia.scm.security.PermissionObject;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableSet;
//~--- JDK imports ------------------------------------------------------------
@@ -53,73 +60,38 @@ import java.io.Serializable;
*/
@XmlRootElement(name = "permissions")
@XmlAccessorType(XmlAccessType.FIELD)
-public class Permission implements PermissionObject, Serializable
+public class RepositoryPermission implements PermissionObject, Serializable
{
private static final long serialVersionUID = -2915175031430884040L;
private boolean groupPermission = false;
private String name;
- private PermissionType type = PermissionType.READ;
+ @XmlElement(name = "verb")
+ private Set verbs;
/**
- * Constructs a new {@link Permission}.
- * This constructor is used by JAXB.
- *
+ * Constructs a new {@link RepositoryPermission}.
+ * This constructor is used by JAXB and mapstruct.
*/
- public Permission() {}
+ public RepositoryPermission() {}
- /**
- * Constructs a new {@link Permission} with type = {@link PermissionType#READ}
- * for the specified user.
- *
- *
- * @param name name of the user
- */
- public Permission(String name)
+ public RepositoryPermission(String name, Collection verbs, boolean groupPermission)
{
- this();
this.name = name;
- }
-
- /**
- * Constructs a new {@link Permission} with the specified type for
- * the given user.
- *
- *
- * @param name name of the user
- * @param type type of the permission
- */
- public Permission(String name, PermissionType type)
- {
- this(name);
- this.type = type;
- }
-
- /**
- * Constructs a new {@link Permission} with the specified type for
- * the given user or group.
- *
- *
- * @param name name of the user or group
- * @param type type of the permission
- * @param groupPermission true if the permission is a permission for a group
- */
- public Permission(String name, PermissionType type, boolean groupPermission)
- {
- this(name, type);
+ this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs));
this.groupPermission = groupPermission;
}
//~--- methods --------------------------------------------------------------
/**
- * Returns true if the {@link Permission} is the same as the obj argument.
+ * Returns true if the {@link RepositoryPermission} is the same as the obj argument.
*
*
* @param obj the reference object with which to compare
*
- * @return true if the {@link Permission} is the same as the obj argument
+ * @return true if the {@link RepositoryPermission} is the same as the obj argument
*/
@Override
public boolean equals(Object obj)
@@ -134,23 +106,26 @@ public class Permission implements PermissionObject, Serializable
return false;
}
- final Permission other = (Permission) obj;
+ final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name)
- && Objects.equal(type, other.type)
+ && verbs.containsAll(other.verbs)
+ && verbs.size() == other.verbs.size()
&& Objects.equal(groupPermission, other.groupPermission);
}
/**
- * Returns the hash code value for the {@link Permission}.
+ * Returns the hash code value for the {@link RepositoryPermission}.
*
*
- * @return the hash code value for the {@link Permission}
+ * @return the hash code value for the {@link RepositoryPermission}
*/
@Override
public int hashCode()
{
- return Objects.hashCode(name, type, groupPermission);
+ // Normally we do not have a log of repository permissions having the same size of verbs, but different content.
+ // Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
+ return Objects.hashCode(name, verbs.size(), groupPermission);
}
@@ -160,7 +135,7 @@ public class Permission implements PermissionObject, Serializable
//J-
return MoreObjects.toStringHelper(this)
.add("name", name)
- .add("type", type)
+ .add("verbs", verbs)
.add("groupPermission", groupPermission)
.toString();
//J+
@@ -181,14 +156,14 @@ public class Permission implements PermissionObject, Serializable
}
/**
- * Returns the {@link PermissionType} of the permission.
+ * Returns the verb of the permission.
*
*
- * @return {@link PermissionType} of the permission
+ * @return verb of the permission
*/
- public PermissionType getType()
+ public Collection getVerbs()
{
- return type;
+ return verbs == null? emptyList(): verbs;
}
/**
@@ -228,13 +203,13 @@ public class Permission implements PermissionObject, Serializable
}
/**
- * Sets the type of the permission.
+ * Sets the verb of the permission.
*
*
- * @param type type of the permission
+ * @param verbs verbs of the permission
*/
- public void setType(PermissionType type)
+ public void setVerbs(Collection verbs)
{
- this.type = type;
+ this.verbs = unmodifiableSet(new LinkedHashSet<>(verbs));
}
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java
deleted file mode 100644
index a90eb4cc34..0000000000
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryUtil.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Preconditions;
-import sonia.scm.io.DirectoryFileFilter;
-import sonia.scm.util.IOUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-//~--- JDK imports ------------------------------------------------------------
-
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.11
- */
-public final class RepositoryUtil {
-
- private RepositoryUtil() {}
-
- public static List searchRepositoryDirectories(File directory, String... names) {
- List repositories = new ArrayList<>();
-
- searchRepositoryDirectories(repositories, directory, Arrays.asList(names));
-
- return repositories;
- }
-
- @SuppressWarnings("squid:S2083") // ignore, because the path is validated at {@link #getRepositoryId(File, File)}
- public static String getRepositoryId(AbstractRepositoryHandler handler, String directoryPath) throws IOException {
- return getRepositoryId(handler.getConfig().getRepositoryDirectory(), new File(directoryPath));
- }
-
- public static String getRepositoryId(AbstractRepositoryHandler handler, File directory) throws IOException {
- return getRepositoryId(handler.getConfig(), directory);
- }
-
- public static String getRepositoryId(RepositoryConfig config, File directory) throws IOException {
- return getRepositoryId(config.getRepositoryDirectory(), directory);
- }
-
- public static String getRepositoryId(File baseDirectory, File directory) throws IOException {
- String path = directory.getCanonicalPath();
- String basePath = baseDirectory.getCanonicalPath();
-
- Preconditions.checkArgument(
- path.startsWith(basePath),
- "repository path %s is not in the main repository path %s", path, basePath
- );
-
- String id = IOUtil.trimSeperatorChars(path.substring(basePath.length()));
-
- Preconditions.checkArgument(
- !id.contains("\\") && !id.contains("/"),
- "got illegal repository directory with separators in id: %s", path
- );
-
- return id;
- }
-
- private static void searchRepositoryDirectories(List repositories, File directory, List names) {
- boolean found = false;
-
- for (String name : names) {
- if (new File(directory, name).exists()) {
- found = true;
-
- break;
- }
- }
-
- if (found) {
- repositories.add(directory);
- } else {
- File[] directories = directory.listFiles(DirectoryFileFilter.instance);
-
- if (directories != null) {
- for (File d : directories) {
- searchRepositoryDirectories(repositories, d, names);
- }
- }
- }
- }
-}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
index 7217d0e97a..9e7094d5bf 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/DiffCommandBuilder.java
@@ -38,6 +38,8 @@ package sonia.scm.repository.api;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.FeatureNotSupportedException;
+import sonia.scm.repository.Feature;
import sonia.scm.repository.spi.DiffCommand;
import sonia.scm.repository.spi.DiffCommandRequest;
import sonia.scm.util.IOUtil;
@@ -45,6 +47,7 @@ import sonia.scm.util.IOUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -85,10 +88,12 @@ public final class DiffCommandBuilder
* only be called from the {@link RepositoryService}.
*
* @param diffCommand implementation of {@link DiffCommand}
+ * @param supportedFeatures The supported features of the provider
*/
- DiffCommandBuilder(DiffCommand diffCommand)
+ DiffCommandBuilder(DiffCommand diffCommand, Set supportedFeatures)
{
this.diffCommand = diffCommand;
+ this.supportedFeatures = supportedFeatures;
}
//~--- methods --------------------------------------------------------------
@@ -174,7 +179,8 @@ public final class DiffCommandBuilder
}
/**
- * Show the difference only for the given revision.
+ * Show the difference only for the given revision or (using {@link #setAncestorChangeset(String)}) between this
+ * and another revision.
*
*
* @param revision revision for difference
@@ -187,6 +193,22 @@ public final class DiffCommandBuilder
return this;
}
+ /**
+ * Compute the incoming changes of the branch set with {@link #setRevision(String)} in respect to the changeset given
+ * here. In other words: What changes would be new to the ancestor changeset given here when the branch would
+ * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}!
+ *
+ * @return {@code this}
+ */
+ public DiffCommandBuilder setAncestorChangeset(String revision)
+ {
+ if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
+ throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
+ }
+ request.setAncestorChangeset(revision);
+
+ return this;
+ }
//~--- get methods ----------------------------------------------------------
@@ -215,6 +237,7 @@ public final class DiffCommandBuilder
/** implementation of the diff command */
private final DiffCommand diffCommand;
+ private Set supportedFeatures;
/** request for the diff command implementation */
private final DiffCommandRequest request = new DiffCommandRequest();
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java
index 6c7c620fa4..6098bdf92b 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java
@@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
-import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.IncomingCommand;
import sonia.scm.repository.spi.IncomingCommandRequest;
-import sonia.scm.security.RepositoryPermission;
import java.io.IOException;
@@ -94,8 +93,7 @@ public final class IncomingCommandBuilder
{
Subject subject = SecurityUtils.getSubject();
- subject.checkPermission(new RepositoryPermission(remoteRepository,
- PermissionType.READ));
+ subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
request.setRemoteRepository(remoteRepository);
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
index 73062a0244..917b81391f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java
@@ -39,10 +39,12 @@ import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.FeatureNotSupportedException;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
+import sonia.scm.repository.Feature;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCacheKey;
@@ -51,6 +53,7 @@ import sonia.scm.repository.spi.LogCommandRequest;
import java.io.IOException;
import java.io.Serializable;
+import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -104,19 +107,20 @@ public final class LogCommandBuilder
/**
* Constructs a new {@link LogCommandBuilder}, this constructor should
* only be called from the {@link RepositoryService}.
- *
- * @param cacheManager cache manager
+ * @param cacheManager cache manager
* @param logCommand implementation of the {@link LogCommand}
* @param repository repository to query
* @param preProcessorUtil
+ * @param supportedFeatures The supported features of the provider
*/
LogCommandBuilder(CacheManager cacheManager, LogCommand logCommand,
- Repository repository, PreProcessorUtil preProcessorUtil)
+ Repository repository, PreProcessorUtil preProcessorUtil, Set supportedFeatures)
{
this.cache = cacheManager.getCache(CACHE_NAME);
this.logCommand = logCommand;
this.repository = repository;
this.preProcessorUtil = preProcessorUtil;
+ this.supportedFeatures = supportedFeatures;
}
//~--- methods --------------------------------------------------------------
@@ -397,7 +401,17 @@ public final class LogCommandBuilder
return this;
}
+ /**
+ * Compute the incoming changes of the branch set with {@link #setBranch(String)} in respect to the changeset given
+ * here. In other words: What changesets would be new to the ancestor changeset given here when the branch would
+ * be merged into it. Requires feature {@link sonia.scm.repository.Feature#INCOMING_REVISION}!
+ *
+ * @return {@code this}
+ */
public LogCommandBuilder setAncestorChangeset(String ancestorChangeset) {
+ if (!supportedFeatures.contains(Feature.INCOMING_REVISION)) {
+ throw new FeatureNotSupportedException(Feature.INCOMING_REVISION.name());
+ }
request.setAncestorChangeset(ancestorChangeset);
return this;
}
@@ -527,6 +541,7 @@ public final class LogCommandBuilder
/** Field description */
private final PreProcessorUtil preProcessorUtil;
+ private Set supportedFeatures;
/** repository to query */
private final Repository repository;
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
index 881a374864..8fcfc937e5 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java
@@ -15,7 +15,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
*
* To actually merge feature_branch into integration_branch do this:
*
- * repositoryService.gerMergeCommand()
+ * repositoryService.getMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .executeMerge();
@@ -33,7 +33,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
*
* To check whether they can be merged without conflicts beforehand do this:
*
- * repositoryService.gerMergeCommand()
+ * repositoryService.getMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .dryRun()
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java
index 2753128eac..d39c95e0e2 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java
@@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager;
import sonia.scm.repository.ChangesetPagingResult;
-import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.OutgoingCommand;
import sonia.scm.repository.spi.OutgoingCommandRequest;
-import sonia.scm.security.RepositoryPermission;
import java.io.IOException;
@@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder
{
Subject subject = SecurityUtils.getSubject();
- subject.checkPermission(new RepositoryPermission(remoteRepository,
- PermissionType.READ));
+ subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
request.setRemoteRepository(remoteRepository);
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java
index a0f5ff4115..969ec6ef11 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java
@@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PullCommand;
import sonia.scm.repository.spi.PullCommandRequest;
-import sonia.scm.security.RepositoryPermission;
import java.io.IOException;
import java.net.URL;
@@ -96,9 +95,7 @@ public final class PullCommandBuilder
public PullResponse pull(String url) throws IOException {
Subject subject = SecurityUtils.getSubject();
//J-
- subject.checkPermission(
- new RepositoryPermission(localRepository, PermissionType.WRITE)
- );
+ subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
//J+
URL remoteUrl = new URL(url);
@@ -124,12 +121,8 @@ public final class PullCommandBuilder
Subject subject = SecurityUtils.getSubject();
//J-
- subject.checkPermission(
- new RepositoryPermission(localRepository, PermissionType.WRITE)
- );
- subject.checkPermission(
- new RepositoryPermission(remoteRepository, PermissionType.READ)
- );
+ subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
+ subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
//J+
request.reset();
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java
index 7b318e49ec..a734225281 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java
@@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PushCommand;
import sonia.scm.repository.spi.PushCommandRequest;
-import sonia.scm.security.RepositoryPermission;
import java.io.IOException;
import java.net.URL;
@@ -92,9 +91,7 @@ public final class PushCommandBuilder
Subject subject = SecurityUtils.getSubject();
//J-
- subject.checkPermission(
- new RepositoryPermission(remoteRepository, PermissionType.WRITE)
- );
+ subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
//J+
logger.info("push changes to repository {}", remoteRepository.getId());
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index fe0529e6b5..ad53c3a8f7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -221,7 +221,7 @@ public final class RepositoryService implements Closeable {
logger.debug("create diff command for repository {}",
repository.getNamespaceAndName());
- return new DiffCommandBuilder(provider.getDiffCommand());
+ return new DiffCommandBuilder(provider.getDiffCommand(), provider.getSupportedFeatures());
}
/**
@@ -253,7 +253,7 @@ public final class RepositoryService implements Closeable {
repository.getNamespaceAndName());
return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
- repository, preProcessorUtil);
+ repository, preProcessorUtil, provider.getSupportedFeatures());
}
/**
@@ -363,8 +363,8 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
* @since 2.0.0
*/
- public MergeCommandBuilder gerMergeCommand() {
- logger.debug("create unbundle command for repository {}",
+ public MergeCommandBuilder getMergeCommand() {
+ logger.debug("create merge command for repository {}",
repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand());
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
index 597826676d..591b8167e5 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
@@ -3,10 +3,29 @@ package sonia.scm.repository.api;
import sonia.scm.plugin.ExtensionPoint;
import sonia.scm.repository.Repository;
+/**
+ * Provider for scm native protocols.
+ *
+ * @param type of protocol
+ *
+ * @since 2.0.0
+ */
@ExtensionPoint(multi = true)
public interface ScmProtocolProvider {
+ /**
+ * Returns type of repository (e.g.: git, svn, hg, etc.)
+ *
+ * @return name of type
+ */
String getType();
+ /**
+ * Returns protocol for the given repository.
+ *
+ * @param repository repository
+ *
+ * @return protocol for repository
+ */
T get(Repository repository);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
index 4bbe61ea41..27cafd1a16 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/HookEventFacade.java
@@ -74,19 +74,24 @@ public final class HookEventFacade
//~--- methods --------------------------------------------------------------
public HookEventHandler handle(String id) {
- return handle(repositoryManagerProvider.get().get(id));
+ Repository repository = repositoryManagerProvider.get().get(id);
+ if (repository == null)
+ {
+ throw notFound(entity("repository", id));
+ }
+ return handle(repository);
}
public HookEventHandler handle(NamespaceAndName namespaceAndName) {
- return handle(repositoryManagerProvider.get().get(namespaceAndName));
+ Repository repository = repositoryManagerProvider.get().get(namespaceAndName);
+ if (repository == null)
+ {
+ throw notFound(entity(namespaceAndName));
+ }
+ return handle(repository);
}
public HookEventHandler handle(Repository repository) {
- if (repository == null)
- {
- throw notFound(entity(repository));
- }
-
return new HookEventHandler(repositoryManagerProvider.get(),
hookContextFactory, repository);
}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
index baf03a0aef..45dfc0b2b7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java
@@ -5,7 +5,6 @@ import com.google.common.base.Objects;
import com.google.common.base.Strings;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
-import sonia.scm.util.Util;
import java.io.Serializable;
diff --git a/scm-core/src/main/java/sonia/scm/security/AccessToken.java b/scm-core/src/main/java/sonia/scm/security/AccessToken.java
index 714b09eff8..3341500199 100644
--- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java
+++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java
@@ -31,77 +31,105 @@
package sonia.scm.security;
import java.util.Date;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
/**
* An access token can be used to access scm-manager without providing username and password. An {@link AccessToken} can
* be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be
* send along with every request. The token should be send in its compact representation as bearer authorization header
* or as cookie.
- *
+ *
* @author Sebastian Sdorra
* @since 2.0.0
*/
public interface AccessToken {
-
+
/**
* Returns unique id of the access token.
- *
+ *
* @return unique id
*/
String getId();
-
+
/**
* Returns name of subject which identifies the principal.
- *
+ *
* @return name of subject
*/
String getSubject();
-
+
/**
* Returns optional issuer. The issuer identifies the principal that issued the token.
- *
+ *
* @return optional issuer
*/
Optional getIssuer();
-
+
/**
* Returns time at which the token was issued.
- *
+ *
* @return time at which the token was issued
*/
Date getIssuedAt();
-
+
/**
* Returns the expiration time of token.
- *
+ *
* @return expiration time
*/
Date getExpiration();
-
+
/**
- * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
+ * Returns refresh expiration of token.
+ *
+ * @return refresh expiration
+ */
+ Optional getRefreshExpiration();
+
+ /**
+ * Returns id of the parent key.
+ *
+ * @return parent key id
+ */
+ Optional getParentKey();
+
+ /**
+ * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
* token. For example we could issue a token which can only be used to read a single repository. for more informations
* please have a look at {@link Scope}.
- *
+ *
* @return scope of token.
*/
Scope getScope();
-
+
+ /**
+ * Returns name of groups, in which the user should be a member.
+ *
+ * @return name of groups
+ */
+ Set getGroups();
+
/**
* Returns an optional value of a custom token field.
- *
+ *
* @param type of field
* @param key key of token field
- *
+ *
* @return optional value of custom field
*/
Optional getCustom(String key);
-
+
/**
* Returns compact representation of token.
- *
+ *
* @return compact representation
*/
String compact();
+
+ /**
+ * Returns read only map of all claim keys with their values.
+ */
+ Map getClaims();
}
diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java
index dd7986c22a..0924716bd8 100644
--- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java
@@ -74,21 +74,40 @@ public interface AccessTokenBuilder {
* Sets the expiration for the token.
*
* @param count expiration count
- * @param unit expirtation unit
+ * @param unit expiration unit
*
* @return {@code this}
*/
AccessTokenBuilder expiresIn(long count, TimeUnit unit);
-
+
+ /**
+ * Sets the time how long this token may be refreshed. Set this to 0 (zero) to disable automatic refresh.
+ *
+ * @param count Time unit count. If set to 0, automatic refresh is disabled.
+ * @param unit time unit
+ *
+ * @return {@code this}
+ */
+ AccessTokenBuilder refreshableFor(long count, TimeUnit unit);
+
/**
* Reduces the permissions of the token by providing a scope.
- *
+ *
* @param scope scope of token
- *
+ *
* @return {@code this}
*/
AccessTokenBuilder scope(Scope scope);
-
+
+ /**
+ * Define the logged in user as member of the given groups.
+ *
+ * @param groups group names
+ *
+ * @return {@code this}
+ */
+ AccessTokenBuilder groups(String... groups);
+
/**
* Creates a new {@link AccessToken} with the provided settings.
*
diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java
new file mode 100644
index 0000000000..999c693b8f
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java
@@ -0,0 +1,30 @@
+package sonia.scm.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Generates cookies and invalidates access token cookies.
+ *
+ * @author Sebastian Sdorra
+ * @since 2.0.0
+ */
+public interface AccessTokenCookieIssuer {
+
+ /**
+ * Creates a cookie for token authentication and attaches it to the response.
+ *
+ * @param request http servlet request
+ * @param response http servlet response
+ * @param accessToken access token
+ */
+ void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
+ /**
+ * Invalidates the authentication cookie.
+ *
+ * @param request http servlet request
+ * @param response http servlet response
+ */
+ void invalidate(HttpServletRequest request, HttpServletResponse response);
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java
similarity index 82%
rename from scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java
rename to scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java
index 4389e7bfb7..24a92929f9 100644
--- a/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java
+++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenValidator.java
@@ -30,26 +30,25 @@
*/
package sonia.scm.security;
-import java.util.Map;
import sonia.scm.plugin.ExtensionPoint;
/**
- * Validates the claims of a jwt token. The validator is called durring authentication
- * with a jwt token.
+ * Validates an {@link AccessToken}. The validator is called during authentication
+ * with an {@link AccessToken}.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
@ExtensionPoint
-public interface TokenClaimsValidator {
+public interface AccessTokenValidator {
/**
- * Returns {@code true} if the claims is valid. If the token is not valid and the
+ * Returns {@code true} if the {@link AccessToken} is valid. If the token is not valid and the
* method returns {@code false}, the authentication is treated as failed.
*
- * @param claims token claims
+ * @param token the access token to verify
*
- * @return {@code true} if the claims is valid
+ * @return {@code true} if the token is valid
*/
- boolean validate(Map claims);
+ boolean validate(AccessToken token);
}
diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java
index 56b8d04a41..c98d81f8ba 100644
--- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java
+++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java
@@ -89,8 +89,12 @@ public class AssignedPermission implements PermissionObject, Serializable
*/
public AssignedPermission(String name, String permission)
{
- this.name = name;
- this.permission = permission;
+ this(name, new PermissionDescriptor(permission));
+ }
+
+ public AssignedPermission(String name, PermissionDescriptor permission)
+ {
+ this(name, false, permission);
}
/**
@@ -103,6 +107,12 @@ public class AssignedPermission implements PermissionObject, Serializable
*/
public AssignedPermission(String name, boolean groupPermission,
String permission)
+ {
+ this(name, groupPermission, new PermissionDescriptor(permission));
+ }
+
+ public AssignedPermission(String name, boolean groupPermission,
+ PermissionDescriptor permission)
{
this.name = name;
this.groupPermission = groupPermission;
@@ -173,12 +183,9 @@ public class AssignedPermission implements PermissionObject, Serializable
}
/**
- * Returns the string representation of the permission.
- *
- *
- * @return string representation of the permission
+ * Returns the description of the permission.
*/
- public String getPermission()
+ public PermissionDescriptor getPermission()
{
return permission;
}
@@ -205,5 +212,5 @@ public class AssignedPermission implements PermissionObject, Serializable
private String name;
/** string representation of the permission */
- private String permission;
+ private PermissionDescriptor permission;
}
diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java
similarity index 90%
rename from scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java
rename to scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java
index ad93bf25a9..9ebea488a5 100644
--- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java
+++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java
@@ -51,7 +51,7 @@ import java.io.Serializable;
* @since 1.31
*/
@Event
-public final class StoredAssignedPermissionEvent implements Serializable
+public final class AssignedPermissionEvent implements Serializable
{
/** serial version uid */
@@ -60,14 +60,14 @@ public final class StoredAssignedPermissionEvent implements Serializable
//~--- constructors ---------------------------------------------------------
/**
- * Constructs a new StoredAssignedPermissionEvent.
+ * Constructs a new AssignedPermissionEvent.
*
*
* @param type type of the event
* @param permission permission object which has changed
*/
- public StoredAssignedPermissionEvent(HandlerEventType type,
- StoredAssignedPermission permission)
+ public AssignedPermissionEvent(HandlerEventType type,
+ AssignedPermission permission)
{
this.type = type;
this.permission = permission;
@@ -91,8 +91,8 @@ public final class StoredAssignedPermissionEvent implements Serializable
return false;
}
- final StoredAssignedPermissionEvent other =
- (StoredAssignedPermissionEvent) obj;
+ final AssignedPermissionEvent other =
+ (AssignedPermissionEvent) obj;
return Objects.equal(type, other.type)
&& Objects.equal(permission, other.permission);
@@ -140,7 +140,7 @@ public final class StoredAssignedPermissionEvent implements Serializable
*
* @return changed permission
*/
- public StoredAssignedPermission getPermission()
+ public AssignedPermission getPermission()
{
return permission;
}
@@ -148,7 +148,7 @@ public final class StoredAssignedPermissionEvent implements Serializable
//~--- fields ---------------------------------------------------------------
/** changed permission */
- private StoredAssignedPermission permission;
+ private AssignedPermission permission;
/** type of the event */
private HandlerEventType type;
diff --git a/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java b/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java
index d1151c3b35..b12d8b6978 100644
--- a/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java
+++ b/scm-core/src/main/java/sonia/scm/security/AuthorizationCollector.java
@@ -34,6 +34,7 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.subject.PrincipalCollection;
import sonia.scm.plugin.ExtensionPoint;
/**
@@ -42,15 +43,16 @@ import sonia.scm.plugin.ExtensionPoint;
* @author Sebastian Sdorra
* @since 2.0.0
*/
-@ExtensionPoint(multi = false)
+@ExtensionPoint
public interface AuthorizationCollector
{
/**
* Returns {@link AuthorizationInfo} for the authenticated user.
*
+ * @param principalCollection collected principals
*
* @return {@link AuthorizationInfo} for authenticated user
*/
- public AuthorizationInfo collect();
+ AuthorizationInfo collect(PrincipalCollection principalCollection);
}
diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
index 3fcab8762c..115bb082c9 100644
--- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
+++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java
@@ -37,7 +37,6 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
-import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
@@ -54,6 +53,8 @@ import sonia.scm.group.GroupNames;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
+import java.util.Collections;
+
import static com.google.common.base.Preconditions.checkArgument;
/**
@@ -63,8 +64,7 @@ import static com.google.common.base.Preconditions.checkArgument;
* @author Sebastian Sdorra
* @since 2.0.0
*/
-public final class DAORealmHelper
-{
+public final class DAORealmHelper {
/**
* the logger for DAORealmHelper
@@ -109,37 +109,37 @@ public final class DAORealmHelper
public CredentialsMatcher wrapCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
return new RetryLimitPasswordMatcher(loginAttemptHandler, credentialsMatcher);
}
-
+
/**
- * Method description
+ * Creates {@link AuthenticationInfo} from a {@link UsernamePasswordToken}. The method accepts
+ * {@link AuthenticationInfo} as argument, so that the caller does not need to cast.
*
+ * @param token authentication token, it must be {@link UsernamePasswordToken}
*
- * @param token
- *
- * @return
- *
- * @throws AuthenticationException
+ * @return authentication info
*/
- public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
+ public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) {
checkArgument(token instanceof UsernamePasswordToken, "%s is required", UsernamePasswordToken.class);
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String principal = upt.getUsername();
- return getAuthenticationInfo(principal, null, null);
+ return getAuthenticationInfo(principal, null, null, Collections.emptySet());
}
/**
- * Method description
+ * Returns a builder for {@link AuthenticationInfo}.
*
+ * @param principal name of principal (username)
*
- * @param principal
- * @param credentials
- * @param scope
- *
- * @return
+ * @return authentication info builder
*/
- public AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) {
+ public AuthenticationInfoBuilder authenticationInfoBuilder(String principal) {
+ return new AuthenticationInfoBuilder(principal);
+ }
+
+
+ private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable groups) {
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
LOG.debug("try to authenticate {}", principal);
@@ -157,7 +157,7 @@ public final class DAORealmHelper
collection.add(principal, realm);
collection.add(user, realm);
- collection.add(collectGroups(principal), realm);
+ collection.add(collectGroups(principal, groups), realm);
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
String creds = credentials;
@@ -171,11 +171,15 @@ public final class DAORealmHelper
//~--- methods --------------------------------------------------------------
- private GroupNames collectGroups(String principal) {
+ private GroupNames collectGroups(String principal, Iterable groupNames) {
Builder builder = ImmutableSet.builder();
builder.add(GroupNames.AUTHENTICATED);
+ for (String group : groupNames) {
+ builder.add(group);
+ }
+
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
@@ -187,6 +191,69 @@ public final class DAORealmHelper
return groups;
}
+ /**
+ * Builder class for {@link AuthenticationInfo}.
+ */
+ public class AuthenticationInfoBuilder {
+
+ private final String principal;
+
+ private String credentials;
+ private Scope scope;
+ private Iterable groups = Collections.emptySet();
+
+ private AuthenticationInfoBuilder(String principal) {
+ this.principal = principal;
+ }
+
+ /**
+ * With credentials uses the given credentials for the {@link AuthenticationInfo}, this is particularly important
+ * for caching purposes.
+ *
+ * @param credentials credentials such as password
+ *
+ * @return {@code this}
+ */
+ public AuthenticationInfoBuilder withCredentials(String credentials) {
+ this.credentials = credentials;
+ return this;
+ }
+
+ /**
+ * With the scope object it is possible to limit the access permissions to scm-manager.
+ *
+ * @param scope scope object
+ *
+ * @return {@code this}
+ */
+ public AuthenticationInfoBuilder withScope(Scope scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ /**
+ * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
+ *
+ * @param groups extra groups
+ *
+ * @return {@code this}
+ */
+ public AuthenticationInfoBuilder withGroups(Iterable groups) {
+ this.groups = groups;
+ return this;
+ }
+
+ /**
+ * Build creates the authentication info from the given information.
+ *
+ * @return authentication info
+ */
+ public AuthenticationInfo build() {
+ return getAuthenticationInfo(principal, credentials, scope, groups);
+ }
+
+ }
+
private static class RetryLimitPasswordMatcher implements CredentialsMatcher {
private final LoginAttemptHandler loginAttemptHandler;
diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
index b4f0d81cd3..9c1fa590cc 100644
--- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
+++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
@@ -164,7 +164,7 @@ public class DefaultCipherHandler implements CipherHandler {
String result = null;
try {
- byte[] encodedInput = Base64.getDecoder().decode(value);
+ byte[] encodedInput = Base64.getUrlDecoder().decode(value);
byte[] salt = new byte[SALT_LENGTH];
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
@@ -221,7 +221,7 @@ public class DefaultCipherHandler implements CipherHandler {
System.arraycopy(salt, 0, result, 0, SALT_LENGTH);
System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
result.length - SALT_LENGTH);
- res = new String(Base64.getEncoder().encode(result), ENCODING);
+ res = new String(Base64.getUrlEncoder().encode(result), ENCODING);
} catch (IOException | GeneralSecurityException ex) {
throw new CipherException("could not encode string", ex);
}
diff --git a/scm-core/src/main/java/sonia/scm/security/Permission.java b/scm-core/src/main/java/sonia/scm/security/Permission.java
new file mode 100644
index 0000000000..a7aa2798e7
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/Permission.java
@@ -0,0 +1,13 @@
+package sonia.scm.security;
+
+import com.github.sdorra.ssp.PermissionObject;
+import com.github.sdorra.ssp.StaticPermissions;
+
+@StaticPermissions(
+ value = "permission",
+ permissions = {},
+ globalPermissions = {"list", "read", "assign"},
+ custom = true, customGlobal = true
+)
+public interface Permission extends PermissionObject {
+}
diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
index 20d95958a1..8d95131ee6 100644
--- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
+++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
@@ -39,7 +39,6 @@ import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
@@ -67,19 +66,8 @@ public class PermissionDescriptor implements Serializable
*/
public PermissionDescriptor() {}
- /**
- * Constructs ...
- *
- *
- * @param displayName
- * @param description
- * @param value
- */
- public PermissionDescriptor(String displayName, String description,
- String value)
+ public PermissionDescriptor(String value)
{
- this.displayName = displayName;
- this.description = description;
this.value = value;
}
@@ -103,9 +91,7 @@ public class PermissionDescriptor implements Serializable
final PermissionDescriptor other = (PermissionDescriptor) obj;
- return Objects.equal(displayName, other.displayName)
- && Objects.equal(description, other.description)
- && Objects.equal(value, other.value);
+ return Objects.equal(value, other.value);
}
/**
@@ -114,7 +100,7 @@ public class PermissionDescriptor implements Serializable
@Override
public int hashCode()
{
- return Objects.hashCode(displayName, description, value);
+ return value.hashCode();
}
/**
@@ -126,8 +112,6 @@ public class PermissionDescriptor implements Serializable
//J-
return MoreObjects.toStringHelper(this)
- .add("displayName", displayName)
- .add("description", description)
.add("value", value)
.toString();
@@ -136,28 +120,6 @@ public class PermissionDescriptor implements Serializable
//~--- get methods ----------------------------------------------------------
- /**
- * Returns the description of the permission.
- *
- *
- * @return description
- */
- public String getDescription()
- {
- return description;
- }
-
- /**
- * Returns the display name of the permission.
- *
- *
- * @return display name
- */
- public String getDisplayName()
- {
- return displayName;
- }
-
/**
* Returns the string representation of the permission.
*
@@ -171,13 +133,6 @@ public class PermissionDescriptor implements Serializable
//~--- fields ---------------------------------------------------------------
- /** description */
- private String description;
-
- /** display name */
- @XmlElement(name = "display-name")
- private String displayName;
-
/** value */
private String value;
}
diff --git a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java
deleted file mode 100644
index 1b0229d6f5..0000000000
--- a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.security;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Objects;
-import org.apache.shiro.authz.Permission;
-import sonia.scm.repository.PermissionType;
-import sonia.scm.repository.Repository;
-
-import java.io.Serializable;
-
-//~--- JDK imports ------------------------------------------------------------
-
-/**
- * This class represents the permission to a repository of a user.
- *
- * @author Sebastian Sdorra
- * @since 1.21
- */
-public final class RepositoryPermission
- implements StringablePermission, Serializable
-{
-
- /**
- * Type string of the permission
- * @since 1.31
- */
- public static final String TYPE = "repository";
-
- /** Field description */
- public static final String WILDCARD = "*";
-
- /** Field description */
- private static final long serialVersionUID = 3832804235417228043L;
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- *
- * @param repository
- * @param permissionType
- */
- public RepositoryPermission(Repository repository,
- PermissionType permissionType)
- {
- this(repository.getId(), permissionType);
- }
-
- /**
- * Constructs ...
- *
- *
- * @param repositoryId
- * @param permissionType
- */
- public RepositoryPermission(String repositoryId,
- PermissionType permissionType)
- {
- this.repositoryId = repositoryId;
- this.permissionType = permissionType;
- }
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param obj
- *
- * @return
- */
- @Override
- public boolean equals(Object obj)
- {
- if (obj == null)
- {
- return false;
- }
-
- if (getClass() != obj.getClass())
- {
- return false;
- }
-
- final RepositoryPermission other = (RepositoryPermission) obj;
-
- return Objects.equal(repositoryId, other.repositoryId)
- && Objects.equal(permissionType, other.permissionType);
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public int hashCode()
- {
- return Objects.hashCode(repositoryId, permissionType);
- }
-
- /**
- * Method description
- *
- *
- * @param p
- *
- * @return
- */
- @Override
- public boolean implies(Permission p)
- {
- boolean result = false;
-
- if (p instanceof RepositoryPermission)
- {
- RepositoryPermission rp = (RepositoryPermission) p;
-
- //J-
- result = (repositoryId.equals(WILDCARD) || repositoryId.equals(rp.repositoryId))
- && (permissionType.getValue() >= rp.permissionType.getValue());
- //J+
- }
-
- return result;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public String toString()
- {
- //J-
- return MoreObjects.toStringHelper(this)
- .add("repositoryId", repositoryId)
- .add("permissionType", permissionType)
- .toString();
- //J+
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public String getAsString()
- {
- StringBuilder buffer = new StringBuilder(TYPE);
-
- buffer.append(":").append(repositoryId).append(":").append(permissionType);
-
- return buffer.toString();
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- public PermissionType getPermissionType()
- {
- return permissionType;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- public String getRepositoryId()
- {
- return repositoryId;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private PermissionType permissionType;
-
- /** Field description */
- private String repositoryId;
-}
diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java
index f43afcb7f2..174b64f5e6 100644
--- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java
+++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java
@@ -32,13 +32,8 @@
package sonia.scm.security;
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Predicate;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.util.List;
+import java.util.Collection;
+import java.util.function.Predicate;
/**
* The SecuritySystem manages global permissions.
@@ -57,7 +52,7 @@ public interface SecuritySystem
*
* @return stored permission
*/
- public StoredAssignedPermission addPermission(AssignedPermission permission);
+ void addPermission(AssignedPermission permission);
/**
* Delete stored permission.
@@ -65,51 +60,17 @@ public interface SecuritySystem
*
* @param permission permission to be deleted
*/
- public void deletePermission(StoredAssignedPermission permission);
-
- /**
- * Delete stored permission.
- *
- *
- * @param id id of the permission
- */
- public void deletePermission(String id);
-
- /**
- * Modify stored permission.
- *
- *
- * @param permission stored permisison
- */
- public void modifyPermission(StoredAssignedPermission permission);
+ void deletePermission(AssignedPermission permission);
//~--- get methods ----------------------------------------------------------
- /**
- * Return all stored permissions.
- *
- *
- * @return stored permission
- */
- public List getAllPermissions();
-
/**
* Return all available permissions.
*
*
* @return available permissions
*/
- public List getAvailablePermissions();
-
- /**
- * Return the stored permission which is stored with the given id.
- *
- *
- * @param id id of the stored permission
- *
- * @return stored permission
- */
- public StoredAssignedPermission getPermission(String id);
+ Collection getAvailablePermissions();
/**
* Returns all stored permissions which are matched by the given
@@ -120,6 +81,5 @@ public interface SecuritySystem
*
* @return filtered permissions
*/
- public List getPermissions(
- Predicate predicate);
+ Collection getPermissions(Predicate predicate);
}
diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java
index 0e3c06e32d..da11400140 100644
--- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java
+++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java
@@ -45,7 +45,12 @@ import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
/**
* Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated
@@ -80,22 +85,107 @@ public final class SyncingRealmHelper {
this.groupManager = groupManager;
}
- //~--- methods --------------------------------------------------------------
/**
* Create {@link AuthenticationInfo} from user and groups.
- *
- *
- * @param realm name of the realm
- * @param user authenticated user
- * @param groups groups of the authenticated user
- *
- * @return authentication info
*/
- public AuthenticationInfo createAuthenticationInfo(String realm, User user,
- String... groups) {
- return createAuthenticationInfo(realm, user, ImmutableList.copyOf(groups));
+ public AuthenticationInfoBuilder.ForRealm authenticationInfo() {
+ return new AuthenticationInfoBuilder().new ForRealm();
}
+ public class AuthenticationInfoBuilder {
+ private String realm;
+ private User user;
+ private Collection groups;
+ private boolean external;
+
+ private AuthenticationInfo build() {
+ return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, external);
+ }
+
+ public class ForRealm {
+ private ForRealm() {
+ }
+
+ /**
+ * Sets the realm.
+ * @param realm name of the realm
+ */
+ public ForUser forRealm(String realm) {
+ AuthenticationInfoBuilder.this.realm = realm;
+ return AuthenticationInfoBuilder.this.new ForUser();
+ }
+ }
+
+ public class ForUser {
+ private ForUser() {
+ }
+
+ /**
+ * Sets the user.
+ * @param user authenticated user
+ */
+ public AuthenticationInfoBuilder.WithGroups andUser(User user) {
+ AuthenticationInfoBuilder.this.user = user;
+ return AuthenticationInfoBuilder.this.new WithGroups();
+ }
+ }
+
+ public class WithGroups {
+ private WithGroups() {
+ }
+
+ /**
+ * Build the authentication info without groups.
+ * @return The complete {@link AuthenticationInfo}
+ */
+ public AuthenticationInfo withoutGroups() {
+ return withGroups(emptyList());
+ }
+
+ /**
+ * Set the internal groups for the user.
+ * @param groups groups of the authenticated user
+ * @return The complete {@link AuthenticationInfo}
+ */
+ public AuthenticationInfo withGroups(String... groups) {
+ return withGroups(asList(groups));
+ }
+
+ /**
+ * Set the internal groups for the user.
+ * @param groups groups of the authenticated user
+ * @return The complete {@link AuthenticationInfo}
+ */
+ public AuthenticationInfo withGroups(Collection groups) {
+ AuthenticationInfoBuilder.this.groups = groups;
+ AuthenticationInfoBuilder.this.external = false;
+ return build();
+ }
+
+ /**
+ * Set the external groups for the user.
+ * @param groups external groups of the authenticated user
+ * @return The complete {@link AuthenticationInfo}
+ */
+ public AuthenticationInfo withExternalGroups(String... groups) {
+ return withExternalGroups(asList(groups));
+ }
+
+ /**
+ * Set the external groups for the user.
+ * @param groups external groups of the authenticated user
+ * @return The complete {@link AuthenticationInfo}
+ */
+ public AuthenticationInfo withExternalGroups(Collection groups) {
+ AuthenticationInfoBuilder.this.groups = groups;
+ AuthenticationInfoBuilder.this.external = true;
+ return build();
+ }
+ }
+ }
+
+ //~--- methods --------------------------------------------------------------
+
/**
* Create {@link AuthenticationInfo} from user and groups.
*
@@ -106,13 +196,13 @@ public final class SyncingRealmHelper {
*
* @return authentication info
*/
- public AuthenticationInfo createAuthenticationInfo(String realm, User user,
- Collection groups) {
+ private AuthenticationInfo createAuthenticationInfo(String realm, User user,
+ Collection groups, boolean externalGroups) {
SimplePrincipalCollection collection = new SimplePrincipalCollection();
collection.add(user.getId(), realm);
collection.add(user, realm);
- collection.add(new GroupNames(groups), realm);
+ collection.add(new GroupNames(groups, externalGroups), realm);
return new SimpleAuthenticationInfo(collection, user.getPassword());
}
@@ -161,6 +251,6 @@ public final class SyncingRealmHelper {
}
}
- });
- }
+ });
}
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java
index 941b3923d1..cf58fc43c7 100644
--- a/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java
+++ b/scm-core/src/main/java/sonia/scm/store/BlobStoreFactory.java
@@ -32,9 +32,25 @@
package sonia.scm.store;
+import sonia.scm.repository.Repository;
+
/**
- * The BlobStoreFactory can be used to create new or get existing
- * {@link BlobStore}s.
+ * The BlobStoreFactory can be used to create a new or get an existing {@link BlobStore}s.
+ *
+ * You can either create a global {@link BlobStore} or a {@link BlobStore} for a specific repository. To create a global
+ * {@link BlobStore} call:
+ *
+ * blobStoreFactory
+ * .withName("name")
+ * .build();
+ *
+ * To create a {@link BlobStore} for a specific repository call:
+ *
+ * blobStoreFactory
+ * .withName("name")
+ * .forRepository(repository)
+ * .build();
+ *
*
* @author Sebastian Sdorra
* @since 1.23
@@ -45,13 +61,68 @@ package sonia.scm.store;
public interface BlobStoreFactory {
/**
- * Returns a {@link BlobStore} with the given name, if the {@link BlobStore}
- * with the given name does not exists the factory will create a new one.
+ * Creates a new or gets an existing {@link BlobStore}. Instead of calling this method you should use the floating API
+ * from {@link #withName(String)}.
*
- *
- * @param name name of the {@link BlobStore}
- *
- * @return {@link BlobStore} with the given name
+ * @param storeParameters The parameters for the blob store.
+ * @return A new or an existing {@link BlobStore} for the given parameters.
*/
- public BlobStore getBlobStore(String name);
+ BlobStore getStore(final StoreParameters storeParameters);
+
+ /**
+ * Use this to create a new or get an existing {@link BlobStore} with a floating API.
+ * @param name The name for the {@link BlobStore}.
+ * @return Floating API to either specify a repository or directly build a global {@link BlobStore}.
+ */
+ default FloatingStoreParameters.Builder withName(String name) {
+ return new FloatingStoreParameters(this).new Builder(name);
+ }
+}
+
+final class FloatingStoreParameters implements StoreParameters {
+
+ private String name;
+ private Repository repository;
+
+ private final BlobStoreFactory factory;
+
+ FloatingStoreParameters(BlobStoreFactory factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public class Builder {
+
+ Builder(String name) {
+ FloatingStoreParameters.this.name = name;
+ }
+
+ /**
+ * Use this to create or get a {@link BlobStore} for a specific repository. This step is optional. If you want to
+ * have a global {@link BlobStore}, omit this.
+ * @param repository The optional repository for the {@link BlobStore}.
+ * @return Floating API to finish the call.
+ */
+ public FloatingStoreParameters.Builder forRepository(Repository repository) {
+ FloatingStoreParameters.this.repository = repository;
+ return this;
+ }
+
+ /**
+ * Creates or gets the {@link BlobStore} with the given name and (if specified) the given repository. If no
+ * repository is given, the {@link BlobStore} will be global.
+ */
+ public BlobStore build(){
+ return factory.getStore(FloatingStoreParameters.this);
+ }
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java
index 7cfebd69c1..80f9cb3df9 100644
--- a/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java
+++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationEntryStoreFactory.java
@@ -32,31 +32,104 @@
package sonia.scm.store;
+import sonia.scm.repository.Repository;
+
/**
- * The ConfigurationEntryStoreFactory can be used to create new or get existing
- * {@link ConfigurationEntryStore}s. Note: the default implementation
- * uses the same location as the {@link StoreFactory}, so be sure that the
- * store names are unique for all {@link ConfigurationEntryStore}s and
- * {@link Store}s.
- *
+ * The ConfigurationEntryStoreFactory can be used to create new or get existing {@link ConfigurationEntryStore}s.
+ *
+ * Note: the default implementation uses the same location as the {@link ConfigurationStoreFactory}, so be sure
+ * that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationEntryStore}s.
+ *
+ * You can either create a global {@link ConfigurationEntryStore} or a {@link ConfigurationEntryStore} for a specific
+ * repository. To create a global {@link ConfigurationEntryStore} call:
+ *
+ * configurationEntryStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .build();
+ *
+ * To create a {@link ConfigurationEntryStore} for a specific repository call:
+ *
+ * configurationEntryStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .forRepository(repository)
+ * .build();
+ *
+ *
* @author Sebastian Sdorra
* @since 1.31
*
* @apiviz.landmark
* @apiviz.uses sonia.scm.store.ConfigurationEntryStore
*/
-public interface ConfigurationEntryStoreFactory
-{
+public interface ConfigurationEntryStoreFactory {
/**
- * Get an existing {@link ConfigurationEntryStore} or create a new one.
+ * Creates a new or gets an existing {@link ConfigurationEntryStore}. Instead of calling this method you should use
+ * the floating API from {@link #withType(Class)}.
*
- *
- * @param type type of the store objects
- * @param name name of the store
- * @param type of the store objects
- *
- * @return {@link ConfigurationEntryStore} with given name and type
+ * @param storeParameters The parameters for the {@link ConfigurationEntryStore}.
+ * @return A new or an existing {@link ConfigurationEntryStore} for the given parameters.
*/
- public ConfigurationEntryStore getStore(Class type, String name);
+ ConfigurationEntryStore getStore(final TypedStoreParameters storeParameters);
+
+ /**
+ * Use this to create a new or get an existing {@link ConfigurationEntryStore} with a floating API.
+ * @param type The type for the {@link ConfigurationEntryStore}.
+ * @return Floating API to set the name and either specify a repository or directly build a global
+ * {@link ConfigurationEntryStore}.
+ */
+ default TypedFloatingConfigurationEntryStoreParameters.Builder withType(Class type) {
+ return new TypedFloatingConfigurationEntryStoreParameters(this).new Builder(type);
+ }
+}
+
+final class TypedFloatingConfigurationEntryStoreParameters {
+
+ private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>();
+ private final ConfigurationEntryStoreFactory factory;
+
+ TypedFloatingConfigurationEntryStoreParameters(ConfigurationEntryStoreFactory factory) {
+ this.factory = factory;
+ }
+
+ public class Builder {
+
+ Builder(Class type) {
+ parameters.setType(type);
+ }
+
+ /**
+ * Use this to set the name for the {@link ConfigurationEntryStore}.
+ * @param name The name for the {@link ConfigurationEntryStore}.
+ * @return Floating API to either specify a repository or directly build a global {@link ConfigurationEntryStore}.
+ */
+ public OptionalRepositoryBuilder withName(String name) {
+ parameters.setName(name);
+ return new OptionalRepositoryBuilder();
+ }
+ }
+
+ public class OptionalRepositoryBuilder {
+
+ /**
+ * Use this to create or get a {@link ConfigurationEntryStore} for a specific repository. This step is optional. If
+ * you want to have a global {@link ConfigurationEntryStore}, omit this.
+ * @param repository The optional repository for the {@link ConfigurationEntryStore}.
+ * @return Floating API to finish the call.
+ */
+ public OptionalRepositoryBuilder forRepository(Repository repository) {
+ parameters.setRepository(repository);
+ return this;
+ }
+
+ /**
+ * Creates or gets the {@link ConfigurationEntryStore} with the given name and (if specified) the given repository.
+ * If no repository is given, the {@link ConfigurationEntryStore} will be global.
+ */
+ public ConfigurationEntryStore build(){
+ return factory.getStore(parameters);
+ }
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java
index a7f21dd304..b1c38f1a00 100644
--- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java
+++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java
@@ -33,6 +33,10 @@
package sonia.scm.store;
+import java.util.Optional;
+
+import static java.util.Optional.ofNullable;
+
/**
* ConfigurationStore for configuration objects. Note: the default
* implementation use JAXB to marshall the configuration objects.
@@ -50,7 +54,17 @@ public interface ConfigurationStore
*
* @return configuration object from store
*/
- public T get();
+ T get();
+
+ /**
+ * Returns the configuration object from store.
+ *
+ *
+ * @return configuration object from store
+ */
+ default Optional getOptional() {
+ return ofNullable(get());
+ }
//~--- set methods ----------------------------------------------------------
@@ -58,7 +72,7 @@ public interface ConfigurationStore
* Stores the given configuration object to the store.
*
*
- * @param obejct configuration object to store
+ * @param object configuration object to store
*/
- public void set(T obejct);
+ void set(T object);
}
diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java
index d9a97de98d..6624f307e7 100644
--- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java
+++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStoreFactory.java
@@ -33,27 +33,103 @@
package sonia.scm.store;
+import sonia.scm.repository.Repository;
+
/**
- * The ConfigurationStoreFactory can be used to create new or get existing
- * {@link ConfigurationStore} objects.
+ * The ConfigurationStoreFactory can be used to create new or get existing {@link ConfigurationStore} objects.
+ *
+ * Note: the default implementation uses the same location as the {@link ConfigurationEntryStoreFactory}, so be
+ * sure that the store names are unique for all {@link ConfigurationEntryStore}s and {@link ConfigurationStore}s.
+ *
+ * You can either create a global {@link ConfigurationStore} or a {@link ConfigurationStore} for a specific repository.
+ * To create a global {@link ConfigurationStore} call:
+ *
+ * configurationStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .build();
+ *
+ * To create a {@link ConfigurationStore} for a specific repository call:
+ *
+ * configurationStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .forRepository(repository)
+ * .build();
+ *
*
* @author Sebastian Sdorra
*
* @apiviz.landmark
* @apiviz.uses sonia.scm.store.ConfigurationStore
*/
-public interface ConfigurationStoreFactory
-{
+public interface ConfigurationStoreFactory {
/**
- * Get an existing {@link ConfigurationStore} or create a new one.
+ * Creates a new or gets an existing {@link ConfigurationStore}. Instead of calling this method you should use the
+ * floating API from {@link #withType(Class)}.
*
- *
- * @param type type of the store objects
- * @param name name of the store
- * @param type of the store objects
- *
- * @return {@link ConfigurationStore} of the given type and name
+ * @param storeParameters The parameters for the {@link ConfigurationStore}.
+ * @return A new or an existing {@link ConfigurationStore} for the given parameters.
*/
- public ConfigurationStore getStore(Class type, String name);
+ ConfigurationStore getStore(final TypedStoreParameters storeParameters);
+
+ /**
+ * Use this to create a new or get an existing {@link ConfigurationStore} with a floating API.
+ * @param type The type for the {@link ConfigurationStore}.
+ * @return Floating API to set the name and either specify a repository or directly build a global
+ * {@link ConfigurationStore}.
+ */
+ default TypedFloatingConfigurationStoreParameters.Builder withType(Class type) {
+ return new TypedFloatingConfigurationStoreParameters(this).new Builder(type);
+ }
+}
+
+final class TypedFloatingConfigurationStoreParameters {
+
+ private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>();
+ private final ConfigurationStoreFactory factory;
+
+ TypedFloatingConfigurationStoreParameters(ConfigurationStoreFactory factory) {
+ this.factory = factory;
+ }
+
+ public class Builder {
+
+ Builder(Class type) {
+ parameters.setType(type);
+ }
+
+ /**
+ * Use this to set the name for the {@link ConfigurationStore}.
+ * @param name The name for the {@link ConfigurationStore}.
+ * @return Floating API to either specify a repository or directly build a global {@link ConfigurationStore}.
+ */
+ public OptionalRepositoryBuilder withName(String name) {
+ parameters.setName(name);
+ return new OptionalRepositoryBuilder();
+ }
+ }
+
+ public class OptionalRepositoryBuilder {
+
+ /**
+ * Use this to create or get a {@link ConfigurationStore} for a specific repository. This step is optional. If you
+ * want to have a global {@link ConfigurationStore}, omit this.
+ * @param repository The optional repository for the {@link ConfigurationStore}.
+ * @return Floating API to finish the call.
+ */
+ public OptionalRepositoryBuilder forRepository(Repository repository) {
+ parameters.setRepository(repository);
+ return this;
+ }
+
+ /**
+ * Creates or gets the {@link ConfigurationStore} with the given name and (if specified) the given repository. If no
+ * repository is given, the {@link ConfigurationStore} will be global.
+ */
+ public ConfigurationStore build(){
+ return factory.getStore(parameters);
+ }
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java
index caed974ee4..564c339d3d 100644
--- a/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java
+++ b/scm-core/src/main/java/sonia/scm/store/DataStoreFactory.java
@@ -32,9 +32,27 @@
package sonia.scm.store;
+import sonia.scm.repository.Repository;
+
/**
- * The DataStoreFactory can be used to create new or get existing
- * {@link DataStore}s.
+ * The DataStoreFactory can be used to create new or get existing {@link DataStore}s.
+ *
+ * You can either create a global {@link DataStore} or a {@link DataStore} for a specific repository.
+ * To create a global {@link DataStore} call:
+ *
+ * dataStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .build();
+ *
+ * To create a {@link DataStore} for a specific repository call:
+ *
+ * dataStoreFactory
+ * .withType(PersistedType.class)
+ * .withName("name")
+ * .forRepository(repository)
+ * .build();
+ *
*
* @author Sebastian Sdorra
* @since 1.23
@@ -45,14 +63,70 @@ package sonia.scm.store;
public interface DataStoreFactory {
/**
- * Get an existing {@link DataStore} or create a new one.
+ * Creates a new or gets an existing {@link DataStore}. Instead of calling this method you should use the
+ * floating API from {@link #withType(Class)}.
*
- *
- * @param type type of the store objects
- * @param name name of the store
- * @param type of the store objects
- *
- * @return {@link DataStore} with given name and type
+ * @param storeParameters The parameters for the {@link DataStore}.
+ * @return A new or an existing {@link DataStore} for the given parameters.
*/
- public DataStore getStore(Class type, String name);
+ DataStore getStore(final TypedStoreParameters storeParameters);
+
+ /**
+ * Use this to create a new or get an existing {@link DataStore} with a floating API.
+ * @param type The type for the {@link DataStore}.
+ * @return Floating API to set the name and either specify a repository or directly build a global
+ * {@link DataStore}.
+ */
+ default TypedFloatingDataStoreParameters.Builder withType(Class type) {
+ return new TypedFloatingDataStoreParameters(this).new Builder(type);
+ }
+}
+
+final class TypedFloatingDataStoreParameters {
+
+ private final TypedStoreParametersImpl parameters = new TypedStoreParametersImpl<>();
+ private final DataStoreFactory factory;
+
+ TypedFloatingDataStoreParameters(DataStoreFactory factory) {
+ this.factory = factory;
+ }
+
+ public class Builder {
+
+ Builder(Class type) {
+ parameters.setType(type);
+ }
+
+ /**
+ * Use this to set the name for the {@link DataStore}.
+ * @param name The name for the {@link DataStore}.
+ * @return Floating API to either specify a repository or directly build a global {@link DataStore}.
+ */
+ public OptionalRepositoryBuilder withName(String name) {
+ parameters.setName(name);
+ return new OptionalRepositoryBuilder();
+ }
+ }
+
+ public class OptionalRepositoryBuilder {
+
+ /**
+ * Use this to create or get a {@link DataStore} for a specific repository. This step is optional. If you
+ * want to have a global {@link DataStore}, omit this.
+ * @param repository The optional repository for the {@link DataStore}.
+ * @return Floating API to finish the call.
+ */
+ public OptionalRepositoryBuilder forRepository(Repository repository) {
+ parameters.setRepository(repository);
+ return this;
+ }
+
+ /**
+ * Creates or gets the {@link DataStore} with the given name and (if specified) the given repository. If no
+ * repository is given, the {@link DataStore} will be global.
+ */
+ public DataStore build(){
+ return factory.getStore(parameters);
+ }
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java b/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java
index 9a35cee0e0..c1a8863758 100644
--- a/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java
+++ b/scm-core/src/main/java/sonia/scm/store/MultiEntryStore.java
@@ -32,6 +32,10 @@
package sonia.scm.store;
+import java.util.Optional;
+
+import static java.util.Optional.ofNullable;
+
/**
* Base class for {@link BlobStore} and {@link DataStore}.
*
@@ -67,4 +71,16 @@ public interface MultiEntryStore {
* @return item with the given id
*/
public T get(String id);
+
+ /**
+ * Returns the item with the given id from the store.
+ *
+ *
+ * @param id id of the item to return
+ *
+ * @return item with the given id
+ */
+ default Optional getOptional(String id) {
+ return ofNullable(get(id));
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/StoreParameters.java b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java
new file mode 100644
index 0000000000..da8ee4c916
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/StoreParameters.java
@@ -0,0 +1,16 @@
+package sonia.scm.store;
+
+import sonia.scm.repository.Repository;
+
+/**
+ * The fields of the {@link StoreParameters} are used from the {@link BlobStoreFactory} to create a store.
+ *
+ * @author Mohamed Karray
+ * @since 2.0.0
+ */
+public interface StoreParameters {
+
+ String getName();
+
+ Repository getRepository();
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java
new file mode 100644
index 0000000000..116bccac41
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParameters.java
@@ -0,0 +1,19 @@
+package sonia.scm.store;
+
+import sonia.scm.repository.Repository;
+
+/**
+ * The fields of the {@link TypedStoreParameters} are used from the {@link ConfigurationStoreFactory},
+ * {@link ConfigurationEntryStoreFactory} and {@link DataStoreFactory} to create a type safe store.
+ *
+ * @author Mohamed Karray
+ * @since 2.0.0
+ */
+public interface TypedStoreParameters {
+
+ Class getType();
+
+ String getName();
+
+ Repository getRepository();
+}
diff --git a/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java
new file mode 100644
index 0000000000..50ce6a496b
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/store/TypedStoreParametersImpl.java
@@ -0,0 +1,36 @@
+package sonia.scm.store;
+
+import sonia.scm.repository.Repository;
+
+class TypedStoreParametersImpl implements TypedStoreParameters {
+ private Class type;
+ private String name;
+ private Repository repository;
+
+ @Override
+ public Class getType() {
+ return type;
+ }
+
+ void setType(Class type) {
+ this.type = type;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Repository getRepository() {
+ return repository;
+ }
+
+ void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
index caa35e0b88..b0f8117e82 100644
--- a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
+++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java
@@ -1,12 +1,13 @@
package sonia.scm.user;
+import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry;
-import sonia.scm.ExceptionWithContext;
-public class ChangePasswordNotAllowedException extends ExceptionWithContext {
+@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
+public class ChangePasswordNotAllowedException extends BadRequestException {
private static final String CODE = "9BR7qpDAe1";
- public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
+ public static final String WRONG_USER_TYPE = "Users of type %s are not allowed to change password";
public ChangePasswordNotAllowedException(ContextEntry.ContextBuilder context, String type) {
super(context.build(), String.format(WRONG_USER_TYPE, type));
diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
index 93a6a7c1d1..6f1bfd9954 100644
--- a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
+++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java
@@ -1,9 +1,10 @@
package sonia.scm.user;
+import sonia.scm.BadRequestException;
import sonia.scm.ContextEntry;
-import sonia.scm.ExceptionWithContext;
-public class InvalidPasswordException extends ExceptionWithContext {
+@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
+public class InvalidPasswordException extends BadRequestException {
private static final String CODE = "8YR7aawFW1";
diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java
index cae383a402..3c185ae3b8 100644
--- a/scm-core/src/main/java/sonia/scm/user/User.java
+++ b/scm-core/src/main/java/sonia/scm/user/User.java
@@ -59,7 +59,9 @@ import java.security.Principal;
@StaticPermissions(
value = "user",
globalPermissions = {"create", "list", "autocomplete"},
- permissions = {"read", "modify", "delete", "changePassword"})
+ permissions = {"read", "modify", "delete", "changePassword"},
+ custom = true, customGlobal = true
+)
@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
diff --git a/scm-core/src/main/java/sonia/scm/util/Comparables.java b/scm-core/src/main/java/sonia/scm/util/Comparables.java
new file mode 100644
index 0000000000..1fb0c5e358
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/util/Comparables.java
@@ -0,0 +1,88 @@
+package sonia.scm.util;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public final class Comparables {
+
+ private static final CacheLoader beanInfoCacheLoader = new CacheLoader() {
+ @Override
+ public BeanInfo load(Class type) throws IntrospectionException {
+ return Introspector.getBeanInfo(type);
+ }
+ };
+
+ private static final LoadingCache beanInfoCache = CacheBuilder.newBuilder()
+ .maximumSize(50) // limit the cache to avoid consuming to much memory on miss usage
+ .build(beanInfoCacheLoader);
+
+ private Comparables() {
+ }
+
+ public static Comparator comparator(Class type, String sortBy) {
+ BeanInfo info = createBeanInfo(type);
+ PropertyDescriptor propertyDescriptor = findPropertyDescriptor(sortBy, info);
+
+ Method readMethod = propertyDescriptor.getReadMethod();
+ checkIfPropertyIsComparable(readMethod, sortBy);
+
+ return new MethodComparator<>(readMethod);
+ }
+
+ private static void checkIfPropertyIsComparable(Method readMethod, String sortBy) {
+ checkArgument(isReturnTypeComparable(readMethod), "property %s is not comparable", sortBy);
+ }
+
+ private static boolean isReturnTypeComparable(Method readMethod) {
+ return Comparable.class.isAssignableFrom(readMethod.getReturnType());
+ }
+
+ private static PropertyDescriptor findPropertyDescriptor(String sortBy, BeanInfo info) {
+ PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
+
+ Optional sortByPropertyDescriptor = Arrays.stream(propertyDescriptors)
+ .filter(p -> p.getName().equals(sortBy))
+ .findFirst();
+
+ return sortByPropertyDescriptor.orElseThrow(() -> new IllegalArgumentException("could not find property " + sortBy));
+ }
+
+ private static BeanInfo createBeanInfo(Class type) {
+ return beanInfoCache.getUnchecked(type);
+ }
+
+ private static class MethodComparator implements Comparator {
+
+ private final Method readMethod;
+
+ private MethodComparator(Method readMethod) {
+ this.readMethod = readMethod;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public int compare(T left, T right) {
+ try {
+ Comparable leftResult = (Comparable) readMethod.invoke(left);
+ Comparable rightResult = (Comparable) readMethod.invoke(right);
+ return leftResult.compareTo(rightResult);
+ } catch (IllegalAccessException | InvocationTargetException ex) {
+ throw new IllegalArgumentException("failed to invoke read method", ex);
+ }
+ }
+ }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java b/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java
new file mode 100644
index 0000000000..2cb4674d24
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/web/AbstractRepositoryJsonEnricher.java
@@ -0,0 +1,40 @@
+package sonia.scm.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static java.util.Collections.singletonMap;
+import static sonia.scm.web.VndMediaType.REPOSITORY;
+import static sonia.scm.web.VndMediaType.REPOSITORY_COLLECTION;
+
+public abstract class AbstractRepositoryJsonEnricher extends JsonEnricherBase {
+
+ public AbstractRepositoryJsonEnricher(ObjectMapper objectMapper) {
+ super(objectMapper);
+ }
+
+ @Override
+ public void enrich(JsonEnricherContext context) {
+ if (resultHasMediaType(REPOSITORY, context)) {
+ JsonNode repositoryNode = context.getResponseEntity();
+ enrichRepositoryNode(repositoryNode);
+ } else if (resultHasMediaType(REPOSITORY_COLLECTION, context)) {
+ JsonNode repositoryCollectionNode = context.getResponseEntity().get("_embedded").withArray("repositories");
+ repositoryCollectionNode.elements().forEachRemaining(this::enrichRepositoryNode);
+ }
+ }
+
+ private void enrichRepositoryNode(JsonNode repositoryNode) {
+ String namespace = repositoryNode.get("namespace").asText();
+ String name = repositoryNode.get("name").asText();
+
+ enrichRepositoryNode(repositoryNode, namespace, name);
+ }
+
+ protected abstract void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name);
+
+ protected void addLink(JsonNode repositoryNode, String linkName, String link) {
+ JsonNode hrefNode = createObject(singletonMap("href", value(link)));
+ addPropertyNode(repositoryNode.get("_links"), linkName, hrefNode);
+ }
+}
diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
index 1baecb62af..6bdd321c86 100644
--- a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
+++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
@@ -15,7 +15,7 @@ public abstract class JsonEnricherBase implements JsonEnricher {
}
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
- return mediaType.equals(context.getResponseMediaType().toString());
+ return mediaType.equalsIgnoreCase(context.getResponseMediaType().toString());
}
protected JsonNode value(Object object) {
diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
index e2a2218d34..19859b876b 100644
--- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
+++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java
@@ -20,7 +20,7 @@ public class VndMediaType {
public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
- public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
+ public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
@@ -33,6 +33,7 @@ public class VndMediaType {
public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX;
public static final String CONFIG = PREFIX + "config" + SUFFIX;
+ public static final String REPOSITORY_PERMISSION_COLLECTION = PREFIX + "repositoryPermissionCollection" + SUFFIX;
public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
@@ -41,6 +42,9 @@ public class VndMediaType {
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
@SuppressWarnings("squid:S2068")
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
+ public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX;
+ public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
+ public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
index ffe6ecc787..c6a8463998 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java
@@ -128,7 +128,7 @@ public class AuthenticationFilter extends HttpFilter
}
else if (subject.isAuthenticated())
{
- logger.trace("user is allready authenticated");
+ logger.trace("user is already authenticated");
processChain(request, response, chain, subject);
}
else if (isAnonymousAccessEnabled())
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
index 328494a626..a062fdb360 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
@@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
}
else
{
- permitted = RepositoryPermissions.read(repository).isPermitted();
+ permitted = RepositoryPermissions.pull(repository).isPermitted();
}
return permitted;
diff --git a/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java b/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java
index 81e973f379..f900ceb234 100644
--- a/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java
+++ b/scm-core/src/main/java/sonia/scm/xml/IndentXMLStreamWriter.java
@@ -45,7 +45,7 @@ import javax.xml.stream.XMLStreamWriter;
* @author Sebastian Sdorra
* @since 1.31
*/
-public final class IndentXMLStreamWriter implements XMLStreamWriter
+public final class IndentXMLStreamWriter implements XMLStreamWriter, AutoCloseable
{
/** line separator */
@@ -475,7 +475,7 @@ public final class IndentXMLStreamWriter implements XMLStreamWriter
//~--- fields ---------------------------------------------------------------
/** indent string */
- private String indent = " ";
+ private String indent = " ";
/** current level */
private int level = 0;
diff --git a/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
new file mode 100644
index 0000000000..9b8d718851
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
@@ -0,0 +1,25 @@
+package sonia.scm.xml;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+
+/**
+ * JAXB adapter for {@link Instant} objects.
+ *
+ * @since 2.0.0
+ */
+public class XmlInstantAdapter extends XmlAdapter {
+
+ @Override
+ public String marshal(Instant instant) {
+ return DateTimeFormatter.ISO_INSTANT.format(instant);
+ }
+
+ @Override
+ public Instant unmarshal(String text) {
+ TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text);
+ return Instant.from(parsed);
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java
new file mode 100644
index 0000000000..4fb9dfd4fa
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/BasicContextProviderTest.java
@@ -0,0 +1,44 @@
+package sonia.scm;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junitpioneer.jupiter.TempDirectory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(TempDirectory.class)
+class BasicContextProviderTest {
+
+ private Path baseDirectory;
+
+ private BasicContextProvider context;
+
+ @BeforeEach
+ void setUpContext(@TempDirectory.TempDir Path baseDirectory) {
+ this.baseDirectory = baseDirectory;
+ context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION);
+ }
+
+ @Test
+ void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) {
+ Path absolutePath = path.toAbsolutePath();
+ Path resolved = context.resolve(absolutePath);
+
+ assertThat(resolved).isSameAs(absolutePath);
+ }
+
+ @Test
+ void shouldResolveRelatePath() {
+ Path path = Paths.get("repos", "42");
+ Path resolved = context.resolve(path);
+
+ assertThat(resolved).isAbsolute();
+ assertThat(resolved).startsWithRaw(baseDirectory);
+ assertThat(resolved).endsWithRaw(path);
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java
new file mode 100644
index 0000000000..ff658cc26a
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalAppenderMapperTest.java
@@ -0,0 +1,74 @@
+package sonia.scm.api.v2.resources;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class HalAppenderMapperTest {
+
+ @Mock
+ private HalAppender appender;
+
+ private HalEnricherRegistry registry;
+ private HalAppenderMapper mapper;
+
+ @BeforeEach
+ void beforeEach() {
+ registry = new HalEnricherRegistry();
+ mapper = new HalAppenderMapper();
+ mapper.setRegistry(registry);
+ }
+
+ @Test
+ void shouldAppendSimpleLink() {
+ registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
+
+ mapper.applyEnrichers(appender, "hello");
+
+ verify(appender).appendLink("42", "https://hitchhiker.com");
+ }
+
+ @Test
+ void shouldCallMultipleEnrichers() {
+ registry.register(String.class, (ctx, appender) -> appender.appendLink("42", "https://hitchhiker.com"));
+ registry.register(String.class, (ctx, appender) -> appender.appendLink("21", "https://scm.hitchhiker.com"));
+
+ mapper.applyEnrichers(appender, "hello");
+
+ verify(appender).appendLink("42", "https://hitchhiker.com");
+ verify(appender).appendLink("21", "https://scm.hitchhiker.com");
+ }
+
+ @Test
+ void shouldAppendLinkByUsingSourceFromContext() {
+ registry.register(String.class, (ctx, appender) -> {
+ Optional rel = ctx.oneByType(String.class);
+ appender.appendLink(rel.get(), "https://hitchhiker.com");
+ });
+
+ mapper.applyEnrichers(appender, "42");
+
+ verify(appender).appendLink("42", "https://hitchhiker.com");
+ }
+
+ @Test
+ void shouldAppendLinkByUsingMultipleContextEntries() {
+ registry.register(Integer.class, (ctx, appender) -> {
+ Optional rel = ctx.oneByType(Integer.class);
+ Optional href = ctx.oneByType(String.class);
+ appender.appendLink(String.valueOf(rel.get()), href.get());
+ });
+
+ mapper.applyEnrichers(appender, Integer.valueOf(42), "https://hitchhiker.com");
+
+ verify(appender).appendLink("42", "https://hitchhiker.com");
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java
new file mode 100644
index 0000000000..1aecb5ad46
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherContextTest.java
@@ -0,0 +1,44 @@
+package sonia.scm.api.v2.resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.NoSuchElementException;
+
+class HalEnricherContextTest {
+
+ @Test
+ void shouldCreateContextFromSingleObject() {
+ HalEnricherContext context = HalEnricherContext.of("hello");
+ assertThat(context.oneByType(String.class)).contains("hello");
+ }
+
+ @Test
+ void shouldCreateContextFromMultipleObjects() {
+ HalEnricherContext context = HalEnricherContext.of("hello", Integer.valueOf(42), Long.valueOf(21L));
+ assertThat(context.oneByType(String.class)).contains("hello");
+ assertThat(context.oneByType(Integer.class)).contains(42);
+ assertThat(context.oneByType(Long.class)).contains(21L);
+ }
+
+ @Test
+ void shouldReturnEmptyOptionalForUnknownTypes() {
+ HalEnricherContext context = HalEnricherContext.of();
+ assertThat(context.oneByType(String.class)).isNotPresent();
+ }
+
+ @Test
+ void shouldReturnRequiredObject() {
+ HalEnricherContext context = HalEnricherContext.of("hello");
+ assertThat(context.oneRequireByType(String.class)).isEqualTo("hello");
+ }
+
+ @Test
+ void shouldThrowAnNoSuchElementExceptionForUnknownTypes() {
+ HalEnricherContext context = HalEnricherContext.of();
+ assertThrows(NoSuchElementException.class, () -> context.oneRequireByType(String.class));
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java
new file mode 100644
index 0000000000..6a863d2f04
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/api/v2/resources/HalEnricherRegistryTest.java
@@ -0,0 +1,60 @@
+package sonia.scm.api.v2.resources;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HalEnricherRegistryTest {
+
+ private HalEnricherRegistry registry;
+
+ @BeforeEach
+ void setUpObjectUnderTest() {
+ registry = new HalEnricherRegistry();
+ }
+
+ @Test
+ void shouldRegisterTheEnricher() {
+ SampleHalEnricher enricher = new SampleHalEnricher();
+ registry.register(String.class, enricher);
+
+ Iterable enrichers = registry.allByType(String.class);
+ assertThat(enrichers).containsOnly(enricher);
+ }
+
+ @Test
+ void shouldRegisterMultipleEnrichers() {
+ SampleHalEnricher one = new SampleHalEnricher();
+ registry.register(String.class, one);
+
+ SampleHalEnricher two = new SampleHalEnricher();
+ registry.register(String.class, two);
+
+ Iterable enrichers = registry.allByType(String.class);
+ assertThat(enrichers).containsOnly(one, two);
+ }
+
+ @Test
+ void shouldRegisterEnrichersForDifferentTypes() {
+ SampleHalEnricher one = new SampleHalEnricher();
+ registry.register(String.class, one);
+
+ SampleHalEnricher two = new SampleHalEnricher();
+ registry.register(Integer.class, two);
+
+ Iterable enrichers = registry.allByType(String.class);
+ assertThat(enrichers).containsOnly(one);
+
+ enrichers = registry.allByType(Integer.class);
+ assertThat(enrichers).containsOnly(two);
+ }
+
+ private static class SampleHalEnricher implements HalEnricher {
+ @Override
+ public void enrich(HalEnricherContext context, HalAppender appender) {
+
+ }
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java
new file mode 100644
index 0000000000..c3648fd4b8
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/filter/GZipResponseFilterTest.java
@@ -0,0 +1,87 @@
+package sonia.scm.filter;
+
+import com.google.inject.util.Providers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.WriterInterceptorContext;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.GZIPOutputStream;
+
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class GZipResponseFilterTest {
+
+ @Mock
+ private HttpServletRequest request;
+
+ @Mock
+ private WriterInterceptorContext context;
+
+ @Mock
+ private MultivaluedMap headers;
+
+ private GZipResponseFilter filter;
+
+ @BeforeEach
+ void setupObjectUnderTest() {
+ filter = new GZipResponseFilter(Providers.of(request));
+ }
+
+ @Test
+ void shouldSkipGZipCompression() throws IOException {
+ when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("deflate, br");
+
+ filter.aroundWriteTo(context);
+
+ verifySkipped();
+ }
+
+ @Test
+ void shouldSkipGZipCompressionWithoutAcceptEncodingHeader() throws IOException {
+ filter.aroundWriteTo(context);
+
+ verifySkipped();
+ }
+
+ private void verifySkipped() throws IOException {
+ verify(context, never()).getOutputStream();
+ verify(context).proceed();
+ }
+
+
+ @Nested
+ class AcceptGZipEncoding {
+
+ @BeforeEach
+ void setUpContext() {
+ when(request.getHeader(HttpHeaders.ACCEPT_ENCODING)).thenReturn("gzip, deflate, br");
+ when(context.getHeaders()).thenReturn(headers);
+ when(context.getOutputStream()).thenReturn(new ByteArrayOutputStream());
+ }
+
+ @Test
+ void shouldEncode() throws IOException {
+ filter.aroundWriteTo(context);
+
+ verify(headers).remove(HttpHeaders.CONTENT_LENGTH);
+ verify(headers).add(HttpHeaders.CONTENT_ENCODING, "gzip");
+
+ verify(context).setOutputStream(any(GZIPOutputStream.class));
+ verify(context, times(2)).setOutputStream(any(OutputStream.class));
+ }
+
+ }
+
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java
index b7fa9ae84a..92ca488ddf 100644
--- a/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java
+++ b/scm-core/src/test/java/sonia/scm/net/ahc/AdvancedHttpRequestWithBodyTest.java
@@ -36,7 +36,7 @@ import com.google.common.io.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
diff --git a/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java
new file mode 100644
index 0000000000..e4cd22b060
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java
@@ -0,0 +1,45 @@
+package sonia.scm.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith({MockitoExtension.class})
+class InitialRepositoryLocationResolverTest {
+
+ private InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver();
+
+ @Test
+ void shouldComputeInitialPath() {
+ Path path = resolver.getPath("42");
+
+ assertThat(path).isRelative();
+ assertThat(path.toString()).isEqualTo("repositories" + File.separator + "42");
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionIfIdHasASlash() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("../../../passwd"));
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionIfIdHasABackSlash() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("..\\..\\..\\users.ntlm"));
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionIfIdIsDotDot() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath(".."));
+ }
+
+ @Test
+ void shouldThrowIllegalArgumentExceptionIfIdIsDot() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("."));
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java
new file mode 100644
index 0000000000..05f9af3773
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryLocationResolverTest.java
@@ -0,0 +1,65 @@
+package sonia.scm.repository;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+import sonia.scm.SCMContextProvider;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith({MockitoExtension.class})
+class RepositoryLocationResolverTest {
+
+ @Mock
+ private SCMContextProvider contextProvider;
+
+ @Mock
+ private PathBasedRepositoryDAO pathBasedRepositoryDAO;
+
+ @Mock
+ private RepositoryDAO repositoryDAO;
+
+ @Mock
+ private InitialRepositoryLocationResolver initialRepositoryLocationResolver;
+
+
+ @BeforeEach
+ void beforeEach() {
+ when(contextProvider.resolve(any(Path.class))).then((Answer) invocationOnMock -> invocationOnMock.getArgument(0));
+ }
+
+ private RepositoryLocationResolver createResolver(RepositoryDAO pathBasedRepositoryDAO) {
+ return new RepositoryLocationResolver(contextProvider, pathBasedRepositoryDAO, initialRepositoryLocationResolver);
+ }
+
+ @Test
+ void shouldReturnPathFromDao() {
+ Path repositoryPath = Paths.get("repos", "42");
+ when(pathBasedRepositoryDAO.getPath("42")).thenReturn(repositoryPath);
+
+ RepositoryLocationResolver resolver = createResolver(pathBasedRepositoryDAO);
+ Path path = resolver.getPath("42");
+
+ assertThat(path).isSameAs(repositoryPath);
+ }
+
+ @Test
+ void shouldReturnInitialPathIfDaoIsNotPathBased() {
+ Path repositoryPath = Paths.get("r", "42");
+ when(initialRepositoryLocationResolver.getPath("42")).thenReturn(repositoryPath);
+
+ RepositoryLocationResolver resolver = createResolver(repositoryDAO);
+ Path path = resolver.getPath("42");
+
+ assertThat(path).isSameAs(repositoryPath);
+ }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java
new file mode 100644
index 0000000000..d65358a66e
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java
@@ -0,0 +1,58 @@
+package sonia.scm.repository;
+
+import org.junit.jupiter.api.Test;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RepositoryPermissionTest {
+
+ @Test
+ void shouldBeEqualWithSameVerbs() {
+ RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
+ RepositoryPermission permission2 = new RepositoryPermission("name", asList("two", "one"), false);
+
+ assertThat(permission1).isEqualTo(permission2);
+ }
+
+ @Test
+ void shouldHaveSameHashCodeWithSameVerbs() {
+ long hash1 = new RepositoryPermission("name", asList("one", "two"), false).hashCode();
+ long hash2 = new RepositoryPermission("name", asList("two", "one"), false).hashCode();
+
+ assertThat(hash1).isEqualTo(hash2);
+ }
+
+ @Test
+ void shouldNotBeEqualWithSameVerbs() {
+ RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
+ RepositoryPermission permission2 = new RepositoryPermission("name", asList("three", "one"), false);
+
+ assertThat(permission1).isNotEqualTo(permission2);
+ }
+
+ @Test
+ void shouldNotBeEqualWithDifferentType() {
+ RepositoryPermission permission1 = new RepositoryPermission("name", asList("one"), false);
+ RepositoryPermission permission2 = new RepositoryPermission("name", asList("one"), true);
+
+ assertThat(permission1).isNotEqualTo(permission2);
+ }
+
+ @Test
+ void shouldNotBeEqualWithDifferentName() {
+ RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one"), false);
+ RepositoryPermission permission2 = new RepositoryPermission("name2", asList("one"), false);
+
+ assertThat(permission1).isNotEqualTo(permission2);
+ }
+
+ @Test
+ void shouldBeEqualWithRedundantVerbs() {
+ RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one", "two"), false);
+ RepositoryPermission permission2 = new RepositoryPermission("name1", asList("one", "two"), false);
+ permission2.setVerbs(asList("one", "two", "two"));
+
+ assertThat(permission1).isEqualTo(permission2);
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryUtilTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryUtilTest.java
deleted file mode 100644
index fabc55b9c9..0000000000
--- a/scm-core/src/test/java/sonia/scm/repository/RepositoryUtilTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package sonia.scm.repository;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-import java.io.File;
-import java.io.IOException;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
-
-@RunWith(MockitoJUnitRunner.class)
-public class RepositoryUtilTest {
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Mock
- private AbstractRepositoryHandler repositoryHandler;
-
- private RepositoryConfig repositoryConfig = new RepositoryConfig() {
- @Override
- public String getId() {
- return "repository";
- }
- };
-
- @Before
- public void setUpMocks() {
- when(repositoryHandler.getConfig()).thenReturn(repositoryConfig);
- }
-
- @Test
- public void testGetRepositoryId() throws IOException {
- File repositoryTypeRoot = temporaryFolder.newFolder();
- repositoryConfig.setRepositoryDirectory(repositoryTypeRoot);
-
- File repository = new File(repositoryTypeRoot, "abc");
- String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
- assertEquals("abc", id);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testGetRepositoryIdWithInvalidPath() throws IOException {
- File repositoryTypeRoot = temporaryFolder.newFolder();
- repositoryConfig.setRepositoryDirectory(repositoryTypeRoot);
-
- File repository = new File("/etc/abc");
- String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
- assertEquals("abc", id);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testGetRepositoryIdWithInvalidPathButSameLength() throws IOException {
- File repositoryTypeRoot = temporaryFolder.newFolder();
- repositoryConfig.setRepositoryDirectory(repositoryTypeRoot);
-
- File repository = new File(temporaryFolder.newFolder(), "abc");
-
- String id = RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
- assertEquals("abc", id);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testGetRepositoryIdWithInvalidId() throws IOException {
- File repositoryTypeRoot = temporaryFolder.newFolder();
- repositoryConfig.setRepositoryDirectory(repositoryTypeRoot);
-
- File repository = new File(repositoryTypeRoot, "abc/123");
- RepositoryUtil.getRepositoryId(repositoryHandler, repository.getPath());
- }
-
-}
diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java
new file mode 100644
index 0000000000..7ddeabd8ac
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java
@@ -0,0 +1,154 @@
+package sonia.scm.security;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.DisabledAccountException;
+import org.apache.shiro.authc.UnknownAccountException;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.group.Group;
+import sonia.scm.group.GroupDAO;
+import sonia.scm.group.GroupNames;
+import sonia.scm.user.User;
+import sonia.scm.user.UserDAO;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DAORealmHelperTest {
+
+ @Mock
+ private LoginAttemptHandler loginAttemptHandler;
+
+ @Mock
+ private UserDAO userDAO;
+
+ @Mock
+ private GroupDAO groupDAO;
+
+ private DAORealmHelper helper;
+
+ @BeforeEach
+ void setUpObjectUnderTest() {
+ helper = new DAORealmHelper(loginAttemptHandler, userDAO, groupDAO, "hitchhiker");
+ }
+
+ @Test
+ void shouldThrowExceptionWithoutUsername() {
+ assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder(null).build());
+ }
+
+ @Test
+ void shouldThrowExceptionWithEmptyUsername() {
+ assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder("").build());
+ }
+
+ @Test
+ void shouldThrowExceptionWithUnknownUser() {
+ assertThrows(UnknownAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build());
+ }
+
+ @Test
+ void shouldThrowExceptionOnDisabledAccount() {
+ User user = new User("trillian");
+ user.setActive(false);
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ assertThrows(DisabledAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build());
+ }
+
+ @Test
+ void shouldReturnAuthenticationInfo() {
+ User user = new User("trillian");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build();
+ PrincipalCollection principals = authenticationInfo.getPrincipals();
+ assertThat(principals.oneByType(User.class)).isSameAs(user);
+ assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
+ assertThat(principals.oneByType(Scope.class)).isEmpty();
+ }
+
+ @Test
+ void shouldReturnAuthenticationInfoWithGroups() {
+ User user = new User("trillian");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ Group one = new Group("xml", "one", "trillian");
+ Group two = new Group("xml", "two", "trillian");
+ Group six = new Group("xml", "six", "dent");
+ when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six));
+
+ AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
+ .withGroups(ImmutableList.of("three"))
+ .build();
+
+ PrincipalCollection principals = authenticationInfo.getPrincipals();
+ assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three");
+ }
+
+ @Test
+ void shouldReturnAuthenticationInfoWithScope() {
+ User user = new User("trillian");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ Scope scope = Scope.valueOf("user:*", "group:*");
+
+ AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
+ .withScope(scope)
+ .build();
+
+ PrincipalCollection principals = authenticationInfo.getPrincipals();
+ assertThat(principals.oneByType(Scope.class)).isSameAs(scope);
+ }
+
+ @Test
+ void shouldReturnAuthenticationInfoWithCredentials() {
+ User user = new User("trillian");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
+ .withCredentials("secret")
+ .build();
+
+ assertThat(authenticationInfo.getCredentials()).isEqualTo("secret");
+ }
+
+ @Test
+ void shouldReturnAuthenticationInfoWithCredentialsFromUser() {
+ User user = new User("trillian");
+ user.setPassword("secret");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build();
+
+ assertThat(authenticationInfo.getCredentials()).isEqualTo("secret");
+ }
+
+ @Test
+ void shouldThrowExceptionWithWrongTypeOfToken() {
+ assertThrows(IllegalArgumentException.class, () -> helper.getAuthenticationInfo(BearerToken.valueOf("__bearer__")));
+ }
+
+ @Test
+ void shouldGetAuthenticationInfo() {
+ User user = new User("trillian");
+ when(userDAO.get("trillian")).thenReturn(user);
+
+ AuthenticationInfo authenticationInfo = helper.getAuthenticationInfo(new UsernamePasswordToken("trillian", "secret"));
+
+ PrincipalCollection principals = authenticationInfo.getPrincipals();
+ assertThat(principals.oneByType(User.class)).isSameAs(user);
+ assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
+ assertThat(principals.oneByType(Scope.class)).isEmpty();
+
+ assertThat(authenticationInfo.getCredentials()).isNull();
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java
deleted file mode 100644
index e8180ca24a..0000000000
--- a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-package sonia.scm.security;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import org.junit.Test;
-
-import sonia.scm.repository.PermissionType;
-
-import static org.junit.Assert.*;
-
-/**
- *
- * @author Sebastian Sdorra
- */
-public class RepositoryPermissionTest
-{
-
- /**
- * Method description
- *
- */
- @Test
- public void testImplies()
- {
- RepositoryPermission p = new RepositoryPermission("asd",
- PermissionType.READ);
-
- assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
- assertFalse(p.implies(new RepositoryPermission("asd",
- PermissionType.OWNER)));
- assertFalse(p.implies(new RepositoryPermission("asd",
- PermissionType.WRITE)));
- p = new RepositoryPermission("asd", PermissionType.OWNER);
- assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
- assertFalse(p.implies(new RepositoryPermission("bdb",
- PermissionType.READ)));
- }
-
- /**
- * Method description
- *
- */
- @Test
- public void testImpliesWithWildcard()
- {
- RepositoryPermission p = new RepositoryPermission("*",
- PermissionType.OWNER);
-
- assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
- assertTrue(p.implies(new RepositoryPermission("bdb",
- PermissionType.OWNER)));
- assertTrue(p.implies(new RepositoryPermission("cgd",
- PermissionType.WRITE)));
- }
-}
diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
index 50bde53ae1..b8d6ae909e 100644
--- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
+++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java
@@ -37,6 +37,7 @@ package sonia.scm.security;
import com.google.common.base.Throwables;
import org.apache.shiro.authc.AuthenticationInfo;
+import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,10 +54,14 @@ import sonia.scm.web.security.PrivilegedAction;
import java.io.IOException;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.util.Arrays.asList;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -106,25 +111,6 @@ public class SyncingRealmHelperTest {
helper = new SyncingRealmHelper(ctx, userManager, groupManager);
}
- /**
- * Tests {@link SyncingRealmHelper#createAuthenticationInfo(String, User, String...)}.
- */
- @Test
- public void testCreateAuthenticationInfo() {
- User user = new User("tricia");
- AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test",
- user, "heartOfGold");
-
- assertNotNull(authInfo);
- assertEquals("tricia", authInfo.getPrincipals().getPrimaryPrincipal());
- assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
- assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
-
- GroupNames groups = authInfo.getPrincipals().oneByType(GroupNames.class);
-
- assertThat(groups, hasItem("heartOfGold"));
- }
-
/**
* Tests {@link SyncingRealmHelper#store(Group)}.
*
@@ -198,4 +184,45 @@ public class SyncingRealmHelperTest {
helper.store(user);
verify(userManager, times(1)).modify(user);
}
+
+ @Test
+ public void builderShouldSetInternalGroups() {
+ AuthenticationInfo authenticationInfo = helper
+ .authenticationInfo()
+ .forRealm("unit-test")
+ .andUser(new User("ziltoid"))
+ .withGroups("internal");
+
+ GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
+ Assertions.assertThat(groupNames.getCollection()).containsOnly("internal");
+ Assertions.assertThat(groupNames.isExternal()).isFalse();
+ }
+
+ @Test
+ public void builderShouldSetExternalGroups() {
+ AuthenticationInfo authenticationInfo = helper
+ .authenticationInfo()
+ .forRealm("unit-test")
+ .andUser(new User("ziltoid"))
+ .withExternalGroups("external");
+
+ GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
+ Assertions.assertThat(groupNames.getCollection()).containsOnly("external");
+ Assertions.assertThat(groupNames.isExternal()).isTrue();
+ }
+
+ @Test
+ public void builderShouldSetValues() {
+ User user = new User("ziltoid");
+ AuthenticationInfo authInfo = helper
+ .authenticationInfo()
+ .forRealm("unit-test")
+ .andUser(user)
+ .withoutGroups();
+
+ assertNotNull(authInfo);
+ assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal());
+ assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
+ assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
+ }
}
diff --git a/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java
new file mode 100644
index 0000000000..50ae2254e6
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/util/ComparablesTest.java
@@ -0,0 +1,57 @@
+package sonia.scm.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Comparator;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ComparablesTest {
+
+ @Test
+ void shouldCompare() {
+ One a = new One("a");
+ One b = new One("b");
+
+ Comparator comparable = Comparables.comparator(One.class, "value");
+ assertThat(comparable.compare(a, b)).isEqualTo(-1);
+ }
+
+ @Test
+ void shouldThrowAnExceptionForNonExistingField() {
+ assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "awesome"));
+ }
+
+ @Test
+ void shouldThrowAnExceptionForNonComparableField() {
+ assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "nonComparable"));
+ }
+
+ @Test
+ void shouldThrowAnExceptionIfTheFieldHasNoGetter() {
+ assertThrows(IllegalArgumentException.class, () -> Comparables.comparator(One.class, "incredible"));
+ }
+
+ private static class One {
+
+ private String value;
+ private String incredible;
+ private NonComparable nonComparable;
+
+ One(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public NonComparable getNonComparable() {
+ return nonComparable;
+ }
+ }
+
+ private static class NonComparable {}
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java b/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java
new file mode 100644
index 0000000000..2c8ef76464
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/web/AbstractRepositoryJsonEnricherTest.java
@@ -0,0 +1,107 @@
+package sonia.scm.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class AbstractRepositoryJsonEnricherTest {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private AbstractRepositoryJsonEnricher linkEnricher;
+ private JsonNode rootNode;
+
+ @BeforeEach
+ void globalSetUp() {
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+ pathInfoStore.set(() -> URI.create("/"));
+
+ linkEnricher = new AbstractRepositoryJsonEnricher(objectMapper) {
+ @Override
+ protected void enrichRepositoryNode(JsonNode repositoryNode, String namespace, String name) {
+ addLink(repositoryNode, "new-link", "/somewhere");
+ }
+ };
+ }
+
+ @Test
+ void shouldEnrichRepositories() throws IOException {
+ URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
+ rootNode = objectMapper.readTree(resource);
+
+ JsonEnricherContext context = new JsonEnricherContext(
+ URI.create("/"),
+ MediaType.valueOf(VndMediaType.REPOSITORY),
+ rootNode
+ );
+
+ linkEnricher.enrich(context);
+
+ String configLink = context.getResponseEntity()
+ .get("_links")
+ .get("new-link")
+ .get("href")
+ .asText();
+
+ assertThat(configLink).isEqualTo("/somewhere");
+ }
+
+ @Test
+ void shouldEnrichAllRepositories() throws IOException {
+ URL resource = Resources.getResource("sonia/scm/repository/repository-collection-001.json");
+ rootNode = objectMapper.readTree(resource);
+
+ JsonEnricherContext context = new JsonEnricherContext(
+ URI.create("/"),
+ MediaType.valueOf(VndMediaType.REPOSITORY_COLLECTION),
+ rootNode
+ );
+
+ linkEnricher.enrich(context);
+
+ context.getResponseEntity()
+ .get("_embedded")
+ .withArray("repositories")
+ .elements()
+ .forEachRemaining(node -> {
+ String configLink = node
+ .get("_links")
+ .get("new-link")
+ .get("href")
+ .asText();
+
+ assertThat(configLink).isEqualTo("/somewhere");
+ });
+ }
+
+ @Test
+ void shouldNotModifyObjectsWithUnsupportedMediaType() throws IOException {
+ URL resource = Resources.getResource("sonia/scm/repository/repository-001.json");
+ rootNode = objectMapper.readTree(resource);
+ JsonEnricherContext context = new JsonEnricherContext(
+ URI.create("/"),
+ MediaType.valueOf(VndMediaType.USER),
+ rootNode
+ );
+
+ linkEnricher.enrich(context);
+
+ boolean hasNewPullRequestLink = context.getResponseEntity()
+ .get("_links")
+ .has("new-link");
+
+ assertThat(hasNewPullRequestLink).isFalse();
+ }
+}
diff --git a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java
index 43ed4940fa..2f53ae8102 100644
--- a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java
+++ b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java
@@ -23,6 +23,14 @@ public class JsonEnricherBaseTest {
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
}
+ @Test
+ public void testResultHasMediaTypeWithCamelCaseMediaType() {
+ String mediaType = "application/hitchhikersGuideToTheGalaxy";
+ JsonEnricherContext context = new JsonEnricherContext(null, MediaType.valueOf(mediaType), null);
+
+ assertThat(enricher.resultHasMediaType(mediaType, context)).isTrue();
+ }
+
@Test
public void testAppendLink() {
ObjectNode root = objectMapper.createObjectNode();
diff --git a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java
index ecfdd06a0f..16c4278793 100644
--- a/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java
+++ b/scm-core/src/test/java/sonia/scm/xml/IndentXMLStreamWriterTest.java
@@ -89,7 +89,7 @@ public class IndentXMLStreamWriterTest
StringBuilder buffer = new StringBuilder("");
buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR);
buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR);
- buffer.append(" Hello");
+ buffer.append(" Hello");
buffer.append(IndentXMLStreamWriter.LINE_SEPARATOR);
buffer.append("").append(IndentXMLStreamWriter.LINE_SEPARATOR);
diff --git a/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java
new file mode 100644
index 0000000000..eb1ea86aee
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java
@@ -0,0 +1,47 @@
+package sonia.scm.xml;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junitpioneer.jupiter.TempDirectory;
+
+import javax.xml.bind.JAXB;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.nio.file.Path;
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(TempDirectory.class)
+class XmlInstantAdapterTest {
+
+ @Test
+ void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) {
+ Path path = tempDirectory.resolve("instant.xml");
+
+ Instant instant = Instant.now();
+ InstantObject object = new InstantObject(instant);
+ JAXB.marshal(object, path.toFile());
+
+ InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class);
+ assertEquals(instant, unmarshaled.instant);
+ }
+
+ @XmlRootElement(name = "instant-object")
+ @XmlAccessorType(XmlAccessType.FIELD)
+ public static class InstantObject {
+
+ @XmlJavaTypeAdapter(XmlInstantAdapter.class)
+ private Instant instant;
+
+ public InstantObject() {
+ }
+
+ InstantObject(Instant instant) {
+ this.instant = instant;
+ }
+ }
+
+}
diff --git a/scm-core/src/test/resources/sonia/scm/repository/repository-001.json b/scm-core/src/test/resources/sonia/scm/repository/repository-001.json
new file mode 100644
index 0000000000..43ea136942
--- /dev/null
+++ b/scm-core/src/test/resources/sonia/scm/repository/repository-001.json
@@ -0,0 +1,42 @@
+{
+ "creationDate": "2018-11-09T09:48:32.732Z",
+ "description": "Handling static webresources made easy",
+ "healthCheckFailures": [],
+ "lastModified": "2018-11-09T09:49:20.973Z",
+ "namespace": "scmadmin",
+ "name": "web-resources",
+ "archived": false,
+ "type": "git",
+ "_links": {
+ "self": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "delete": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "update": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "permissions": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
+ },
+ "protocol": [
+ {
+ "href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
+ "name": "http"
+ }
+ ],
+ "tags": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
+ },
+ "branches": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
+ },
+ "changesets": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
+ },
+ "sources": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
+ }
+ }
+}
diff --git a/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json b/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json
new file mode 100644
index 0000000000..f4eeb24bbc
--- /dev/null
+++ b/scm-core/src/test/resources/sonia/scm/repository/repository-collection-001.json
@@ -0,0 +1,106 @@
+{
+ "page": 0,
+ "pageTotal": 1,
+ "_links": {
+ "self": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
+ },
+ "first": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
+ },
+ "last": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/?page=0&pageSize=10"
+ },
+ "create": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/"
+ }
+ },
+ "_embedded": {
+ "repositories": [
+ {
+ "creationDate": "2018-11-09T09:48:32.732Z",
+ "description": "Handling static webresources made easy",
+ "healthCheckFailures": [],
+ "lastModified": "2018-11-09T09:49:20.973Z",
+ "namespace": "scmadmin",
+ "name": "web-resources",
+ "archived": false,
+ "type": "git",
+ "_links": {
+ "self": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "delete": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "update": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "permissions": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
+ },
+ "protocol": [
+ {
+ "href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
+ "name": "http"
+ }
+ ],
+ "tags": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
+ },
+ "branches": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
+ },
+ "changesets": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
+ },
+ "sources": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
+ }
+ }
+ },
+ {
+ "creationDate": "2018-11-09T09:48:32.732Z",
+ "description": "Handling static webresources made easy",
+ "healthCheckFailures": [],
+ "lastModified": "2018-11-09T09:49:20.973Z",
+ "namespace": "scmadmin",
+ "name": "web-resources",
+ "archived": false,
+ "type": "git",
+ "_links": {
+ "self": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "delete": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "update": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources"
+ },
+ "permissions": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/permissions/"
+ },
+ "protocol": [
+ {
+ "href": "http://localhost:8081/scm/repo/scmadmin/web-resources",
+ "name": "http"
+ }
+ ],
+ "tags": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/tags/"
+ },
+ "branches": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/branches/"
+ },
+ "changesets": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/changesets/"
+ },
+ "sources": {
+ "href": "http://localhost:8081/scm/api/v2/repositories/scmadmin/web-resources/sources/"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini
index fbdd35ba50..fda268ec83 100644
--- a/scm-core/src/test/resources/sonia/scm/shiro.ini
+++ b/scm-core/src/test/resources/sonia/scm/shiro.ini
@@ -8,5 +8,5 @@ unpriv = secret
[roles]
admin = *
user = something:*
-repo_read = "repository:read:1"
-repo_write = "repository:push:1"
+repo_read = "repository:read,pull:1"
+repo_write = "repository:read,write,pull,push:1"
diff --git a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java
index 577d732317..d6b65b41bd 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/group/xml/XmlGroupDAO.java
@@ -64,9 +64,11 @@ public class XmlGroupDAO extends AbstractXmlDAO
* @param storeFactory
*/
@Inject
- public XmlGroupDAO(ConfigurationStoreFactory storeFactory)
- {
- super(storeFactory.getStore(XmlGroupDatabase.class, STORE_NAME));
+ public XmlGroupDAO(ConfigurationStoreFactory storeFactory) {
+ super(storeFactory
+ .withType(XmlGroupDatabase.class)
+ .withName(STORE_NAME)
+ .build());
}
//~--- methods --------------------------------------------------------------
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java
new file mode 100644
index 0000000000..1f5f0e81b6
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/MetadataStore.java
@@ -0,0 +1,50 @@
+package sonia.scm.repository.xml;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.ContextEntry;
+import sonia.scm.repository.InternalRepositoryException;
+import sonia.scm.repository.Repository;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import java.nio.file.Path;
+
+class MetadataStore {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class);
+
+ private final JAXBContext jaxbContext;
+
+ MetadataStore() {
+ try {
+ jaxbContext = JAXBContext.newInstance(Repository.class);
+ } catch (JAXBException ex) {
+ throw new IllegalStateException("failed to create jaxb context for repository", ex);
+ }
+ }
+
+ Repository read(Path path) {
+ LOG.trace("read repository metadata from {}", path);
+ try {
+ return (Repository) jaxbContext.createUnmarshaller().unmarshal(path.toFile());
+ } catch (JAXBException ex) {
+ throw new InternalRepositoryException(
+ ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex
+ );
+ }
+ }
+
+ void write(Path path, Repository repository) {
+ LOG.trace("write repository metadata of {} to {}", repository.getNamespaceAndName(), path);
+ try {
+ Marshaller marshaller = jaxbContext.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+ marshaller.marshal(repository, path.toFile());
+ } catch (JAXBException ex) {
+ throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
+ }
+ }
+
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java
new file mode 100644
index 0000000000..70698aed59
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java
@@ -0,0 +1,146 @@
+package sonia.scm.repository.xml;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sonia.scm.ContextEntry;
+import sonia.scm.repository.InternalRepositoryException;
+import sonia.scm.xml.IndentXMLStreamWriter;
+import sonia.scm.xml.XmlStreams;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+class PathDatabase {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PathDatabase.class);
+
+ private static final String ENCODING = "UTF-8";
+ private static final String VERSION = "1.0";
+
+ private static final String ELEMENT_REPOSITORIES = "repositories";
+ private static final String ATTRIBUTE_CREATION_TIME = "creation-time";
+ private static final String ATTRIBUTE_LAST_MODIFIED = "last-modified";
+
+ private static final String ELEMENT_REPOSITORY = "repository";
+ private static final String ATTRIBUTE_ID = "id";
+
+ private final Path storePath;
+
+ PathDatabase(Path storePath){
+ this.storePath = storePath;
+ }
+
+ void write(Long creationTime, Long lastModified, Map pathDatabase) {
+ ensureParentDirectoryExists();
+ LOG.trace("write repository path database to {}", storePath);
+
+ try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) {
+ writer.writeStartDocument(ENCODING, VERSION);
+
+ writeRepositoriesStart(writer, creationTime, lastModified);
+ for (Map.Entry e : pathDatabase.entrySet()) {
+ writeRepository(writer, e.getKey(), e.getValue());
+ }
+ writer.writeEndElement();
+
+ writer.writeEndDocument();
+ } catch (XMLStreamException | IOException ex) {
+ throw new InternalRepositoryException(
+ ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
+ "failed to write repository path database",
+ ex
+ );
+ }
+ }
+
+ private void ensureParentDirectoryExists() {
+ Path parent = storePath.getParent();
+ // Files.exists is slow on java 8
+ if (!parent.toFile().exists()) {
+ try {
+ Files.createDirectories(parent);
+ } catch (IOException ex) {
+ throw new InternalRepositoryException(
+ ContextEntry.ContextBuilder.entity(Path.class, parent.toString()).build(),
+ "failed to create parent directory",
+ ex
+ );
+ }
+ }
+ }
+
+ private void writeRepositoriesStart(XMLStreamWriter writer, Long creationTime, Long lastModified) throws XMLStreamException {
+ writer.writeStartElement(ELEMENT_REPOSITORIES);
+ writer.writeAttribute(ATTRIBUTE_CREATION_TIME, String.valueOf(creationTime));
+ writer.writeAttribute(ATTRIBUTE_LAST_MODIFIED, String.valueOf(lastModified));
+ }
+
+ private void writeRepository(XMLStreamWriter writer, String id, Path value) throws XMLStreamException {
+ writer.writeStartElement(ELEMENT_REPOSITORY);
+ writer.writeAttribute(ATTRIBUTE_ID, id);
+ writer.writeCharacters(value.toString());
+ writer.writeEndElement();
+ }
+
+ void read(OnRepositories onRepositories, OnRepository onRepository) {
+ LOG.trace("read repository path database from {}", storePath);
+ XMLStreamReader reader = null;
+ try {
+ reader = XmlStreams.createReader(storePath);
+
+ while (reader.hasNext()) {
+ int eventType = reader.next();
+
+ if (eventType == XMLStreamReader.START_ELEMENT) {
+ String element = reader.getLocalName();
+ if (ELEMENT_REPOSITORIES.equals(element)) {
+ readRepositories(reader, onRepositories);
+ } else if (ELEMENT_REPOSITORY.equals(element)) {
+ readRepository(reader, onRepository);
+ }
+ }
+ }
+ } catch (XMLStreamException | IOException ex) {
+ throw new InternalRepositoryException(
+ ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
+ "failed to read repository path database",
+ ex
+ );
+ } finally {
+ XmlStreams.close(reader);
+ }
+ }
+
+ private void readRepository(XMLStreamReader reader, OnRepository onRepository) throws XMLStreamException {
+ String id = reader.getAttributeValue(null, ATTRIBUTE_ID);
+ Path path = Paths.get(reader.getElementText());
+ onRepository.handle(id, path);
+ }
+
+ private void readRepositories(XMLStreamReader reader, OnRepositories onRepositories) {
+ String creationTime = reader.getAttributeValue(null, ATTRIBUTE_CREATION_TIME);
+ String lastModified = reader.getAttributeValue(null, ATTRIBUTE_LAST_MODIFIED);
+ onRepositories.handle(Long.parseLong(creationTime), Long.parseLong(lastModified));
+ }
+
+ @FunctionalInterface
+ interface OnRepositories {
+
+ void handle(Long creationTime, Long lastModified);
+
+ }
+
+ @FunctionalInterface
+ interface OnRepository {
+
+ void handle(String id, Path path);
+
+ }
+
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
index 4510706721..4987b269da 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java
@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
- *
+ *
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- *
+ *
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,9 +24,8 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
+ *
* http://bitbucket.org/sdorra/scm-manager
- *
*/
@@ -34,82 +33,231 @@ package sonia.scm.repository.xml;
//~--- non-JDK imports --------------------------------------------------------
-import com.google.inject.Inject;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Singleton;
+import sonia.scm.SCMContextProvider;
+import sonia.scm.io.FileSystem;
+import sonia.scm.repository.InitialRepositoryLocationResolver;
+import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
+import sonia.scm.repository.PathBasedRepositoryDAO;
import sonia.scm.repository.Repository;
-import sonia.scm.repository.RepositoryDAO;
-import sonia.scm.store.ConfigurationStoreFactory;
-import sonia.scm.xml.AbstractXmlDAO;
+import sonia.scm.store.StoreConstants;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
/**
- *
* @author Sebastian Sdorra
*/
@Singleton
-public class XmlRepositoryDAO
- extends AbstractXmlDAO
- implements RepositoryDAO
-{
+public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
- /** Field description */
- public static final String STORE_NAME = "repositories";
+ private static final String STORE_NAME = "repositories";
- //~--- constructors ---------------------------------------------------------
+ private final PathDatabase pathDatabase;
+ private final MetadataStore metadataStore = new MetadataStore();
+
+ private final SCMContextProvider context;
+ private final InitialRepositoryLocationResolver locationResolver;
+ private final FileSystem fileSystem;
+
+ private final Map pathById;
+ private final Map byId;
+ private final Map byNamespaceAndName;
+
+ private final Clock clock;
+
+ private Long creationTime;
+ private Long lastModified;
- /**
- * Constructs ...
- *
- *
- * @param storeFactory
- */
@Inject
- public XmlRepositoryDAO(ConfigurationStoreFactory storeFactory)
- {
- super(storeFactory.getStore(XmlRepositoryDatabase.class, STORE_NAME));
+ public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) {
+ this(context, locationResolver, fileSystem, Clock.systemUTC());
}
- //~--- methods --------------------------------------------------------------
+ XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem, Clock clock) {
+ this.context = context;
+ this.locationResolver = locationResolver;
+ this.fileSystem = fileSystem;
- @Override
- public boolean contains(NamespaceAndName namespaceAndName)
- {
- return db.contains(namespaceAndName);
+ this.clock = clock;
+ this.creationTime = clock.millis();
+
+ this.pathById = new ConcurrentHashMap<>();
+ this.byId = new ConcurrentHashMap<>();
+ this.byNamespaceAndName = new ConcurrentHashMap<>();
+
+ pathDatabase = new PathDatabase(resolveStorePath());
+ read();
}
- //~--- get methods ----------------------------------------------------------
+ private void read() {
+ Path storePath = resolveStorePath();
- @Override
- public Repository get(NamespaceAndName namespaceAndName)
- {
- return db.get(namespaceAndName);
+ // Files.exists is slow on java 8
+ if (storePath.toFile().exists()) {
+ pathDatabase.read(this::onLoadDates, this::onLoadRepository);
+ }
}
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param repository
- *
- * @return
- */
- @Override
- protected Repository clone(Repository repository)
- {
- return repository.clone();
+ private void onLoadDates(Long creationTime, Long lastModified) {
+ this.creationTime = creationTime;
+ this.lastModified = lastModified;
+ }
+
+ private void onLoadRepository(String id, Path repositoryPath) {
+ Path metadataPath = resolveMetadataPath(context.resolve(repositoryPath));
+
+ Repository repository = metadataStore.read(metadataPath);
+
+ byId.put(id, repository);
+ byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
+ pathById.put(id, repositoryPath);
+ }
+
+ @VisibleForTesting
+ Path resolveStorePath() {
+ return context.getBaseDirectory()
+ .toPath()
+ .resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
+ .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
+ }
+
+
+ @VisibleForTesting
+ Path resolveMetadataPath(Path repositoryPath) {
+ return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
}
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- protected XmlRepositoryDatabase createNewDatabase()
- {
- return new XmlRepositoryDatabase();
+ public String getType() {
+ return "xml";
+ }
+
+ @Override
+ public Long getCreationTime() {
+ return creationTime;
+ }
+
+ @Override
+ public Long getLastModified() {
+ return lastModified;
+ }
+
+ @Override
+ public void add(Repository repository) {
+ Repository clone = repository.clone();
+
+ Path repositoryPath = locationResolver.getPath(repository.getId());
+ Path resolvedPath = context.resolve(repositoryPath);
+
+ try {
+ fileSystem.create(resolvedPath.toFile());
+
+ Path metadataPath = resolveMetadataPath(resolvedPath);
+ metadataStore.write(metadataPath, repository);
+
+ synchronized (this) {
+ pathById.put(repository.getId(), repositoryPath);
+
+ byId.put(repository.getId(), clone);
+ byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
+
+ writePathDatabase();
+ }
+
+ } catch (IOException e) {
+ throw new InternalRepositoryException(repository, "failed to create filesystem", e);
+ }
+ }
+
+ private void writePathDatabase() {
+ lastModified = clock.millis();
+ pathDatabase.write(creationTime, lastModified, pathById);
+ }
+
+ @Override
+ public boolean contains(Repository repository) {
+ return byId.containsKey(repository.getId());
+ }
+
+ @Override
+ public boolean contains(NamespaceAndName namespaceAndName) {
+ return byNamespaceAndName.containsKey(namespaceAndName);
+ }
+
+ @Override
+ public boolean contains(String id) {
+ return byId.containsKey(id);
+ }
+
+ @Override
+ public Repository get(NamespaceAndName namespaceAndName) {
+ return byNamespaceAndName.get(namespaceAndName);
+ }
+
+ @Override
+ public Repository get(String id) {
+ return byId.get(id);
+ }
+
+ @Override
+ public Collection getAll() {
+ return ImmutableList.copyOf(byNamespaceAndName.values());
+ }
+
+ @Override
+ public void modify(Repository repository) {
+ Repository clone = repository.clone();
+
+ synchronized (this) {
+ // remove old namespaceAndName from map, in case of rename
+ Repository prev = byId.put(clone.getId(), clone);
+ if (prev != null) {
+ byNamespaceAndName.remove(prev.getNamespaceAndName());
+ }
+ byNamespaceAndName.put(clone.getNamespaceAndName(), clone);
+
+ writePathDatabase();
+ }
+
+ Path repositoryPath = context.resolve(getPath(repository.getId()));
+ Path metadataPath = resolveMetadataPath(repositoryPath);
+ metadataStore.write(metadataPath, clone);
+ }
+
+ @Override
+ public void delete(Repository repository) {
+ Path path;
+ synchronized (this) {
+ Repository prev = byId.remove(repository.getId());
+ if (prev != null) {
+ byNamespaceAndName.remove(prev.getNamespaceAndName());
+ }
+
+ path = pathById.remove(repository.getId());
+
+ writePathDatabase();
+ }
+
+ path = context.resolve(path);
+
+ try {
+ fileSystem.destroy(path.toFile());
+ } catch (IOException e) {
+ throw new InternalRepositoryException(repository, "failed to destroy filesystem", e);
+ }
+ }
+
+ @Override
+ public Path getPath(String repositoryId) {
+ return pathById.get(repositoryId);
}
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java
deleted file mode 100644
index 93be611213..0000000000
--- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDatabase.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository.xml;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import sonia.scm.repository.NamespaceAndName;
-import sonia.scm.repository.Repository;
-import sonia.scm.xml.XmlDatabase;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlRootElement;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-//~--- JDK imports ------------------------------------------------------------
-
-@XmlRootElement(name = "repository-db")
-@XmlAccessorType(XmlAccessType.FIELD)
-public class XmlRepositoryDatabase implements XmlDatabase
-{
-
- public XmlRepositoryDatabase()
- {
- long c = System.currentTimeMillis();
-
- creationTime = c;
- lastModified = c;
- }
-
- static String createKey(NamespaceAndName namespaceAndName)
- {
- return namespaceAndName.getNamespace() + ":" + namespaceAndName.getName();
- }
-
- static String createKey(Repository repository)
- {
- return createKey(repository.getNamespaceAndName());
- }
-
- @Override
- public void add(Repository repository)
- {
- repositoryMap.put(createKey(repository), repository);
- }
-
- public boolean contains(NamespaceAndName namespaceAndName)
- {
- return repositoryMap.containsKey(createKey(namespaceAndName));
- }
-
- @Override
- public boolean contains(String id)
- {
- return get(id) != null;
- }
-
- public boolean contains(Repository repository)
- {
- return repositoryMap.containsKey(createKey(repository));
- }
-
- public void remove(Repository repository)
- {
- repositoryMap.remove(createKey(repository));
- }
-
- @Override
- public Repository remove(String id)
- {
- Repository r = get(id);
-
- remove(r);
-
- return r;
- }
-
- @Override
- public Collection values()
- {
- return repositoryMap.values();
- }
-
- //~--- get methods ----------------------------------------------------------
-
- public Repository get(NamespaceAndName namespaceAndName)
- {
- return repositoryMap.get(createKey(namespaceAndName));
- }
-
- /**
- * Method description
- *
- *
- * @param id
- *
- * @return
- */
- @Override
- public Repository get(String id)
- {
- Repository repository = null;
-
- for (Repository r : values())
- {
- if (r.getId().equals(id))
- {
- repository = r;
-
- break;
- }
- }
-
- return repository;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public long getCreationTime()
- {
- return creationTime;
- }
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public long getLastModified()
- {
- return lastModified;
- }
-
- //~--- set methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param creationTime
- */
- @Override
- public void setCreationTime(long creationTime)
- {
- this.creationTime = creationTime;
- }
-
- /**
- * Method description
- *
- *
- * @param lastModified
- */
- @Override
- public void setLastModified(long lastModified)
- {
- this.lastModified = lastModified;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private Long creationTime;
-
- /** Field description */
- private Long lastModified;
-
- /** Field description */
- @XmlJavaTypeAdapter(XmlRepositoryMapAdapter.class)
- @XmlElement(name = "repositories")
- private Map repositoryMap = new LinkedHashMap<>();
-}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java
deleted file mode 100644
index d9807e9188..0000000000
--- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryList.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository.xml;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import sonia.scm.repository.Repository;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.Map;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlRootElement;
-
-/**
- *
- * @author Sebastian Sdorra
- */
-@XmlRootElement(name = "repositories")
-@XmlAccessorType(XmlAccessType.FIELD)
-public class XmlRepositoryList implements Iterable
-{
-
- /**
- * Constructs ...
- *
- */
- public XmlRepositoryList() {}
-
- /**
- * Constructs ...
- *
- *
- *
- * @param repositoryMap
- */
- public XmlRepositoryList(Map repositoryMap)
- {
- this.repositories = new LinkedList(repositoryMap.values());
- }
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- public Iterator iterator()
- {
- return repositories.iterator();
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
- public LinkedList getRepositories()
- {
- return repositories;
- }
-
- //~--- set methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param repositories
- */
- public void setRepositories(LinkedList repositories)
- {
- this.repositories = repositories;
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- @XmlElement(name = "repository")
- private LinkedList repositories;
-}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
index 5bfc4f34b9..d37a150723 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
@@ -31,18 +31,21 @@
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
-//~--- JDK imports ------------------------------------------------------------
import java.io.File;
+//~--- JDK imports ------------------------------------------------------------
+
/**
* Abstract store factory for file based stores.
- *
+ *
* @author Sebastian Sdorra
*/
public abstract class FileBasedStoreFactory {
@@ -51,39 +54,54 @@ public abstract class FileBasedStoreFactory {
* the logger for FileBasedStoreFactory
*/
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
+ private SCMContextProvider contextProvider;
+ private RepositoryLocationResolver repositoryLocationResolver;
+ private Store store;
- private static final String BASE_DIRECTORY = "var";
-
- private final SCMContextProvider context;
-
- private final String dataDirectoryName;
-
- private File dataDirectory;
-
- protected FileBasedStoreFactory(SCMContextProvider context,
- String dataDirectoryName) {
- this.context = context;
- this.dataDirectoryName = dataDirectoryName;
+ protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
+ this.contextProvider = contextProvider;
+ this.repositoryLocationResolver = repositoryLocationResolver;
+ this.store = store;
}
- //~--- get methods ----------------------------------------------------------
- /**
- * Returns data directory for given name.
- *
- * @param name name of data directory
- *
- * @return data directory
- */
- protected File getDirectory(String name) {
- if (dataDirectory == null) {
- dataDirectory = new File(context.getBaseDirectory(),
- BASE_DIRECTORY.concat(File.separator).concat(dataDirectoryName));
- LOG.debug("create data directory {}", dataDirectory);
- }
+ protected File getStoreLocation(StoreParameters storeParameters) {
+ return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepository());
+ }
- File storeDirectory = new File(dataDirectory, name);
+ protected File getStoreLocation(TypedStoreParameters storeParameters) {
+ return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepository());
+ }
+
+ protected File getStoreLocation(String name, Class type, Repository repository) {
+ File storeDirectory;
+ if (repository != null) {
+ LOG.debug("create store with type: {}, name: {} and repository: {}", type, name, repository.getNamespaceAndName());
+ storeDirectory = this.getStoreDirectory(store, repository);
+ } else {
+ LOG.debug("create store with type: {} and name: {} ", type, name);
+ storeDirectory = this.getStoreDirectory(store);
+ }
IOUtil.mkdirs(storeDirectory);
- return storeDirectory;
+ return new File(storeDirectory, name);
+ }
+
+ /**
+ * Get the store directory of a specific repository
+ * @param store the type of the store
+ * @param repository the repo
+ * @return the store directory of a specific repository
+ */
+ private File getStoreDirectory(Store store, Repository repository) {
+ return new File(repositoryLocationResolver.getPath(repository.getId()).toFile(), store.getRepositoryStoreDirectory());
+ }
+
+ /**
+ * Get the global store directory
+ * @param store the type of the store
+ * @return the global store directory
+ */
+ private File getStoreDirectory(Store store) {
+ return new File(contextProvider.getBaseDirectory(), store.getGlobalStoreDirectory());
}
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
index 8cc5b34ac2..7e2e5a9e29 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java
@@ -31,14 +31,17 @@
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
+
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
+import sonia.scm.util.IOUtil;
+
+import java.io.File;
/**
* File based store factory.
@@ -48,8 +51,6 @@ import sonia.scm.security.KeyGenerator;
@Singleton
public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory {
- private static final String DIRECTORY_NAME = "blob";
-
/**
* the logger for FileBlobStoreFactory
*/
@@ -60,21 +61,22 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
/**
* Constructs a new instance.
*
- * @param context scm context
+ * @param repositoryLocationResolver location resolver
* @param keyGenerator key generator
*/
@Inject
- public FileBlobStoreFactory(SCMContextProvider context,
- KeyGenerator keyGenerator) {
- super(context, DIRECTORY_NAME);
+ public FileBlobStoreFactory(SCMContextProvider contextProvider ,RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
+ super(contextProvider, repositoryLocationResolver, Store.BLOB);
this.keyGenerator = keyGenerator;
}
@Override
- public BlobStore getBlobStore(String name) {
- LOG.debug("create new blob with name {}", name);
-
- return new FileBlobStore(keyGenerator, getDirectory(name));
+ @SuppressWarnings("unchecked")
+ public BlobStore getStore(StoreParameters storeParameters) {
+ File storeLocation = getStoreLocation(storeParameters);
+ IOUtil.mkdirs(storeLocation);
+ return new FileBlobStore(keyGenerator, storeLocation);
}
+
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java
index 6a9098b545..40cf03c8a8 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStore.java
@@ -35,32 +35,14 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
-import com.google.common.base.Charsets;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Maps;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.security.KeyGenerator;
import sonia.scm.xml.IndentXMLStreamWriter;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Map.Entry;
+import sonia.scm.xml.XmlStreams;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
@@ -68,11 +50,14 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLOutputFactory;
-import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
-import javax.xml.stream.XMLStreamWriter;
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
+
+//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -255,74 +240,6 @@ public class JAXBConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore implements ConfigurationEntryStore
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- *
+ *
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,97 +24,42 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
+ *
* http://bitbucket.org/sdorra/scm-manager
- *
*/
-
package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
-import sonia.scm.util.IOUtil;
//~--- JDK imports ------------------------------------------------------------
-import java.io.File;
-
/**
*
* @author Sebastian Sdorra
*/
@Singleton
-public class JAXBConfigurationEntryStoreFactory
- implements ConfigurationEntryStoreFactory
-{
+public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
+ implements ConfigurationEntryStoreFactory {
- /**
- * the logger for JAXBConfigurationEntryStoreFactory
- */
- private static final Logger logger =
- LoggerFactory.getLogger(JAXBConfigurationEntryStoreFactory.class);
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- *
- * @param keyGenerator
- * @param context
- */
- @Inject
- public JAXBConfigurationEntryStoreFactory(KeyGenerator keyGenerator,
- SCMContextProvider context)
- {
- this.keyGenerator = keyGenerator;
- directory = new File(context.getBaseDirectory(),
- StoreConstants.CONFIGDIRECTORY_NAME);
- IOUtil.mkdirs(directory);
- }
-
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param type
- * @param name
- * @param
- *
- * @return
- */
- @Override
- public ConfigurationEntryStore getStore(Class type, String name)
- {
- logger.debug("create new configuration store for type {} with name {}",
- type, name);
-
- //J-
- return new JAXBConfigurationEntryStore(
- new File(directory,name.concat(StoreConstants.FILE_EXTENSION)),
- keyGenerator,
- type
- );
- //J+
- }
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private File directory;
-
- /** Field description */
private KeyGenerator keyGenerator;
+
+ @Inject
+ public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
+ super(contextProvider, repositoryLocationResolver, Store.CONFIG);
+ this.keyGenerator = keyGenerator;
+ }
+
+ @Override
+ public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) {
+ return new JAXBConfigurationEntryStore<>(getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()), keyGenerator, storeParameters.getType());
+ }
+
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
index 4a87ea57f6..ac1477d7ea 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java
@@ -61,7 +61,7 @@ public class JAXBConfigurationStore extends AbstractStore {
private JAXBContext context;
- JAXBConfigurationStore(Class type, File configFile) {
+ public JAXBConfigurationStore(Class type, File configFile) {
this.type = type;
try {
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
index 70fd962254..bb68ab93dc 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java
@@ -32,55 +32,29 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import sonia.scm.SCMContextProvider;
-import sonia.scm.util.IOUtil;
-
-import java.io.File;
+import sonia.scm.repository.RepositoryLocationResolver;
/**
- * JAXB implementation of {@link JAXBConfigurationStoreFactory}.
+ * JAXB implementation of {@link ConfigurationStoreFactory}.
*
* @author Sebastian Sdorra
*/
@Singleton
-public class JAXBConfigurationStoreFactory implements ConfigurationStoreFactory {
-
- /**
- * the logger for JAXBConfigurationStoreFactory
- */
- private static final Logger LOG = LoggerFactory.getLogger(JAXBConfigurationStoreFactory.class);
-
- private final File configDirectory;
+public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory implements ConfigurationStoreFactory {
/**
* Constructs a new instance.
*
- * @param context scm context
+ * @param repositoryLocationResolver Resolver to get the repository Directory
*/
@Inject
- public JAXBConfigurationStoreFactory(SCMContextProvider context) {
- configDirectory = new File(context.getBaseDirectory(), StoreConstants.CONFIGDIRECTORY_NAME);
- IOUtil.mkdirs(configDirectory);
+ public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver) {
+ super(contextProvider, repositoryLocationResolver, Store.CONFIG);
}
@Override
- public JAXBConfigurationStore getStore(Class type, String name) {
- if (configDirectory == null) {
- throw new IllegalStateException("store factory is not initialized");
- }
-
- File configFile = new File(configDirectory, name.concat(StoreConstants.FILE_EXTENSION));
-
- if (LOG.isDebugEnabled()) {
- LOG.debug("create store for {} at {}", type.getName(),
- configFile.getPath());
- }
-
- return new JAXBConfigurationStore<>(type, configFile);
+ public JAXBConfigurationStore getStore(TypedStoreParameters storeParameters) {
+ return new JAXBConfigurationStore<>(storeParameters.getType(), getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepository()));
}
-
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
index 732b8c675b..579ef75b71 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java
@@ -37,11 +37,12 @@ package sonia.scm.store;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.security.KeyGenerator;
+import sonia.scm.util.IOUtil;
+
+import java.io.File;
/**
*
@@ -49,57 +50,20 @@ import sonia.scm.security.KeyGenerator;
*/
@Singleton
public class JAXBDataStoreFactory extends FileBasedStoreFactory
- implements DataStoreFactory
-{
+ implements DataStoreFactory {
- /** Field description */
- private static final String DIRECTORY_NAME = "data";
+ private KeyGenerator keyGenerator;
- /**
- * the logger for JAXBDataStoreFactory
- */
- private static final Logger logger =
- LoggerFactory.getLogger(JAXBDataStoreFactory.class);
-
- //~--- constructors ---------------------------------------------------------
-
- /**
- * Constructs ...
- *
- *
- * @param context
- * @param keyGenerator
- */
@Inject
- public JAXBDataStoreFactory(SCMContextProvider context,
- KeyGenerator keyGenerator)
- {
- super(context, DIRECTORY_NAME);
+ public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
+ super(contextProvider, repositoryLocationResolver, Store.DATA);
this.keyGenerator = keyGenerator;
}
- //~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param type
- * @param name
- * @param
- *
- * @return
- */
@Override
- public DataStore getStore(Class type, String name)
- {
- logger.debug("create new store for type {} with name {}", type, name);
-
- return new JAXBDataStore<>(keyGenerator, type, getDirectory(name));
+ public DataStore getStore(TypedStoreParameters storeParameters) {
+ File storeLocation = getStoreLocation(storeParameters);
+ IOUtil.mkdirs(storeLocation);
+ return new JAXBDataStore<>(keyGenerator, storeParameters.getType(), storeLocation);
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- private KeyGenerator keyGenerator;
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/Store.java b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
new file mode 100644
index 0000000000..511ef8323e
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
@@ -0,0 +1,50 @@
+package sonia.scm.store;
+
+import java.io.File;
+
+public enum Store {
+ CONFIG("config"),
+ DATA("data"),
+ BLOB("blob");
+
+ private static final String GLOBAL_STORE_BASE_DIRECTORY = "var";
+ private static final String STORE_DIRECTORY = "store";
+
+ private String directory;
+
+ Store(String directory) {
+
+ this.directory = directory;
+ }
+
+ /**
+ * Get the relative store directory path to be stored in the repository root
+ *
+ * The repository store directories are:
+ * repo_base_dir/store/config/
+ * repo_base_dir/store/blob/
+ * repo_base_dir/store/data/
+ *
+ * @return the relative store directory path to be stored in the repository root
+ */
+ public String getRepositoryStoreDirectory() {
+ return STORE_DIRECTORY + File.separator + directory;
+ }
+
+ /**
+ * Get the relative store directory path to be stored in the global root
+ *
+ * The global store directories are:
+ * base_dir/config/
+ * base_dir/var/blob/
+ * base_dir/var/data/
+ *
+ * @return the relative store directory path to be stored in the global root
+ */
+ public String getGlobalStoreDirectory() {
+ if (this.equals(CONFIG)) {
+ return directory;
+ }
+ return GLOBAL_STORE_BASE_DIRECTORY + File.separator + directory;
+ }
+}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java b/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java
index 0bd9864cd7..cf24d125c2 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/StoreConstants.java
@@ -37,12 +37,14 @@ package sonia.scm.store;
*
* @author Sebastian Sdorra
*/
-public interface StoreConstants
+public class StoreConstants
{
- /** Field description */
- public static final String CONFIGDIRECTORY_NAME = "config";
+ private StoreConstants() { }
+
+ public static final String CONFIG_DIRECTORY_NAME = "config";
+
+ public static final String REPOSITORY_METADATA = "metadata";
- /** Field description */
public static final String FILE_EXTENSION = ".xml";
}
diff --git a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java
index 1bfd877f44..ea7f18fbba 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/user/xml/XmlUserDAO.java
@@ -36,11 +36,10 @@ package sonia.scm.user.xml;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
+import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
import sonia.scm.xml.AbstractXmlDAO;
-import sonia.scm.store.ConfigurationStoreFactory;
/**
*
@@ -65,7 +64,10 @@ public class XmlUserDAO extends AbstractXmlDAO
@Inject
public XmlUserDAO(ConfigurationStoreFactory storeFactory)
{
- super(storeFactory.getStore(XmlUserDatabase.class, STORE_NAME));
+ super(storeFactory
+ .withType(XmlUserDatabase.class)
+ .withName(STORE_NAME)
+ .build());
}
//~--- methods --------------------------------------------------------------
diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java
index b8d1e6f42e..5b24096eb5 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java
@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
- *
+ *
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- *
+ *
* 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,9 +24,8 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
+ *
* http://bitbucket.org/sdorra/scm-manager
- *
*/
@@ -35,18 +34,13 @@ package sonia.scm.xml;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableList;
-import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import sonia.scm.GenericDAO;
import sonia.scm.ModelObject;
-import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.store.ConfigurationStore;
-import sonia.scm.util.AssertUtil;
import java.util.Collection;
-import java.util.stream.Collectors;
//~--- JDK imports ------------------------------------------------------------
@@ -58,7 +52,7 @@ import java.util.stream.Collectors;
* @param
*/
public abstract class AbstractXmlDAO> implements GenericDAO
+ T extends XmlDatabase> implements GenericDAO
{
/** Field description */
@@ -68,7 +62,7 @@ public abstract class AbstractXmlDAO store;
+ protected final ConfigurationStore