diff --git a/Jenkinsfile b/Jenkinsfile
index e317ed77d4..f69069d72c 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -107,7 +107,8 @@ void analyzeWith(Maven mvn) {
"-Dsonar.pullrequest.key=${env.CHANGE_ID} " +
"-Dsonar.pullrequest.provider=bitbucketcloud " +
"-Dsonar.pullrequest.bitbucketcloud.owner=sdorra " +
- "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
+ "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager " +
+ "-Dsonar.cpd.exclusions=**/*StoreFactory.java,**/*UserPassword.js "
} else {
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} "
if (!isMainBranch()) {
diff --git a/pom.xml b/pom.xml
index f8bb7c5727..e04a47ef41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -810,6 +810,13 @@
SCM-BSD1.2.0.Final
+
+
+
+
+
+ **/*StoreFactory.java,**/*UserPassword.js
+
diff --git a/scm-core/pom.xml b/scm-core/pom.xml
index 3c90fec779..66859a12ee 100644
--- a/scm-core/pom.xml
+++ b/scm-core/pom.xml
@@ -221,5 +221,5 @@
-
+
diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java
index b13cc0e26b..a3e8a1da73 100644
--- a/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java
+++ b/scm-core/src/main/java/sonia/scm/repository/AbstractRepositoryHandler.java
@@ -72,9 +72,11 @@ public abstract class AbstractRepositoryHandler
*
* @param storeFactory
*/
- protected AbstractRepositoryHandler(ConfigurationStoreFactory storeFactory)
- {
- this.store = storeFactory.getStore(getConfigClass(), getType().getName());
+ protected AbstractRepositoryHandler(ConfigurationStoreFactory storeFactory) {
+ this.store = storeFactory
+ .withType(getConfigClass())
+ .withName(getType().getName())
+ .build();
}
//~--- get methods ----------------------------------------------------------
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
index 922c16c879..63d499deb8 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
@@ -4,7 +4,6 @@ import groovy.lang.Singleton;
import sonia.scm.SCMContextProvider;
import javax.inject.Inject;
-import java.io.File;
import java.nio.file.Path;
/**
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..c2a5f4b747 100644
--- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java
+++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java
@@ -31,6 +31,7 @@
package sonia.scm.security;
import java.util.Date;
+import java.util.Map;
import java.util.Optional;
/**
@@ -38,70 +39,77 @@ import java.util.Optional;
* 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();
-
+
+ Optional getRefreshExpiration();
+
/**
- * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
+ * 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 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..5e36ba468f 100644
--- a/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java
+++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenBuilder.java
@@ -74,11 +74,21 @@ 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.
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:
+ *
*
* @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:
+ *
+ *
* @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/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:
+ *
*
* @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:
+ *
*
* @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/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-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/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java
index 5bfc4f34b9..099ab53baa 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,57 @@ 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 File storeDirectory;
- 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);
- IOUtil.mkdirs(storeDirectory);
- return storeDirectory;
+ protected File getStoreLocation(TypedStoreParameters storeParameters) {
+ return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepository());
+ }
+
+ protected File getStoreLocation(String name, Class type, Repository repository) {
+ if (storeDirectory == null) {
+ 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 new File(this.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/JAXBConfigurationEntryStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java
index 261c36f8e4..96403140ef 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.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,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.CONFIG_DIRECTORY_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 705b0c1177..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,14 +32,8 @@ 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 ConfigurationStoreFactory}.
@@ -47,40 +41,20 @@ import java.io.File;
* @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.CONFIG_DIRECTORY_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..5b5c00a298 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
@@ -41,7 +41,11 @@ 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 +53,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..6e5cbcdf65
--- /dev/null
+++ b/scm-dao-xml/src/main/java/sonia/scm/store/Store.java
@@ -0,0 +1,49 @@
+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 String directory;
+
+ Store(String directory) {
+
+ this.directory = directory;
+ }
+
+ /**
+ * Get the relkative store directory path to be stored in the repository root
+ *
+ * The repository store directories are:
+ * repo_base_dir/config/
+ * repo_base_dir/blob/
+ * repo_base_dir/data/
+ *
+ * @return the relative store directory path to be stored in the repository root
+ */
+ public String getRepositoryStoreDirectory() {
+ return 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/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/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
index 3910f59bcc..6330db56a0 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
@@ -28,7 +28,8 @@ import java.util.concurrent.atomic.AtomicLong;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@MockitoSettings(strictness = Strictness.LENIENT)
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
index cae872538d..3ec16baa57 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java
@@ -34,8 +34,15 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
+import org.junit.Test;
+import sonia.scm.repository.Repository;
import sonia.scm.security.UUIDKeyGenerator;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertNotNull;
+
/**
*
* @author Sebastian Sdorra
@@ -52,6 +59,24 @@ public class FileBlobStoreTest extends BlobStoreTestBase
@Override
protected BlobStoreFactory createBlobStoreFactory()
{
- return new FileBlobStoreFactory(contextProvider, new UUIDKeyGenerator());
+ return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldStoreAndLoadInRepository() {
+ BlobStore store = createBlobStoreFactory()
+ .withName("test")
+ .forRepository(new Repository("id", "git", "ns", "n"))
+ .build();
+
+ Blob createdBlob = store.create("abc");
+ List storedBlobs = store.getAll();
+
+ assertNotNull(createdBlob);
+ assertThat(storedBlobs)
+ .isNotNull()
+ .hasSize(1)
+ .usingElementComparatorOnFields("id").containsExactly(createdBlob);
}
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
index d0f17fc313..ae84f9d768 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java
@@ -37,25 +37,22 @@ package sonia.scm.store;
import com.google.common.io.Closeables;
import com.google.common.io.Resources;
-
import org.junit.Test;
-
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.UUIDKeyGenerator;
-import static org.junit.Assert.*;
-
-//~--- JDK imports ------------------------------------------------------------
-
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-
import java.net.URL;
-
import java.util.UUID;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -132,13 +129,13 @@ public class JAXBConfigurationEntryStoreTest
public void testStoreAndLoad() throws IOException
{
String name = UUID.randomUUID().toString();
- ConfigurationEntryStore store =
- createPermissionStore(RESOURCE_FIXED, name);
+ ConfigurationEntryStore store = createPermissionStore(RESOURCE_FIXED, name);
store.put("a45", new AssignedPermission("tuser4", "repository:create"));
- store =
- createConfigurationStoreFactory().getStore(AssignedPermission.class,
- name);
+ store = createConfigurationStoreFactory()
+ .withType(AssignedPermission.class)
+ .withName(name)
+ .build();
AssignedPermission ap = store.get("a45");
@@ -147,6 +144,16 @@ public class JAXBConfigurationEntryStoreTest
assertEquals("repository:create", ap.getPermission());
}
+ @Test
+ public void shouldStoreAndLoadInRepository() throws IOException
+ {
+ repoStore.put("abc", new StoreObject("abc_value"));
+ StoreObject storeObject = repoStore.get("abc");
+
+ assertNotNull(storeObject);
+ assertEquals("abc_value", storeObject.getValue());
+ }
+
/**
* Method description
*
@@ -154,10 +161,9 @@ public class JAXBConfigurationEntryStoreTest
* @return
*/
@Override
- protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
+ protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
{
- return new JAXBConfigurationEntryStoreFactory(new UUIDKeyGenerator(),
- contextProvider);
+ return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
}
/**
@@ -225,8 +231,9 @@ public class JAXBConfigurationEntryStoreTest
}
copy(resource, name);
-
- return createConfigurationStoreFactory().getStore(AssignedPermission.class,
- name);
+ return createConfigurationStoreFactory()
+ .withType(AssignedPermission.class)
+ .withName(name)
+ .build();
}
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
index 4151a6ca20..802f193340 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java
@@ -32,9 +32,15 @@
package sonia.scm.store;
+import org.junit.Test;
+import sonia.scm.repository.Repository;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
/**
* Unit tests for {@link JAXBConfigurationStore}.
- *
+ *
* @author Sebastian Sdorra
*/
public class JAXBConfigurationStoreTest extends StoreTestBase {
@@ -42,6 +48,24 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
@Override
protected ConfigurationStoreFactory createStoreFactory()
{
- return new JAXBConfigurationStoreFactory(contextProvider);
+ return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver);
+ }
+
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldStoreAndLoadInRepository()
+ {
+ ConfigurationStore store = createStoreFactory()
+ .withType(StoreObject.class)
+ .withName("test")
+ .forRepository(new Repository("id", "git", "ns", "n"))
+ .build();
+
+ store.set(new StoreObject("value"));
+ StoreObject storeObject = store.get();
+
+ assertNotNull(storeObject);
+ assertEquals("value", storeObject.getValue());
}
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
index 9834a48916..04d86aa625 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java
@@ -34,14 +34,18 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
+import org.junit.Test;
+import sonia.scm.repository.Repository;
import sonia.scm.security.UUIDKeyGenerator;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
/**
*
* @author Sebastian Sdorra
*/
-public class JAXBDataStoreTest extends DataStoreTestBase
-{
+public class JAXBDataStoreTest extends DataStoreTestBase {
/**
* Method description
@@ -52,6 +56,33 @@ public class JAXBDataStoreTest extends DataStoreTestBase
@Override
protected DataStoreFactory createDataStoreFactory()
{
- return new JAXBDataStoreFactory(contextProvider, new UUIDKeyGenerator());
+ return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator());
+ }
+
+ @Override
+ protected DataStore getDataStore(Class type, Repository repository) {
+ return createDataStoreFactory()
+ .withType(type)
+ .withName("test")
+ .forRepository(repository)
+ .build();
+ }
+
+ @Override
+ protected DataStore getDataStore(Class type) {
+ return createDataStoreFactory()
+ .withType(type)
+ .withName("test")
+ .build();
+ }
+
+ @Test
+ public void shouldStoreAndLoadInRepository()
+ {
+ repoStore.put("abc", new StoreObject("abc_value"));
+ StoreObject storeObject = repoStore.get("abc");
+
+ assertNotNull(storeObject);
+ assertEquals("abc_value", storeObject.getValue());
}
}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
index eebd6b8f2b..84733b9ea3 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/LfsBlobStoreFactory.java
@@ -74,7 +74,11 @@ public class LfsBlobStoreFactory {
*
* @return blob store for the corresponding scm repository
*/
+ @SuppressWarnings("unchecked")
public BlobStore getLfsBlobStore(Repository repository) {
- return blobStoreFactory.getBlobStore(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX);
+ return blobStoreFactory
+ .withName(repository.getId() + GIT_LFS_REPOSITORY_POSTFIX)
+ .forRepository(repository)
+ .build();
}
}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
index d3eca1d6e0..dbd67a7f8e 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java
@@ -33,6 +33,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -44,6 +45,8 @@ import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -81,6 +84,10 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
assertTrue(refs.isDirectory());
}
+ @Before
+ public void initFactory() {
+ when(factory.withType(any())).thenCallRealMethod();
+ }
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
index 991e2655f7..93eadf8935 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/LfsBlobStoreFactoryTest.java
@@ -40,9 +40,12 @@ import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStoreFactory;
-import static org.mockito.Matchers.matches;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
/**
* Unit tests for {@link LfsBlobStoreFactory}.
@@ -59,15 +62,21 @@ public class LfsBlobStoreFactoryTest {
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Test
- public void getBlobStore() throws Exception {
- lfsBlobStoreFactory.getLfsBlobStore(new Repository("the-id", "GIT", "space", "the-name"));
+ public void getBlobStore() {
+ when(blobStoreFactory.withName(any())).thenCallRealMethod();
+ Repository repository = new Repository("the-id", "GIT", "space", "the-name");
+ lfsBlobStoreFactory.getLfsBlobStore(repository);
// just make sure the right parameter is passed, as properly validating the return value is nearly impossible with
// the return value (and should not be part of this test)
- verify(blobStoreFactory).getBlobStore(matches("the-id-git-lfs"));
+ verify(blobStoreFactory).getStore(argThat(blobStoreParameters -> {
+ assertThat(blobStoreParameters.getName()).isEqualTo("the-id-git-lfs");
+ assertThat(blobStoreParameters.getRepository()).isEqualTo(repository);
+ return true;
+ }));
// make sure there have been no further usages of the factory
- verifyNoMoreInteractions(blobStoreFactory);
+ verify(blobStoreFactory, times(1)).getStore(any());
}
}
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java
index 7a13c06eb2..c45d9ab358 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/HgRepositoryHandlerTest.java
@@ -34,6 +34,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -44,6 +45,8 @@ import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -67,6 +70,11 @@ public class HgRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
assertTrue(hgDirectory.isDirectory());
}
+ @Before
+ public void initFactory() {
+ when(factory.withType(any())).thenCallRealMethod();
+ }
+
@Override
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory, RepositoryLocationResolver locationResolver, File directory) {
HgRepositoryHandler handler = new HgRepositoryHandler(factory, new HgContextProvider(), locationResolver);
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java
new file mode 100644
index 0000000000..7d74024630
--- /dev/null
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/web/HgHookCallbackServletTest.java
@@ -0,0 +1,36 @@
+package sonia.scm.web;
+
+import org.junit.Test;
+import sonia.scm.repository.HgRepositoryHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static sonia.scm.web.HgHookCallbackServlet.PARAM_REPOSITORYID;
+
+public class HgHookCallbackServletTest {
+
+ @Test
+ public void shouldExtractCorrectRepositoryId() throws ServletException, IOException {
+ HgRepositoryHandler handler = mock(HgRepositoryHandler.class);
+ HgHookCallbackServlet servlet = new HgHookCallbackServlet(null, handler, null, null);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ when(request.getContextPath()).thenReturn("http://example.com/scm");
+ when(request.getRequestURI()).thenReturn("http://example.com/scm/hook/hg/pretxnchangegroup");
+ String path = "/tmp/hg/12345";
+ when(request.getParameter(PARAM_REPOSITORYID)).thenReturn(path);
+
+ servlet.doPost(request, response);
+
+ verify(response, never()).sendError(anyInt());
+ }
+}
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java
index 97698d7a77..86f99cd517 100644
--- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnRepositoryHandler.java
@@ -55,6 +55,7 @@ import sonia.scm.logging.SVNKitLogger;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.repository.spi.SvnRepositoryServiceProvider;
+import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.util.Util;
diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java
index 7b11d1bb7f..7b22e15c94 100644
--- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java
+++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/SvnRepositoryHandlerTest.java
@@ -32,6 +32,7 @@
package sonia.scm.repository;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -42,12 +43,14 @@ import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import java.io.File;
+import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
//~--- JDK imports ------------------------------------------------------------
@@ -55,15 +58,11 @@ import static org.mockito.Mockito.when;
*
* @author Sebastian Sdorra
*/
-@RunWith(MockitoJUnitRunner.class)
public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Mock
private ConfigurationStoreFactory factory;
- @Mock
- private ConfigurationStore store;
-
@Mock
private com.google.inject.Provider repositoryManagerProvider;
@@ -71,6 +70,12 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
private HookEventFacade facade = new HookEventFacade(repositoryManagerProvider, hookContextFactory);
+ @Override
+ protected void postSetUp() throws IOException, RepositoryPathNotFoundException {
+ initMocks(this);
+ super.postSetUp();
+ }
+
@Override
protected void checkDirectory(File directory) {
File format = new File(directory, "format");
@@ -102,7 +107,7 @@ public class SvnRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
@Test
public void getDirectory() {
- when(factory.getStore(any(), any())).thenReturn(store);
+ when(factory.withType(any())).thenCallRealMethod();
SvnRepositoryHandler repositoryHandler = new SvnRepositoryHandler(factory,
facade, locationResolver);
diff --git a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
index 13cde0391e..040b347e4a 100644
--- a/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/AbstractTestBase.java
@@ -46,10 +46,15 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
+import sonia.scm.io.DefaultFileSystem;
+import sonia.scm.repository.InitialRepositoryLocationResolver;
+import sonia.scm.repository.RepositoryDAO;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
import sonia.scm.util.MockUtil;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
//~--- JDK imports ------------------------------------------------------------
@@ -66,10 +71,29 @@ import java.util.logging.Logger;
public class AbstractTestBase
{
- /** Field description */
private static ThreadState subjectThreadState;
- //~--- methods --------------------------------------------------------------
+ protected SCMContextProvider contextProvider;
+
+ private File tempDirectory;
+
+ protected DefaultFileSystem fileSystem;
+
+ protected RepositoryDAO repositoryDAO = mock(RepositoryDAO.class);
+ protected RepositoryLocationResolver repositoryLocationResolver;
+
+ @Before
+ public void setUpTest() throws Exception
+ {
+ tempDirectory = new File(System.getProperty("java.io.tmpdir"),
+ UUID.randomUUID().toString());
+ assertTrue(tempDirectory.mkdirs());
+ contextProvider = MockUtil.getSCMContextProvider(tempDirectory);
+ fileSystem = new DefaultFileSystem();
+ InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver();
+ repositoryLocationResolver = new RepositoryLocationResolver(contextProvider, repositoryDAO, initialRepoLocationResolver);
+ postSetUp();
+ }
/**
* Method description
@@ -165,25 +189,6 @@ public class AbstractTestBase
}
}
- //~--- set methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @throws Exception
- */
- @Before
- public void setUpTest() throws Exception
- {
- tempDirectory = new File(System.getProperty("java.io.tmpdir"),
- UUID.randomUUID().toString());
- assertTrue(tempDirectory.mkdirs());
- contextProvider = MockUtil.getSCMContextProvider(tempDirectory);
- postSetUp();
- }
-
- //~--- methods --------------------------------------------------------------
/**
* Clears Shiro's thread state, ensuring the thread remains clean for
@@ -249,12 +254,4 @@ public class AbstractTestBase
subjectThreadState = createThreadState(subject);
subjectThreadState.bind();
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- protected SCMContextProvider contextProvider;
-
- /** Field description */
- private File tempDirectory;
}
diff --git a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java
index a12fb39726..823e88c9fc 100644
--- a/scm-test/src/main/java/sonia/scm/ManagerTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/ManagerTestBase.java
@@ -37,11 +37,16 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
+import sonia.scm.repository.InitialRepositoryLocationResolver;
+import sonia.scm.repository.RepositoryDAO;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.MockUtil;
import java.io.File;
import java.io.IOException;
+import static org.mockito.Mockito.mock;
+
/**
*
* @author Sebastian Sdorra
@@ -55,14 +60,21 @@ public abstract class ManagerTestBase
public TemporaryFolder tempFolder = new TemporaryFolder();
protected SCMContextProvider contextProvider;
-
+ protected RepositoryLocationResolver locationResolver;
+
protected Manager manager;
- protected File temp;
+
+ protected File temp ;
@Before
public void setUp() throws IOException {
- temp = tempFolder.newFolder();
+ if (temp == null){
+ temp = tempFolder.newFolder();
+ }
contextProvider = MockUtil.getSCMContextProvider(temp);
+ InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver();
+ RepositoryDAO repoDao = mock(RepositoryDAO.class);
+ locationResolver = new RepositoryLocationResolver(contextProvider, repoDao ,initialRepositoryLocationResolver);
manager = createManager();
manager.init(contextProvider);
}
diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
index 37f7266984..f48744d460 100644
--- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
@@ -43,7 +43,6 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
-import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -99,7 +98,6 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
handler.create(repository);
-
assertTrue(nativeRepoDirectory.exists());
assertTrue(nativeRepoDirectory.isDirectory());
checkDirectory(nativeRepoDirectory);
diff --git a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java
index 48504feaf2..f3f252053d 100644
--- a/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/store/BlobStoreTestBase.java
@@ -35,22 +35,24 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.io.ByteStreams;
-
import org.junit.Before;
import org.junit.Test;
-
import sonia.scm.AbstractTestBase;
-
-import static org.junit.Assert.*;
-
-//~--- JDK imports ------------------------------------------------------------
+import sonia.scm.repository.RepositoryTestData;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-
import java.util.List;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -58,12 +60,6 @@ import java.util.List;
public abstract class BlobStoreTestBase extends AbstractTestBase
{
- /**
- * Method description
- *
- *
- * @return
- */
protected abstract BlobStoreFactory createBlobStoreFactory();
/**
@@ -73,7 +69,10 @@ public abstract class BlobStoreTestBase extends AbstractTestBase
@Before
public void createBlobStore()
{
- store = createBlobStoreFactory().getBlobStore("test");
+ store = createBlobStoreFactory()
+ .withName("test")
+ .forRepository(RepositoryTestData.createHeartOfGold())
+ .build();
store.clear();
}
diff --git a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java
index 8d3a63717a..140bd54e65 100644
--- a/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/store/ConfigurationEntryStoreTestBase.java
@@ -32,12 +32,13 @@
package sonia.scm.store;
+import sonia.scm.repository.Repository;
+
/**
*
* @author Sebastian Sdorra
*/
-public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase
-{
+public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestBase {
/**
* Method description
@@ -48,17 +49,20 @@ public abstract class ConfigurationEntryStoreTestBase extends KeyValueStoreTestB
protected abstract ConfigurationEntryStoreFactory createConfigurationStoreFactory();
//~--- get methods ----------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @return
- */
@Override
- protected ConfigurationEntryStore getDataStore()
- {
- return createConfigurationStoreFactory().getStore(StoreObject.class,
- "test");
+ protected ConfigurationEntryStore getDataStore(Class type) {
+ return this.createConfigurationStoreFactory()
+ .withType(type)
+ .withName(storeName)
+ .build();
+ }
+
+ @Override
+ protected ConfigurationEntryStore getDataStore(Class type, Repository repository) {
+ return this.createConfigurationStoreFactory()
+ .withType(type)
+ .withName(repoStoreName)
+ .forRepository(repository)
+ .build();
}
}
diff --git a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java
index 3129d3a339..39ce021715 100644
--- a/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/store/DataStoreTestBase.java
@@ -33,6 +33,13 @@
package sonia.scm.store;
+import org.junit.Test;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryTestData;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
/**
*
* @author Sebastian Sdorra
@@ -48,17 +55,29 @@ public abstract class DataStoreTestBase extends KeyValueStoreTestBase
*/
protected abstract DataStoreFactory createDataStoreFactory();
+
//~--- get methods ----------------------------------------------------------
- /**
- * Method description
- *
- *
- * @return
- */
- @Override
- protected DataStore getDataStore()
+
+
+
+ @Test
+ public void shouldStoreRepositorySpecificData()
{
- return createDataStoreFactory().getStore(StoreObject.class, "test");
+ DataStoreFactory dataStoreFactory = createDataStoreFactory();
+ StoreObject obj = new StoreObject("test-1");
+ Repository repository = RepositoryTestData.createHeartOfGold();
+
+ DataStore store = dataStoreFactory
+ .withType(StoreObject.class)
+ .withName("test")
+ .forRepository(repository)
+ .build();
+
+ String id = store.put(obj);
+
+ assertNotNull(id);
+
+ assertEquals(obj, store.get(id));
}
}
diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java
index d5e9474ff5..2c5641bfd1 100644
--- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java
+++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java
@@ -43,8 +43,7 @@ package sonia.scm.store;
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
@Override
- public ConfigurationStore getStore(Class type, String name)
- {
+ public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
return new InMemoryConfigurationStore<>();
}
}
diff --git a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java
index 0abad4f558..a54b58178f 100644
--- a/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/store/KeyValueStoreTestBase.java
@@ -38,6 +38,8 @@ import org.junit.Before;
import org.junit.Test;
import sonia.scm.AbstractTestBase;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryTestData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -56,13 +58,21 @@ import java.util.Map;
public abstract class KeyValueStoreTestBase extends AbstractTestBase
{
+ protected Repository repository = RepositoryTestData.createHeartOfGold();
+ protected DataStore store;
+ protected DataStore repoStore;
+ protected String repoStoreName = "testRepoStore";
+ protected String storeName = "testStore";
+
/**
* Method description
*
*
* @return
*/
- protected abstract DataStore getDataStore();
+ protected abstract DataStore getDataStore(Class type , Repository repository);
+ protected abstract DataStore getDataStore(Class type );
+
//~--- methods --------------------------------------------------------------
@@ -73,8 +83,10 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase
@Before
public void before()
{
- store = getDataStore();
+ store = getDataStore(StoreObject.class);
+ repoStore = getDataStore(StoreObject.class, repository);
store.clear();
+ repoStore.clear();
}
/**
@@ -215,8 +227,5 @@ public abstract class KeyValueStoreTestBase extends AbstractTestBase
assertNull(store.get("2"));
}
- //~--- fields ---------------------------------------------------------------
- /** Field description */
- private DataStore store;
}
diff --git a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java
index c39efa3ffe..ef806c79f8 100644
--- a/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/store/StoreTestBase.java
@@ -35,10 +35,11 @@ package sonia.scm.store;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
-
import sonia.scm.AbstractTestBase;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
//~--- JDK imports ------------------------------------------------------------
@@ -65,8 +66,7 @@ public abstract class StoreTestBase extends AbstractTestBase
@Test
public void testGet()
{
- ConfigurationStore store = createStoreFactory().getStore(StoreObject.class,
- "test");
+ ConfigurationStore store = createStoreFactory().withType(StoreObject.class).withName("test").build();
assertNotNull(store);
@@ -82,8 +82,7 @@ public abstract class StoreTestBase extends AbstractTestBase
@Test
public void testSet()
{
- ConfigurationStore store = createStoreFactory().getStore(StoreObject.class,
- "test");
+ ConfigurationStore store = createStoreFactory().withType(StoreObject.class).withName("test").build();
assertNotNull(store);
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index 4dfb749690..01f45cb54e 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -72,8 +72,18 @@
io.jsonwebtoken
- jjwt
- 0.4
+ jjwt-impl
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
@@ -540,6 +550,7 @@
DEVELOPMENTtarget/scm-itdefault
+ 0.10.52.53.11.00.8.17
diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
index 90764a7e00..9555ad66b5 100644
--- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
@@ -83,8 +83,10 @@ import sonia.scm.security.AuthorizationChangedEventProducer;
import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.ConfigurableLoginAttemptHandler;
+import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy;
import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.DefaultSecuritySystem;
+import sonia.scm.security.JwtAccessTokenRefreshStrategy;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.SecuritySystem;
diff --git a/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java
index 8e2475d802..5fb5925e6c 100644
--- a/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java
+++ b/scm-webapp/src/main/java/sonia/scm/debug/DebugService.java
@@ -52,7 +52,7 @@ public final class DebugService
private final Multimap receivedHooks = LinkedListMultimap.create();
/**
- * Stores {@link DebugHookData} for the given repository.
+ * Store {@link DebugHookData} for the given repository.
*/
void put(NamespaceAndName namespaceAndName, DebugHookData hookData)
{
diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java
new file mode 100644
index 0000000000..266a327d44
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultJwtAccessTokenRefreshStrategy.java
@@ -0,0 +1,10 @@
+package sonia.scm.security;
+
+import sonia.scm.plugin.Extension;
+
+@Extension
+public class DefaultJwtAccessTokenRefreshStrategy extends PercentageJwtAccessTokenRefreshStrategy {
+ public DefaultJwtAccessTokenRefreshStrategy() {
+ super(0.5F);
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java
index d958dcf41f..e93d4de597 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java
@@ -108,9 +108,13 @@ public class DefaultSecuritySystem implements SecuritySystem
* @param storeFactory
*/
@Inject
+ @SuppressWarnings("unchecked")
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory)
{
- store = storeFactory.getStore(AssignedPermission.class, NAME);
+ store = storeFactory
+ .withType(AssignedPermission.class)
+ .withName(NAME)
+ .build();
readAvailablePermissions();
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java
index 46f4c68e74..8fb5929188 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java
@@ -31,9 +31,14 @@
package sonia.scm.security;
import io.jsonwebtoken.Claims;
+
+import java.util.Collections;
import java.util.Date;
+import java.util.Map;
import java.util.Optional;
+import static java.util.Optional.ofNullable;
+
/**
* Jwt implementation of {@link AccessToken}.
*
@@ -41,7 +46,9 @@ import java.util.Optional;
* @since 2.0.0
*/
public final class JwtAccessToken implements AccessToken {
-
+
+ public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshExpiration";
+ public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId";
private final Claims claims;
private final String compact;
@@ -75,6 +82,15 @@ public final class JwtAccessToken implements AccessToken {
return claims.getExpiration();
}
+ @Override
+ public Optional getRefreshExpiration() {
+ return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
+ }
+
+ public Optional getParentKey() {
+ return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString());
+ }
+
@Override
public Scope getScope() {
return Scopes.fromClaims(claims);
@@ -90,5 +106,9 @@ public final class JwtAccessToken implements AccessToken {
public String compact() {
return compact;
}
-
+
+ @Override
+ public Map getClaims() {
+ return Collections.unmodifiableMap(claims);
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java
index ece96e2954..66db720125 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java
@@ -36,10 +36,12 @@ import com.google.common.collect.Maps;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.time.Clock;
+import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
@@ -48,7 +50,7 @@ import org.slf4j.LoggerFactory;
/**
* Jwt implementation of {@link AccessTokenBuilder}.
- *
+ *
* @author Sebastian Sdorra
* @since 2.0.0
*/
@@ -58,21 +60,27 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
* the logger for JwtAccessTokenBuilder
*/
private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class);
-
- private final KeyGenerator keyGenerator;
- private final SecureKeyResolver keyResolver;
-
+
+ private final KeyGenerator keyGenerator;
+ private final SecureKeyResolver keyResolver;
+ private final Clock clock;
+
private String subject;
private String issuer;
- private long expiresIn = 60l;
- private TimeUnit expiresInUnit = TimeUnit.MINUTES;
+ private long expiresIn = 1;
+ private TimeUnit expiresInUnit = TimeUnit.HOURS;
+ private long refreshableFor = 12;
+ private TimeUnit refreshableForUnit = TimeUnit.HOURS;
+ private Instant refreshExpiration;
+ private String parentKeyId;
private Scope scope = Scope.empty();
-
+
private final Map custom = Maps.newHashMap();
-
- JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
+
+ JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Clock clock) {
this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver;
+ this.clock = clock;
}
@Override
@@ -81,7 +89,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
this.subject = subject;
return this;
}
-
+
@Override
public JwtAccessTokenBuilder custom(String key, Object value) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");
@@ -92,11 +100,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
@Override
public JwtAccessTokenBuilder scope(Scope scope) {
- Preconditions.checkArgument(scope != null, "scope can not be null");
+ Preconditions.checkArgument(scope != null, "scope cannot be null");
this.scope = scope;
return this;
}
-
+
@Override
public JwtAccessTokenBuilder issuer(String issuer) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed");
@@ -106,15 +114,37 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
@Override
public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) {
- Preconditions.checkArgument(count > 0, "expires in must be greater than 0");
- Preconditions.checkArgument(unit != null, "unit can not be null");
-
+ Preconditions.checkArgument(count > 0, "count must be greater than 0");
+ Preconditions.checkArgument(unit != null, "unit cannot be null");
+
this.expiresIn = count;
this.expiresInUnit = unit;
-
+
return this;
}
-
+
+ @Override
+ public JwtAccessTokenBuilder refreshableFor(long count, TimeUnit unit) {
+ Preconditions.checkArgument(count >= 0, "count must be greater or equal to 0");
+ Preconditions.checkArgument(unit != null, "unit cannot be null");
+
+ this.refreshableFor = count;
+ this.refreshableForUnit = unit;
+
+ return this;
+ }
+
+ JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) {
+ this.refreshExpiration = refreshExpiration;
+ this.refreshableFor = 0;
+ return this;
+ }
+
+ public JwtAccessTokenBuilder parentKey(String parentKeyId) {
+ this.parentKeyId = parentKeyId;
+ return this;
+ }
+
private String getSubject(){
if (subject == null) {
Subject currentSubject = SecurityUtils.getSubject();
@@ -130,35 +160,48 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
String id = keyGenerator.createKey();
String sub = getSubject();
-
+
LOG.trace("create new token {} for user {}", id, subject);
SecureKey key = keyResolver.getSecureKey(sub);
-
+
Map customClaims = new HashMap<>(custom);
-
+
// add scope to custom claims
Scopes.toClaims(customClaims, scope);
-
- Date now = new Date();
+
+ Instant now = clock.instant();
long expiration = expiresInUnit.toMillis(expiresIn);
-
+
Claims claims = Jwts.claims(customClaims)
.setSubject(sub)
.setId(id)
- .setIssuedAt(now)
- .setExpiration(new Date(now.getTime() + expiration));
-
+ .setIssuedAt(Date.from(now))
+ .setExpiration(new Date(now.toEpochMilli() + expiration));
+
+
+ if (refreshableFor > 0) {
+ long refreshExpiration = refreshableForUnit.toMillis(refreshableFor);
+ claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, new Date(now.toEpochMilli() + refreshExpiration).getTime());
+ } else if (refreshExpiration != null) {
+ claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, Date.from(refreshExpiration));
+ }
+ if (parentKeyId == null) {
+ claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, id);
+ } else {
+ claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, parentKeyId);
+ }
+
if ( issuer != null ) {
claims.setIssuer(issuer);
}
-
+
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, key.getBytes())
.compact();
-
+
return new JwtAccessToken(claims, compact);
}
-
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java
index 63c4ea981c..a5704b0e82 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java
@@ -30,6 +30,7 @@
*/
package sonia.scm.security;
+import java.time.Clock;
import java.util.Set;
import javax.inject.Inject;
import sonia.scm.plugin.Extension;
@@ -46,19 +47,25 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac
private final KeyGenerator keyGenerator;
private final SecureKeyResolver keyResolver;
private final Set enrichers;
+ private final Clock clock;
@Inject
public JwtAccessTokenBuilderFactory(
- KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers
- ) {
+ KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers) {
+ this(keyGenerator, keyResolver, enrichers, Clock.systemDefaultZone());
+ }
+
+ JwtAccessTokenBuilderFactory(
+ KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers, Clock clock) {
this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver;
this.enrichers = enrichers;
+ this.clock = clock;
}
@Override
public JwtAccessTokenBuilder create() {
- JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver);
+ JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, clock);
// enrich access token builder
enrichers.forEach((enricher) -> {
diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java
new file mode 100644
index 0000000000..9135a0e099
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefreshStrategy.java
@@ -0,0 +1,8 @@
+package sonia.scm.security;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+@ExtensionPoint(multi = false)
+public interface JwtAccessTokenRefreshStrategy {
+ boolean shouldBeRefreshed(JwtAccessToken oldToken);
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java
new file mode 100644
index 0000000000..6db01c904f
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java
@@ -0,0 +1,77 @@
+package sonia.scm.security;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.time.Clock;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class JwtAccessTokenRefresher {
+
+ private static final Logger log = LoggerFactory.getLogger(JwtAccessTokenRefresher.class);
+
+ private final JwtAccessTokenBuilderFactory builderFactory;
+ private final JwtAccessTokenRefreshStrategy refreshStrategy;
+ private final Clock clock;
+
+ @Inject
+ public JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy) {
+ this(builderFactory, refreshStrategy, Clock.systemDefaultZone());
+ }
+
+ JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy, Clock clock) {
+ this.builderFactory = builderFactory;
+ this.refreshStrategy = refreshStrategy;
+ this.clock = clock;
+ }
+
+ @SuppressWarnings("squid:S3655") // the refresh expiration cannot be null at the time building the new token, because
+ // we checked this before in tokenCanBeRefreshed
+ public Optional refresh(JwtAccessToken oldToken) {
+ JwtAccessTokenBuilder builder = builderFactory.create();
+ Map claims = oldToken.getClaims();
+ claims.forEach(builder::custom);
+
+ if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) {
+ Optional