This commit is contained in:
Mohamed Karray
2018-11-29 16:01:43 +01:00
62 changed files with 1253 additions and 983 deletions

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,145 @@
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<String, Path> 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<String, Path> 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();
if (!Files.exists(parent)) {
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);
}
}

View File

@@ -1,101 +0,0 @@
package sonia.scm.repository.xml;
import org.apache.commons.lang.StringUtils;
import sonia.scm.ModelObject;
import sonia.scm.repository.Repository;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
@XmlRootElement(name = "repository-link")
@XmlAccessorType(XmlAccessType.FIELD)
public class RepositoryPath implements ModelObject {
private String path;
private String id;
private Long lastModified;
private Long creationDate;
@XmlTransient
private Repository repository;
@XmlTransient
private boolean toBeSynchronized;
/**
* Needed from JAXB
*/
public RepositoryPath() {
}
public RepositoryPath(String path, String id, Repository repository) {
this.path = path;
this.id = id;
this.repository = repository;
}
public Repository getRepository() {
return repository;
}
public void setRepository(Repository repository) {
this.repository = repository;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String getId() {
return id;
}
@Override
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
@Override
public Long getCreationDate() {
return creationDate;
}
@Override
public void setCreationDate(Long creationDate) {
this.creationDate = creationDate;
}
public void setId(String id) {
this.id = id;
}
@Override
public Long getLastModified() {
return lastModified;
}
@Override
public String getType() {
return getRepository()!= null? getRepository().getType():"";
}
@Override
public boolean isValid() {
return StringUtils.isNotEmpty(path);
}
public boolean toBeSynchronized() {
return toBeSynchronized;
}
public void setToBeSynchronized(boolean toBeSynchronized) {
this.toBeSynchronized = toBeSynchronized;
}
}

View File

@@ -1,19 +1,19 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* <p>
* 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.
* <p>
* 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.
*
* <p>
* http://bitbucket.org/sdorra/scm-manager
*
*/
@@ -34,153 +33,229 @@ 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.InitialRepositoryLocationResolver.InitialRepositoryLocation;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathBasedRepositoryDAO;
import sonia.scm.repository.Repository;
import sonia.scm.store.JAXBConfigurationStore;
import sonia.scm.store.Store;
import sonia.scm.store.StoreConstants;
import sonia.scm.xml.AbstractXmlDAO;
import java.io.File;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Collection;
import java.util.Optional;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Sebastian Sdorra
*/
@Singleton
public class XmlRepositoryDAO
extends AbstractXmlDAO<Repository, XmlRepositoryDatabase>
implements PathBasedRepositoryDAO {
public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
public static final String STORE_NAME = "repositories";
private static final String STORE_NAME = "repositories";
private final PathDatabase pathDatabase;
private final MetadataStore metadataStore = new MetadataStore();
private InitialRepositoryLocationResolver initialRepositoryLocationResolver;
private final FileSystem fileSystem;
private final SCMContextProvider context;
private final InitialRepositoryLocationResolver locationResolver;
private final FileSystem fileSystem;
//~--- constructors ---------------------------------------------------------
@VisibleForTesting
Clock clock = Clock.systemUTC();
private Long creationTime;
private Long lastModified;
private Map<String, Path> pathById;
private Map<String, Repository> byId;
private Map<NamespaceAndName, Repository> byNamespaceAndName;
@Inject
public XmlRepositoryDAO(InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, SCMContextProvider context) {
super(new JAXBConfigurationStore<>(XmlRepositoryDatabase.class,
new File(context.getBaseDirectory(), Store.CONFIG.getGlobalStoreDirectory()+File.separator+ STORE_NAME + StoreConstants.FILE_EXTENSION)));
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
this.fileSystem = fileSystem;
public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) {
this.context = context;
this.locationResolver = locationResolver;
this.fileSystem = fileSystem;
this.creationTime = clock.millis();
this.pathById = new LinkedHashMap<>();
this.byId = new LinkedHashMap<>();
this.byNamespaceAndName = new LinkedHashMap<>();
pathDatabase = new PathDatabase(createStorePath());
read();
}
//~--- methods --------------------------------------------------------------
private void read() {
Path storePath = createStorePath();
@Override
public boolean contains(NamespaceAndName namespaceAndName) {
return db.contains(namespaceAndName);
if (!Files.exists(storePath)) {
return;
}
pathDatabase.read(this::loadDates, this::loadRepository);
}
//~--- get methods ----------------------------------------------------------
@Override
public Repository get(NamespaceAndName namespaceAndName) {
return db.get(namespaceAndName);
private void loadDates(Long creationTime, Long lastModified) {
this.creationTime = creationTime;
this.lastModified = lastModified;
}
//~--- methods --------------------------------------------------------------
private void loadRepository(String id, Path repositoryPath) {
Path metadataPath = createMetadataPath(context.resolve(repositoryPath));
Repository repository = metadataStore.read(metadataPath);
byId.put(id, repository);
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
pathById.put(id, repositoryPath);
}
@VisibleForTesting
Path createStorePath() {
return context.getBaseDirectory()
.toPath()
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
@VisibleForTesting
Path createMetadataPath(Path repositoryPath) {
return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
}
@Override
public void modify(Repository repository) {
RepositoryPath repositoryPath = findExistingRepositoryPath(repository).orElseThrow(() -> new InternalRepositoryException(repository, "path object for repository not found"));
repositoryPath.setRepository(repository);
repositoryPath.setToBeSynchronized(true);
storeDB();
public String getType() {
return "xml";
}
@Override
public Long getCreationTime() {
return creationTime;
}
@Override
public Long getLastModified() {
return lastModified;
}
@Override
public void add(Repository repository) {
InitialRepositoryLocation initialLocation = initialRepositoryLocationResolver.getRelativeRepositoryPath(repository);
Repository clone = repository.clone();
Path repositoryPath = locationResolver.getPath(repository.getId());
Path resolvedPath = context.resolve(repositoryPath);
try {
fileSystem.create(initialLocation.getAbsolutePath());
fileSystem.create(resolvedPath.toFile());
Path metadataPath = createMetadataPath(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, "could not create directory for repository data: " + initialLocation.getAbsolutePath(), e);
}
RepositoryPath repositoryPath = new RepositoryPath(initialLocation.getRelativePath(), repository.getId(), repository.clone());
repositoryPath.setToBeSynchronized(true);
synchronized (store) {
db.add(repositoryPath);
storeDB();
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) {
RepositoryPath repositoryPath = db.get(id);
if (repositoryPath != null) {
return repositoryPath.getRepository();
}
return null;
return byId.get(id);
}
@Override
public Collection<Repository> getAll() {
return db.getRepositories();
return ImmutableList.copyOf(byNamespaceAndName.values());
}
/**
* Method description
*
* @param repository
* @return
*/
@Override
protected Repository clone(Repository repository) {
return repository.clone();
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 = createMetadataPath(repositoryPath);
metadataStore.write(metadataPath, clone);
}
@Override
public void delete(Repository repository) {
Path directory = getPath(repository);
super.delete(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(directory.toFile());
fileSystem.destroy(path.toFile());
} catch (IOException e) {
throw new InternalRepositoryException(repository, "could not delete repository directory", e);
throw new InternalRepositoryException(repository, "failed to destroy filesystem", e);
}
}
/**
* Method description
*
* @return
*/
@Override
protected XmlRepositoryDatabase createNewDatabase() {
return new XmlRepositoryDatabase();
}
@Override
public Path getPath(Repository repository) {
return context
.getBaseDirectory()
.toPath()
.resolve(
findExistingRepositoryPath(repository)
.map(RepositoryPath::getPath)
.orElseThrow(() -> new InternalRepositoryException(repository, "could not find base directory for repository")));
}
private Optional<RepositoryPath> findExistingRepositoryPath(Repository repository) {
return db.values().stream()
.filter(repoPath -> repoPath.getId().equals(repository.getId()))
.findAny();
public Path getPath(String repositoryId) {
return pathById.get(repositoryId);
}
}

View File

@@ -1,188 +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.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
//~--- JDK imports ------------------------------------------------------------
@XmlRootElement(name = "repository-db")
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlRepositoryDatabase implements XmlDatabase<RepositoryPath> {
private Long creationTime;
private Long lastModified;
@XmlJavaTypeAdapter(XmlRepositoryMapAdapter.class)
@XmlElement(name = "repositories")
private Map<String, RepositoryPath> repositoryPathMap = new LinkedHashMap<>();
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(RepositoryPath repositoryPath)
{
repositoryPathMap.put(createKey(repositoryPath.getRepository()), repositoryPath);
}
public boolean contains(NamespaceAndName namespaceAndName)
{
return repositoryPathMap.containsKey(createKey(namespaceAndName));
}
@Override
public boolean contains(String id)
{
return get(id) != null;
}
@Override
public RepositoryPath remove(String id)
{
return repositoryPathMap.remove(createKey(get(id).getRepository()));
}
public Collection<Repository> getRepositories() {
List<Repository> repositories = new ArrayList<>();
for (RepositoryPath repositoryPath : repositoryPathMap.values()) {
Repository repository = repositoryPath.getRepository();
repositories.add(repository);
}
return repositories;
}
@Override
public Collection<RepositoryPath> values()
{
return repositoryPathMap.values();
}
public Repository get(NamespaceAndName namespaceAndName) {
RepositoryPath repositoryPath = repositoryPathMap.get(createKey(namespaceAndName));
if (repositoryPath != null) {
return repositoryPath.getRepository();
}
return null;
}
@Override
public RepositoryPath get(String id) {
return values().stream()
.filter(repoPath -> repoPath.getId().equals(id))
.findFirst()
.orElse(null);
}
/**
* 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;
}
}

View File

@@ -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<RepositoryPath>
{
/**
* Constructs ...
*
*/
public XmlRepositoryList() {}
/**
* Constructs ...
*
*
*
* @param repositoryMap
*/
public XmlRepositoryList(Map<String, RepositoryPath> repositoryMap)
{
this.repositories = new LinkedList<>(repositoryMap.values());
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public Iterator<RepositoryPath> iterator()
{
return repositories.iterator();
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public LinkedList<RepositoryPath> getRepositoryPaths()
{
return repositories;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param repositories
*/
public void setRepositories(LinkedList<RepositoryPath> repositories)
{
this.repositories = repositories;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@XmlElement(name = "repository-path")
private LinkedList<RepositoryPath> repositories;
}

View File

@@ -1,112 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* <p>
* 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.
* <p>
* 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.
* <p>
* http://bitbucket.org/sdorra/scm-manager
*/
package sonia.scm.repository.xml;
import sonia.scm.SCMContext;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.store.StoreConstants;
import sonia.scm.store.StoreException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Sebastian Sdorra
*/
public class XmlRepositoryMapAdapter extends XmlAdapter<XmlRepositoryList, Map<String, RepositoryPath>> {
@Override
public XmlRepositoryList marshal(Map<String, RepositoryPath> repositoryMap) {
XmlRepositoryList repositoryPaths = new XmlRepositoryList(repositoryMap);
try {
JAXBContext context = JAXBContext.newInstance(Repository.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
// marshall the repo_path/metadata.xml files
for (RepositoryPath repositoryPath : repositoryPaths.getRepositoryPaths()) {
if (repositoryPath.toBeSynchronized()) {
File baseDirectory = SCMContext.getContext().getBaseDirectory();
Path dir = baseDirectory.toPath().resolve(repositoryPath.getPath());
if (!Files.isDirectory(dir)) {
throw new InternalRepositoryException(repositoryPath.getRepository(), "repository path not found");
}
marshaller.marshal(repositoryPath.getRepository(), getRepositoryMetadataFile(dir.toFile()));
repositoryPath.setToBeSynchronized(false);
}
}
} catch (JAXBException ex) {
throw new StoreException("failed to marshal repository database", ex);
}
return repositoryPaths;
}
private File getRepositoryMetadataFile(File dir) {
return new File(dir, StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
}
@Override
public Map<String, RepositoryPath> unmarshal(XmlRepositoryList repositoryPaths) {
Map<String, RepositoryPath> repositoryPathMap = new LinkedHashMap<>();
try {
JAXBContext context = JAXBContext.newInstance(Repository.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
for (RepositoryPath repositoryPath : repositoryPaths) {
SCMContextProvider contextProvider = SCMContext.getContext();
File baseDirectory = contextProvider.getBaseDirectory();
Repository repository = (Repository) unmarshaller.unmarshal(getRepositoryMetadataFile(baseDirectory.toPath().resolve(repositoryPath.getPath()).toFile()));
repositoryPath.setRepository(repository);
repositoryPathMap.put(XmlRepositoryDatabase.createKey(repository), repositoryPath);
}
} catch (JAXBException ex) {
throw new StoreException("failed to unmarshal object", ex);
}
return repositoryPathMap;
}
}

View File

@@ -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<V> implements ConfigurationEntryStore<V
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param writer
*/
private void close(XMLStreamWriter writer)
{
if (writer != null)
{
try
{
writer.close();
}
catch (XMLStreamException ex)
{
logger.error("could not close writer", ex);
}
}
}
/**
* Method description
*
*
* @param reader
*/
private void close(XMLStreamReader reader)
{
if (reader != null)
{
try
{
reader.close();
}
catch (XMLStreamException ex)
{
logger.error("could not close reader", ex);
}
}
}
/**
* Method description
*
*
* @return
*
* @throws FileNotFoundException
*/
private Reader createReader() throws FileNotFoundException
{
return new InputStreamReader(new FileInputStream(file), Charsets.UTF_8);
}
/**
* Method description
*
*
* @return
*
* @throws FileNotFoundException
*/
private Writer createWriter() throws FileNotFoundException
{
return new OutputStreamWriter(new FileOutputStream(file), Charsets.UTF_8);
}
/**
* Method description
*
@@ -333,15 +250,13 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
{
logger.debug("load configuration from {}", file);
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
XMLStreamReader reader = null;
try
{
Unmarshaller u = context.createUnmarshaller();
reader = xmlInputFactory.createXMLStreamReader(createReader());
reader = XmlStreams.createReader(file);
// configuration
reader.nextTag();
@@ -390,7 +305,7 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
}
finally
{
close(reader);
XmlStreams.close(reader);
}
}
@@ -402,17 +317,8 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
{
logger.debug("store configuration to {}", file);
IndentXMLStreamWriter writer = null;
try
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(file))
{
//J-
writer = new IndentXMLStreamWriter(
XMLOutputFactory.newInstance().createXMLStreamWriter(
createWriter()
)
);
//J+
writer.writeStartDocument();
// configuration start
@@ -453,10 +359,6 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
{
throw new StoreException("could not store configuration", ex);
}
finally
{
close(writer);
}
}
//~--- fields ---------------------------------------------------------------

View File

@@ -0,0 +1,71 @@
package sonia.scm.xml;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
public final class XmlStreams {
private static final Logger LOG = LoggerFactory.getLogger(XmlStreams.class);
private XmlStreams() {
}
public static void close(XMLStreamWriter writer) {
if (writer != null) {
try {
writer.close();
} catch (XMLStreamException ex) {
LOG.error("could not close writer", ex);
}
}
}
public static void close(XMLStreamReader reader) {
if (reader != null) {
try {
reader.close();
} catch (XMLStreamException ex) {
LOG.error("could not close reader", ex);
}
}
}
public static XMLStreamReader createReader(Path path) throws IOException, XMLStreamException {
return createReader(Files.newBufferedReader(path, Charsets.UTF_8));
}
public static XMLStreamReader createReader(File file) throws IOException, XMLStreamException {
return createReader(file.toPath());
}
private static XMLStreamReader createReader(Reader reader) throws XMLStreamException {
return XMLInputFactory.newInstance().createXMLStreamReader(reader);
}
public static IndentXMLStreamWriter createWriter(Path path) throws IOException, XMLStreamException {
return createWriter(Files.newBufferedWriter(path, Charsets.UTF_8));
}
public static IndentXMLStreamWriter createWriter(File file) throws IOException, XMLStreamException {
return createWriter(file.toPath());
}
private static IndentXMLStreamWriter createWriter(Writer writer) throws XMLStreamException {
return new IndentXMLStreamWriter(XMLOutputFactory.newFactory().createXMLStreamWriter(writer));
}
}

View File

@@ -1,111 +1,362 @@
package sonia.scm.repository.xml;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import com.google.common.base.Charsets;
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 org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.StoreParameters;
import sonia.scm.repository.RepositoryTestData;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicLong;
import static org.assertj.core.api.Assertions.assertThat;
import static org.codehaus.groovy.runtime.InvokerHelper.asList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.repository.xml.XmlRepositoryDAO.STORE_NAME;
@RunWith(MockitoJUnitRunner.class)
@Ignore
public class XmlRepositoryDAOTest {
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@MockitoSettings(strictness = Strictness.LENIENT)
class XmlRepositoryDAOTest {
@Mock
private ConfigurationStoreFactory storeFactory;
@Mock
private ConfigurationStore<XmlRepositoryDatabase> store;
@Mock
private XmlRepositoryDatabase db;
@Mock
private SCMContextProvider context;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock
private InitialRepositoryLocationResolver locationResolver;
private final FileSystem fileSystem = new DefaultFileSystem();
private FileSystem fileSystem = new DefaultFileSystem();
@Before
public void init() throws IOException {
StoreParameters storeParameters = new StoreParameters().withType(XmlRepositoryDatabase.class).withName(STORE_NAME).build();
when(storeFactory.getStore(storeParameters)).thenReturn(store);
when(store.get()).thenReturn(db);
when(context.getBaseDirectory()).thenReturn(temporaryFolder.newFolder());
private XmlRepositoryDAO dao;
private Path baseDirectory;
private AtomicLong atomicClock;
@BeforeEach
void createDAO(@TempDirectory.TempDir Path baseDirectory) {
this.baseDirectory = baseDirectory;
this.atomicClock = new AtomicLong();
when(locationResolver.getPath("42")).thenReturn(Paths.get("repos", "42"));
when(locationResolver.getPath("42+1")).thenReturn(Paths.get("repos", "puzzle"));
when(context.getBaseDirectory()).thenReturn(baseDirectory.toFile());
when(context.resolve(any(Path.class))).then(ic -> {
Path path = ic.getArgument(0);
return baseDirectory.resolve(path);
});
dao = createDAO();
}
private XmlRepositoryDAO createDAO() {
XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem);
Clock clock = mock(Clock.class);
when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
dao.clock = clock;
return dao;
}
@Test
public void addShouldCreateNewRepositoryPathWithRelativePath() {
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(context);
XmlRepositoryDAO dao = new XmlRepositoryDAO(initialRepositoryLocationResolver, fileSystem, context);
dao.add(new Repository("id", "git", "namespace", "name"));
verify(db).add(argThat(repositoryPath -> {
assertThat(repositoryPath.getId()).isEqualTo("id");
assertThat(repositoryPath.getPath()).isEqualTo(InitialRepositoryLocationResolver.DEFAULT_REPOSITORY_PATH + "/id");
return true;
}));
verify(store).set(db);
void shouldReturnXmlType() {
assertThat(dao.getType()).isEqualTo("xml");
}
@Test
public void modifyShouldStoreChangedRepository() {
Repository oldRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("/path", "id", oldRepository);
when(db.values()).thenReturn(asList(repositoryPath));
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
Repository newRepository = new Repository("id", "new", null, null);
dao.modify(newRepository);
assertThat(repositoryPath.getRepository()).isSameAs(newRepository);
verify(store).set(db);
void shouldReturnCreationTimeAfterCreation() {
long now = System.currentTimeMillis();
assertThat(dao.getCreationTime()).isBetween(now - 200, now + 200);
}
@Test
public void shouldGetPathInBaseDirectoryForRelativePath() {
Repository existingRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("path", "id", existingRepository);
when(db.values()).thenReturn(asList(repositoryPath));
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
Path path = dao.getPath(existingRepository);
assertThat(path.toString()).isEqualTo(context.getBaseDirectory().getPath() + "/path");
void shouldNotReturnLastModifiedAfterCreation() {
assertThat(dao.getLastModified()).isNull();
}
@Test
public void shouldGetPathInBaseDirectoryForAbsolutePath() {
Repository existingRepository = new Repository("id", "old", null, null);
RepositoryPath repositoryPath = new RepositoryPath("/tmp/path", "id", existingRepository);
when(db.values()).thenReturn(asList(repositoryPath));
void shouldReturnTrueForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
XmlRepositoryDAO dao = new XmlRepositoryDAO(new InitialRepositoryLocationResolver(context), fileSystem, context);
assertThat(dao.contains(heartOfGold)).isTrue();
assertThat(dao.contains(heartOfGold.getId())).isTrue();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isTrue();
}
Path path = dao.getPath(existingRepository);
private Repository createHeartOfGold() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId("42");
return heartOfGold;
}
assertThat(path.toString()).isEqualTo("/tmp/path");
@Test
void shouldReturnFalseForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold();
assertThat(dao.contains(heartOfGold)).isFalse();
assertThat(dao.contains(heartOfGold.getId())).isFalse();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isFalse();
}
@Test
void shouldReturnNullForEachGetMethod() {
assertThat(dao.get("42")).isNull();
assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isNull();
}
@Test
void shouldReturnRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
assertThat(dao.get("42")).isEqualTo(heartOfGold);
assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isEqualTo(heartOfGold);
}
@Test
void shouldNotReturnTheSameInstance() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Repository repository = dao.get("42");
assertThat(repository).isNotSameAs(heartOfGold);
}
@Test
void shouldReturnAllRepositories() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Repository puzzle = createPuzzle();
dao.add(puzzle);
Collection<Repository> repositories = dao.getAll();
assertThat(repositories).containsExactlyInAnyOrder(heartOfGold, puzzle);
}
private Repository createPuzzle() {
Repository puzzle = RepositoryTestData.create42Puzzle();
puzzle.setId("42+1");
return puzzle;
}
@Test
void shouldModifyRepository() {
Repository heartOfGold = createHeartOfGold();
heartOfGold.setDescription("HeartOfGold");
dao.add(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold");
heartOfGold = createHeartOfGold();
heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold");
}
@Test
void shouldRemoveRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
assertThat(dao.contains("42")).isTrue();
dao.delete(heartOfGold);
assertThat(dao.contains("42")).isFalse();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldUpdateLastModifiedAfterEachWriteOperation() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Long firstLastModified = dao.getLastModified();
assertThat(firstLastModified).isNotNull();
Repository puzzle = createPuzzle();
dao.add(puzzle);
Long lastModifiedAdded = dao.getLastModified();
assertThat(lastModifiedAdded).isGreaterThan(firstLastModified);
heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold);
Long lastModifiedModified = dao.getLastModified();
assertThat(lastModifiedModified).isGreaterThan(lastModifiedAdded);
dao.delete(puzzle);
Long lastModifiedRemoved = dao.getLastModified();
assertThat(lastModifiedRemoved).isGreaterThan(lastModifiedModified);
}
@Test
void shouldRenameTheRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg");
heartOfGold.setName("hog");
dao.modify(heartOfGold);
Repository repository = dao.get("42");
assertThat(repository.getNamespace()).isEqualTo("hg2tg");
assertThat(repository.getName()).isEqualTo("hog");
assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldDeleteRepositoryEvenWithChangedNamespace() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg");
heartOfGold.setName("hog");
dao.delete(heartOfGold);
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldReturnThePathForTheRepository() {
Path repositoryPath = Paths.get("r", "42");
when(locationResolver.getPath("42")).thenReturn(repositoryPath);
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = dao.getPath("42");
assertThat(path).isEqualTo(repositoryPath);
}
@Test
void shouldCreateTheDirectoryForTheRepository() {
Path repositoryPath = Paths.get("r", "42");
when(locationResolver.getPath("42")).thenReturn(repositoryPath);
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = getAbsolutePathFromDao("42");
assertThat(path).isDirectory();
}
@Test
void shouldRemoveRepositoryDirectoryAfterDeletion() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = getAbsolutePathFromDao(heartOfGold.getId());
assertThat(path).isDirectory();
dao.delete(heartOfGold);
assertThat(path).doesNotExist();
}
private Path getAbsolutePathFromDao(String id) {
return context.resolve(dao.getPath(id));
}
@Test
void shouldCreateRepositoryPathDatabase() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path storePath = dao.createStorePath();
assertThat(storePath).isRegularFile();
String content = content(storePath);
assertThat(content).contains(heartOfGold.getId());
assertThat(content).contains(dao.getPath(heartOfGold.getId()).toString());
}
private String content(Path storePath) throws IOException {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
}
@Test
void shouldStoreRepositoryMetadataAfterAdd() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.createMetadataPath(repositoryDirectory);
assertThat(metadataPath).isRegularFile();
String content = content(metadataPath);
assertThat(content).contains(heartOfGold.getName());
assertThat(content).contains(heartOfGold.getNamespace());
assertThat(content).contains(heartOfGold.getDescription());
}
@Test
void shouldUpdateRepositoryMetadataAfterModify() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setDescription("Awesome Spaceship");
dao.modify(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.createMetadataPath(repositoryDirectory);
String content = content(metadataPath);
assertThat(content).contains("Awesome Spaceship");
}
@Test
void shouldReadPathDatabaseAndMetadataOfRepositories() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
// reload data
dao = createDAO();
heartOfGold = dao.get("42");
assertThat(heartOfGold.getName()).isEqualTo("HeartOfGold");
Path path = getAbsolutePathFromDao(heartOfGold.getId());
assertThat(path).isDirectory();
}
@Test
void shouldReadCreationTimeAndLastModifedDateFromDatabase() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Long creationTime = dao.getCreationTime();
Long lastModified = dao.getLastModified();
// reload data
dao = createDAO();
assertThat(dao.getCreationTime()).isEqualTo(creationTime);
assertThat(dao.getLastModified()).isEqualTo(lastModified);
}
}