Refactored model classes to use a key rather than direct ID.

This commit is contained in:
Josh Stark 2019-08-05 13:09:02 +01:00
parent 00ffb31c9c
commit 423ee87b18
30 changed files with 492 additions and 194 deletions

View File

@ -18,20 +18,19 @@
package io.linuxserver.fleet.cache;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.key.ImageKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
public class ImageCache {
private static final Logger LOGGER = LoggerFactory.getLogger(ImageCache.class);
private final Map<Integer, List<Image>> cachedImages;
private final Map<RepositoryKey, List<Image>> cachedImages;
public ImageCache() {
this.cachedImages = new HashMap<>();
@ -41,19 +40,19 @@ public class ImageCache {
Image updatedImage = Image.copyOf(image);
if (cachedImages.containsKey(updatedImage.getRepositoryId())) {
if (cachedImages.containsKey(updatedImage.getKey().getRepositoryKey())) {
List<Image> images = cachedImages.get(updatedImage.getRepositoryId());
List<Image> images = cachedImages.get(updatedImage.getKey().getRepositoryKey());
int cachedImageLocation = images.indexOf(updatedImage);
if (cachedImageLocation > -1) {
LOGGER.info("updateCache({}) Updating existing cached image.", updatedImage.getName());
LOGGER.info("updateCache({}) Updating existing cached image.", updatedImage);
images.set(cachedImageLocation, updatedImage);
} else {
LOGGER.info("updateCache({}) Adding image to cache", updatedImage.getName());
LOGGER.info("updateCache({}) Adding image to cache", updatedImage);
images.add(updatedImage);
}
@ -62,33 +61,37 @@ public class ImageCache {
List<Image> images = new ArrayList<>();
images.add(updatedImage);
LOGGER.info("updateCache({}:{}) Creating new cache for repository {}", updatedImage.getName(), updatedImage.getRepositoryId());
cachedImages.put(updatedImage.getRepositoryId(), images);
LOGGER.info("updateCache({}) Creating new cache for repository {}", updatedImage, updatedImage.getRepositoryId());
cachedImages.put(updatedImage.getKey().getRepositoryKey(), images);
}
}
public List<Image> getAll(int repositoryId) {
public List<Image> getAll(RepositoryKey repositoryKey) {
if (cachedImages.containsKey(repositoryId)) {
return cachedImages.get(repositoryId).stream().map(Image::copyOf).collect(Collectors.toList());
if (cachedImages.containsKey(repositoryKey)) {
return cachedImages.get(repositoryKey).stream().map(Image::copyOf).collect(Collectors.toList());
}
return new ArrayList<>();
}
public Image get(int repositoryId, String imageName) {
public Image get(ImageKey imageKey) {
if (cachedImages.containsKey(repositoryId)) {
return cachedImages.get(repositoryId).stream().filter(i -> i.getName().equals(imageName)).findFirst().orElse(null);
final RepositoryKey repositoryKey = imageKey.getRepositoryKey();
if (cachedImages.containsKey(repositoryKey)) {
return cachedImages.get(repositoryKey).stream().filter(i -> i.getName().equals(imageKey.getName())).findFirst().orElse(null);
}
return null;
}
public void remove(Image image) {
public void remove(ImageKey imageKey) {
if (cachedImages.containsKey(image.getRepositoryId())) {
cachedImages.get(image.getRepositoryId()).remove(image);
final RepositoryKey repositoryKey = imageKey.getRepositoryKey();
if (cachedImages.containsKey(repositoryKey)) {
cachedImages.get(repositoryKey).removeIf(image -> image.getKey().equals(imageKey));
}
}
}

View File

@ -23,8 +23,9 @@ import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.internal.ImagePullStat;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.internal.Tag;
import io.linuxserver.fleet.model.key.ImageKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,15 +47,15 @@ public class DefaultImageDAO implements ImageDAO {
}
@Override
public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) {
public Image findImageByRepositoryAndImageName(ImageKey imageKey) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Image_GetByName(?,?)}");
call.setInt(1, repositoryId);
call.setString(2, imageName);
call.setInt(1, imageKey.getRepositoryKey().getId());
call.setString(2, imageKey.getName());
ResultSet results = call.executeQuery();
if (results.next())
@ -70,16 +71,16 @@ public class DefaultImageDAO implements ImageDAO {
}
@Override
public Image fetchImage(Integer id) {
public Image fetchImage(ImageKey imageKey) {
LOGGER.debug("Fetching image by ID: " + id);
LOGGER.debug("Fetching image by ID: " + imageKey);
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Image_Get(?)}");
call.setInt(1, id);
call.setInt(1, imageKey.getId());
ResultSet results = call.executeQuery();
if (results.next())
@ -95,7 +96,7 @@ public class DefaultImageDAO implements ImageDAO {
}
@Override
public LimitedResult<Image> fetchImagesByRepository(int repositoryId) {
public LimitedResult<Image> fetchImagesByRepository(final RepositoryKey repositoryKey) {
List<Image> images = new ArrayList<>();
@ -104,7 +105,7 @@ public class DefaultImageDAO implements ImageDAO {
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Image_GetAll(?,?)}");
call.setInt(1, repositoryId);
call.setInt(1, repositoryKey.getId());
call.registerOutParameter(2, Types.INTEGER);
ResultSet results = call.executeQuery();
@ -131,7 +132,7 @@ public class DefaultImageDAO implements ImageDAO {
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
setNullableInt(call, 1, image.getId());
setNullableInt(call, 1, image.getKey().getId());
call.setInt(2, image.getRepositoryId());
call.setString(3, image.getName());
setNullableLong(call, 4, image.getPullCount());
@ -155,7 +156,7 @@ public class DefaultImageDAO implements ImageDAO {
String statusMessage = call.getString(15);
if (InsertUpdateStatus.OK == status)
return new InsertUpdateResult<>(fetchImage(imageId), status, statusMessage);
return new InsertUpdateResult<>(fetchImage(image.getKey().cloneWithId(imageId)), status, statusMessage);
return new InsertUpdateResult<>(status, statusMessage);
@ -170,14 +171,14 @@ public class DefaultImageDAO implements ImageDAO {
}
@Override
public void removeImage(Integer id) {
public void removeImage(final ImageKey imageKey) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Image_Delete(?)}");
call.setInt(1, id);
call.setInt(1, imageKey.getId());
call.executeUpdate();
call.close();
@ -190,7 +191,7 @@ public class DefaultImageDAO implements ImageDAO {
}
@Override
public List<ImagePullStat> fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode) {
public List<ImagePullStat> fetchImagePullHistory(final ImageKey imageKey, ImagePullStat.GroupMode groupMode) {
List<ImagePullStat> pullHistory = new ArrayList<>();
@ -199,7 +200,7 @@ public class DefaultImageDAO implements ImageDAO {
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("CALL Image_GetPullHistory(?, ?)");
call.setInt(1, imageId);
call.setInt(1, imageKey.getId());
call.setString(2, groupMode.toString());
ResultSet results = call.executeQuery();
@ -230,12 +231,14 @@ public class DefaultImageDAO implements ImageDAO {
private Image parseImageFromResultSet(ResultSet results) throws SQLException {
Image image = new Image(
results.getInt("ImageId"),
new Repository(
results.getInt("RepositoryId"),
results.getString("RepositoryName")
new ImageKey(
results.getInt("ImageId"),
results.getString("ImageName"),
new RepositoryKey(
results.getInt("RepositoryId"),
results.getString("RepositoryName")
)
),
results.getString("ImageName"),
new Tag(
results.getString("LatestTagVersion"),
results.getString("LatestMaskedTagVersion"),

View File

@ -21,6 +21,7 @@ import io.linuxserver.fleet.db.PoolingDatabaseConnection;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.key.RepositoryKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,9 +29,7 @@ import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.safeClose;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
import static io.linuxserver.fleet.db.dao.Utils.*;
public class DefaultRepositoryDAO implements RepositoryDAO {
@ -43,14 +42,14 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
}
@Override
public Repository fetchRepository(int id) {
public Repository fetchRepository(RepositoryKey repositoryKey) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Repository_Get(?)}");
call.setInt(1, id);
call.setInt(1, repositoryKey.getId());
ResultSet results = call.executeQuery();
if (results.next())
@ -73,7 +72,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}");
setNullableInt(call, 1, repository.getId());
setNullableInt(call, 1, repository.getKey().getId());
call.setString(2, repository.getName());
setNullableString(call, 3, repository.getVersionMask());
call.setBoolean(4, repository.isSyncEnabled());
@ -89,7 +88,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
String statusMessage = call.getString(7);
if (InsertUpdateStatus.OK == status)
return new InsertUpdateResult<>(fetchRepository(repositoryId), status, statusMessage);
return new InsertUpdateResult<>(fetchRepository(repository.getKey().cloneWithId(repositoryId)), status, statusMessage);
return new InsertUpdateResult<>(status, statusMessage);
@ -171,7 +170,7 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
private Repository parseRepositoryFromResultSet(ResultSet results) throws SQLException {
Repository repository = new Repository(results.getInt("RepositoryId"), results.getString("RepositoryName"))
Repository repository = new Repository(new RepositoryKey(results.getInt("RepositoryId"), results.getString("RepositoryName")))
.withSyncEnabled(results.getBoolean("SyncEnabled"))
.withVersionMask(results.getString("RepositoryVersionMask"))
.withModifiedTime(results.getTimestamp("ModifiedTime").toLocalDateTime());

View File

@ -85,7 +85,7 @@ public class DefaultUserDAO implements UserDAO {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_Save(?,?,?,?,?,?)}");
setNullableInt(call, 1, user.getId());
setNullableInt(call, 1, user.getKey().getId());
call.setString(2, user.getUsername());
setNullableString(call, 3, user.getPassword());
@ -137,7 +137,7 @@ public class DefaultUserDAO implements UserDAO {
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL User_Delete(?)}");
call.setInt(1, user.getId());
call.setInt(1, user.getKey().getId());
call.executeUpdate();

View File

@ -4,20 +4,22 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.internal.ImagePullStat;
import io.linuxserver.fleet.model.key.ImageKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
import java.util.List;
public interface ImageDAO {
Image findImageByRepositoryAndImageName(int repositoryId, String imageName);
Image findImageByRepositoryAndImageName(ImageKey imageKey);
Image fetchImage(Integer id);
Image fetchImage(ImageKey imageKey);
LimitedResult<Image> fetchImagesByRepository(int repositoryId);
LimitedResult<Image> fetchImagesByRepository(RepositoryKey repositoryKey);
InsertUpdateResult<Image> saveImage(Image image);
void removeImage(Integer id);
void removeImage(ImageKey imageKey);
List<ImagePullStat> fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode);
List<ImagePullStat> fetchImagePullHistory(ImageKey imageKey, ImagePullStat.GroupMode groupMode);
}

View File

@ -2,12 +2,13 @@ package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.key.RepositoryKey;
import java.util.List;
public interface RepositoryDAO {
Repository fetchRepository(int id);
Repository fetchRepository(RepositoryKey repositoryKey);
InsertUpdateResult<Repository> saveRepository(Repository repository);

View File

@ -24,6 +24,8 @@ import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.internal.ImagePullStat;
import io.linuxserver.fleet.model.key.ImageKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
import java.util.List;
@ -45,13 +47,13 @@ public class ImageDelegate {
* already has the image stored against the repository.
* </p>
*/
public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) {
public Image findImageByRepositoryAndImageName(final ImageKey imageKey) {
Image cachedImage = imageCache.get(repositoryId, imageName);
Image cachedImage = imageCache.get(imageKey);
if (null == cachedImage) {
Image image = imageDAO.findImageByRepositoryAndImageName(repositoryId, imageName);
Image image = imageDAO.findImageByRepositoryAndImageName(imageKey);
if (null != image) {
imageCache.updateCache(image);
@ -63,16 +65,16 @@ public class ImageDelegate {
return cachedImage;
}
public Image fetchImage(int id) {
return imageDAO.fetchImage(id);
public Image fetchImage(ImageKey imageKey) {
return imageDAO.fetchImage(imageKey);
}
public List<Image> fetchImagesByRepository(int repositoryId) {
public List<Image> fetchImagesByRepository(final RepositoryKey repositoryKey) {
List<Image> cachedImages = imageCache.getAll(repositoryId);
List<Image> cachedImages = imageCache.getAll(repositoryKey);
if (cachedImages.isEmpty()) {
List<Image> images = imageDAO.fetchImagesByRepository(repositoryId).getResults();
List<Image> images = imageDAO.fetchImagesByRepository(repositoryKey).getResults();
images.forEach(imageCache::updateCache);
return images;
@ -81,13 +83,13 @@ public class ImageDelegate {
return cachedImages;
}
public void removeImage(Integer id) {
public void removeImage(ImageKey imageKey) {
Image existingImage = imageDAO.fetchImage(id);
Image existingImage = imageDAO.fetchImage(imageKey);
if (null != existingImage) {
imageDAO.removeImage(id);
imageCache.remove(existingImage);
imageDAO.removeImage(imageKey);
imageCache.remove(imageKey);
}
}
@ -106,7 +108,7 @@ public class ImageDelegate {
throw new SaveException(result.getStatusMessage());
}
public List<ImagePullStat> fetchImagePullHistory(int id, ImagePullStat.GroupMode groupMode) {
return imageDAO.fetchImagePullHistory(id, groupMode);
public List<ImagePullStat> fetchImagePullHistory(ImageKey imageKey, ImagePullStat.GroupMode groupMode) {
return imageDAO.fetchImagePullHistory(imageKey, groupMode);
}
}

View File

@ -22,6 +22,7 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.key.RepositoryKey;
import java.util.List;
@ -33,8 +34,8 @@ public class RepositoryDelegate {
this.repositoryDAO = repositoryDAO;
}
public Repository fetchRepository(int id) {
return repositoryDAO.fetchRepository(id);
public Repository fetchRepository(RepositoryKey repositoryKey) {
return repositoryDAO.fetchRepository(repositoryKey);
}
public Repository saveRepository(Repository repository) throws SaveException {

View File

@ -20,8 +20,8 @@ package io.linuxserver.fleet.dockerhub.queue;
import io.linuxserver.fleet.delegate.DockerHubDelegate;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.queue.FleetRequest;
import java.util.Objects;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class DockerHubSyncRequest implements FleetRequest<DockerHubSyncResponse> {
@ -41,14 +41,15 @@ public class DockerHubSyncRequest implements FleetRequest<DockerHubSyncResponse>
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DockerHubSyncRequest that = (DockerHubSyncRequest) o;
return Objects.equals(image, that.image);
if (!(o instanceof DockerHubSyncRequest))
return false;
return EqualsBuilder.reflectionEquals(this, "dockerHubDelegate");
}
@Override
public int hashCode() {
return Objects.hash(image);
return HashCodeBuilder.reflectionHashCode(this, "dockerHubDelegate");
}
}

View File

@ -41,8 +41,9 @@ public class DockerHubSyncResponse implements FleetResponse {
@Override
public void handle() {
DockerImage dockerImage = dockerHubDelegate.fetchImageFromRepository(image.getRepository().getName(), image.getName());
String versionMask = getVersionMask(image.getRepository().getVersionMask(), image.getVersionMask());
DockerImage dockerImage = dockerHubDelegate.fetchImageFromRepository(image.getKey().getRepositoryKey().getName(), image.getName());
String versionMask = getVersionMask(null, image.getVersionMask());
Tag maskedVersion = getLatestTagAndCreateMaskedVersion(versionMask);
image.withPullCount(dockerImage.getPullCount());
@ -59,7 +60,7 @@ public class DockerHubSyncResponse implements FleetResponse {
private Tag getLatestTagAndCreateMaskedVersion(String versionMask) {
DockerTag tag = dockerHubDelegate.fetchLatestImageTag(image.getRepository().getName(), image.getName());
DockerTag tag = dockerHubDelegate.fetchLatestImageTag(image.getKey().getRepositoryKey().getName(), image.getName());
if (null == tag)
return Tag.NONE;

View File

@ -49,7 +49,7 @@ public class ApiImagePullHistory {
public static ApiImagePullHistory fromPullStats(Image image, List<ImagePullStat> stats) {
ApiImagePullHistory history = new ApiImagePullHistory();
history.imageId = image.getId();
history.imageId = image.getKey().getId();
history.imageName = image.getName();
history.groupMode = stats.get(0).getGroupMode().toString();

View File

@ -17,9 +17,11 @@
package io.linuxserver.fleet.model.internal;
import io.linuxserver.fleet.model.key.AbstractHasKey;
import io.linuxserver.fleet.model.key.ImageKey;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/**
* <p>
@ -27,36 +29,35 @@ import java.util.Objects;
* specific information regarding its build status and pull count.
* </p>
*/
public class Image extends PersistableItem<Image> {
public class Image extends AbstractHasKey<ImageKey> {
private final Repository repository;
private final String name;
private static final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss");
private Tag tag;
private long pullCount;
private String versionMask;
private boolean unstable;
private boolean hidden;
private LocalDateTime modifiedTime;
private boolean deprecated;
private String deprecationReason;
public Image(Integer id, Repository repository, String name, Tag latestVersion) {
super(id);
this.name = name;
this.repository = repository;
this.tag = new Tag(latestVersion.getVersion(), latestVersion.getMaskedVersion(), latestVersion.getBuildDate());
public Image(final ImageKey imageKey, final Tag latestVersion) {
super(imageKey);
this.tag = new Tag(latestVersion.getVersion(), latestVersion.getMaskedVersion(), latestVersion.getBuildDate());
}
public Image(Repository repository, String name) {
this(null, repository, name, Tag.NONE);
public Image(final ImageKey imageKey) {
this(imageKey, Tag.NONE);
}
public static Image makeFromKey(ImageKey imageKey) {
return new Image(imageKey);
}
public static Image copyOf(Image image) {
Image cloned = new Image(image.getId(), Repository.copyOf(image.repository), image.name, image.tag);
Image cloned = new Image(image.getKey(), image.tag);
cloned.pullCount = image.pullCount;
cloned.versionMask = image.versionMask;
cloned.unstable = image.unstable;
@ -103,20 +104,22 @@ public class Image extends PersistableItem<Image> {
return this;
}
public Image withModifiedTime(final LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
return this;
}
public void updateTag(Tag maskedVersion) {
this.tag = new Tag(maskedVersion.getVersion(), maskedVersion.getMaskedVersion(), maskedVersion.getBuildDate());
}
public int getRepositoryId() {
return repository.getId();
}
public Repository getRepository() {
return repository;
return getKey().getRepositoryKey().getId();
}
public String getName() {
return name;
return getKey().getName();
}
public long getPullCount() {
@ -138,7 +141,7 @@ public class Image extends PersistableItem<Image> {
public String getBuildDateAsString() {
if (getBuildDate() != null) {
return getBuildDate().format(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss"));
return getBuildDate().format(DATE_PATTERN);
}
return null;
@ -164,17 +167,20 @@ public class Image extends PersistableItem<Image> {
return deprecationReason;
}
@Override
public boolean equals(Object o) {
public LocalDateTime getModifiedTime() {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Image image = (Image) o;
return Objects.equals(repository, image.repository) && Objects.equals(name, image.name);
}
if (null != modifiedTime) {
@Override
public int hashCode() {
return Objects.hash(repository, name);
return LocalDateTime.of(
modifiedTime.getYear(),
modifiedTime.getMonth(),
modifiedTime.getDayOfMonth(),
modifiedTime.getHour(),
modifiedTime.getMinute(),
modifiedTime.getSecond()
);
}
return null;
}
}

View File

@ -17,29 +17,23 @@
package io.linuxserver.fleet.model.internal;
import java.util.Objects;
import io.linuxserver.fleet.model.key.AbstractHasKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
public class Repository extends PersistableItem<Repository> {
import java.time.LocalDateTime;
private final String name;
public class Repository extends AbstractHasKey<RepositoryKey> {
private String versionMask;
private boolean syncEnabled;
private String versionMask;
private boolean syncEnabled;
private LocalDateTime modifiedTime;
public Repository(Integer id, String name) {
super(id);
this.name = name;
}
public Repository(String name) {
super();
this.name = name;
public Repository(final RepositoryKey repositoryKey) {
super(repositoryKey);
}
public static Repository copyOf(Repository repository) {
return new Repository(repository.getId(), repository.name).withVersionMask(repository.versionMask).withSyncEnabled(repository.syncEnabled);
return new Repository(repository.getKey()).withVersionMask(repository.versionMask).withSyncEnabled(repository.syncEnabled);
}
public Repository withVersionMask(String versionMask) {
@ -54,8 +48,14 @@ public class Repository extends PersistableItem<Repository> {
return this;
}
public Repository withModifiedTime(LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
return this;
}
public String getName() {
return name;
return getKey().getName();
}
public String getVersionMask() {
@ -66,16 +66,20 @@ public class Repository extends PersistableItem<Repository> {
return syncEnabled;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Repository that = (Repository) o;
return Objects.equals(name, that.name);
}
public LocalDateTime getModifiedTime() {
@Override
public int hashCode() {
return Objects.hash(name);
if (null != modifiedTime) {
return LocalDateTime.of(
modifiedTime.getYear(),
modifiedTime.getMonth(),
modifiedTime.getDayOfMonth(),
modifiedTime.getHour(),
modifiedTime.getMinute(),
modifiedTime.getSecond()
);
}
return null;
}
}

View File

@ -17,20 +17,27 @@
package io.linuxserver.fleet.model.internal;
public class User extends PersistableItem<User> {
import io.linuxserver.fleet.model.key.AbstractHasKey;
import io.linuxserver.fleet.model.key.UserKey;
private final String username;
private final String password;
import java.time.LocalDateTime;
public class User extends AbstractHasKey<UserKey> {
private final String username;
private final String password;
private LocalDateTime modifiedTime;
public User(String username, String password) {
super();
super(new UserKey());
this.username = username;
this.password = password;
}
public User(Integer id, String username, String password) {
super(id);
super(new UserKey(id));
this.username = username;
this.password = password;
@ -43,4 +50,27 @@ public class User extends PersistableItem<User> {
public String getPassword() {
return password;
}
public User withModifiedTime(LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
return this;
}
public LocalDateTime getModifiedTime() {
if (null != modifiedTime) {
return LocalDateTime.of(
modifiedTime.getYear(),
modifiedTime.getMonth(),
modifiedTime.getDayOfMonth(),
modifiedTime.getHour(),
modifiedTime.getMinute(),
modifiedTime.getSecond()
);
}
return null;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public abstract class AbstractHasKey<KEY extends Key> implements HasKey<KEY> {
private final KEY key;
public AbstractHasKey(final KEY key) {
this.key = key;
}
@Override
public final KEY getKey() {
return key;
}
@Override
public int hashCode() {
return key.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HasKey<?>)) {
return false;
}
return key.equals(((HasKey<?>) o).getKey());
}
@Override
public final String toString() {
return key.toString();
}
}

View File

@ -15,41 +15,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.internal;
package io.linuxserver.fleet.model.key;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public abstract class AbstractKey implements Key {
import java.time.LocalDateTime;
private final Integer id;
public abstract class PersistableItem<T extends PersistableItem> {
private Integer id;
private LocalDateTime modifiedTime;
PersistableItem() { }
PersistableItem(Integer id) {
public AbstractKey(final Integer id) {
this.id = id;
}
@SuppressWarnings("unchecked")
public T withModifiedTime(LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
return (T) this;
}
public Integer getId() {
@Override
public final Integer getId() {
return id;
}
public LocalDateTime getModifiedTime() {
return modifiedTime;
@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) {
return false;
}
if (null == id) {
return ((Key) o).getId() == null;
}
return ((Key) o).getId().equals(id);
}
@Override
public int hashCode() {
if (null == id) {
return -1;
}
return id.hashCode();
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
return null == id ? "<NO_ID>" : String.valueOf(id);
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public interface HasKey<KEY extends Key> {
KEY getKey();
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public class ImageKey extends AbstractKey {
private final RepositoryKey repositoryKey;
private final String name;
public ImageKey(final String name, final RepositoryKey repositoryKey) {
this(null, name, repositoryKey);
}
public ImageKey(final Integer id, final String name, final RepositoryKey repositoryKey) {
super(id);
this.repositoryKey = repositoryKey;
this.name = name;
}
public static ImageKey makeForLookup(final int imageId) {
return new ImageKey(imageId, "<LookupKey>", null);
}
public ImageKey cloneWithId(int id) {
return new ImageKey(id, name, repositoryKey);
}
public final RepositoryKey getRepositoryKey() {
return repositoryKey;
}
public final String getName() {
return name;
}
@Override
public String toString() {
return super.toString() + ":" + (repositoryKey == null ? "<LookupKey>" : repositoryKey.getName()) + "/" + name;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public interface Key {
Integer getId();
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public class RepositoryKey extends AbstractKey {
private final String name;
public RepositoryKey(final int id) {
this(id, "<LookupKey>");
}
public RepositoryKey(final String name) {
this(null, name);
}
public RepositoryKey(final Integer id, final String name) {
super(id);
this.name = name;
}
public RepositoryKey cloneWithId(int id) {
return new RepositoryKey(id, name);
}
public final String getName() {
return name;
}
@Override
public String toString() {
return super.toString() + ":" + name;
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.key;
public class UserKey extends AbstractKey {
public UserKey() {
this(null);
}
public UserKey(Integer id) {
super(id);
}
}

View File

@ -24,6 +24,8 @@ import io.linuxserver.fleet.model.docker.DockerTag;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.internal.Tag;
import io.linuxserver.fleet.model.key.ImageKey;
import io.linuxserver.fleet.model.key.RepositoryKey;
import io.linuxserver.fleet.sync.event.ImageUpdateEvent;
import io.linuxserver.fleet.sync.event.RepositoriesScannedEvent;
import org.slf4j.Logger;
@ -167,7 +169,7 @@ public class DefaultSynchronisationState implements SynchronisationState {
if (!repositories.contains(storedRepository.getName())) {
LOGGER.info("Found repository which no longer exists in Docker Hub. Removing {}", storedRepository.getName());
context.getRepositoryDelegate().removeRepository(storedRepository.getId());
context.getRepositoryDelegate().removeRepository(storedRepository.getKey().getId());
}
}
}
@ -177,12 +179,12 @@ public class DefaultSynchronisationState implements SynchronisationState {
List<String> dockerHubImageNames = images.stream().map(DockerImage::getName).collect(Collectors.toList());
LOGGER.info("Checking for any removed images under {}", repository.getName());
for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getId())) {
for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getKey())) {
if (!dockerHubImageNames.contains(storedImage.getName())) {
LOGGER.info("Found image which no longer exists in Docker Hub. Removing {}", storedImage.getName());
context.getImageDelegate().removeImage(storedImage.getId());
context.getImageDelegate().removeImage(storedImage.getKey());
}
}
}
@ -216,7 +218,7 @@ public class DefaultSynchronisationState implements SynchronisationState {
try {
return context.getRepositoryDelegate().saveRepository(new Repository(repositoryName));
return context.getRepositoryDelegate().saveRepository(new Repository(new RepositoryKey(repositoryName)));
} catch (SaveException e) {
LOGGER.error("Tried to save new repository during sync but failed", e);
@ -232,13 +234,14 @@ public class DefaultSynchronisationState implements SynchronisationState {
*/
private Image configureImage(Repository repository, DockerImage dockerHubImage, SynchronisationContext context) {
Image image = context.getImageDelegate().findImageByRepositoryAndImageName(repository.getId(), dockerHubImage.getName());
final ImageKey baseImageKey = new ImageKey(dockerHubImage.getName(), repository.getKey());
final Image image = context.getImageDelegate().findImageByRepositoryAndImageName(baseImageKey);
if (isImageNew(image)) {
try {
return context.getImageDelegate().saveImage(new Image(repository, dockerHubImage.getName()));
return context.getImageDelegate().saveImage(Image.makeFromKey(baseImageKey));
} catch (SaveException e) {
LOGGER.error("Tried to save new image during sync but failed", e);

View File

@ -51,7 +51,7 @@ public class HomePage extends WebPage {
for (Repository repository : repositories) {
if (repository.isSyncEnabled())
populatedRepositories.add(new RepositoryWithImages(repository, imageDelegate.fetchImagesByRepository(repository.getId())));
populatedRepositories.add(new RepositoryWithImages(repository, imageDelegate.fetchImagesByRepository(repository.getKey())));
}
model.put("populatedRepositories", populatedRepositories);

View File

@ -55,7 +55,7 @@ public class AllImagesApi implements Route {
if (repository.isSyncEnabled()) {
List<Image> savedImages = imageDelegate.fetchImagesByRepository(repository.getId());
List<Image> savedImages = imageDelegate.fetchImagesByRepository(repository.getKey());
List<ApiImage> apiImages = new ArrayList<>();
for (Image savedImage : savedImages) {

View File

@ -21,6 +21,7 @@ import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import io.linuxserver.fleet.model.key.ImageKey;
import spark.Request;
import spark.Response;
import spark.Route;
@ -41,7 +42,7 @@ public class GetImageApi implements Route {
throw new FleetApiException(400, "Missing imageId param");
}
Image image = imageDelegate.fetchImage(Integer.parseInt(imageIdParam));
Image image = imageDelegate.fetchImage(ImageKey.makeForLookup(Integer.parseInt(imageIdParam)));
if (null == image) {
throw new FleetApiException(404, "Image not found");
}

View File

@ -23,6 +23,7 @@ import io.linuxserver.fleet.model.internal.ImagePullStat;
import io.linuxserver.fleet.model.api.ApiImagePullHistory;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import io.linuxserver.fleet.model.key.ImageKey;
import spark.Request;
import spark.Response;
import spark.Route;
@ -45,10 +46,10 @@ public class GetImagePullHistoryApi implements Route {
throw new FleetApiException(400, "Missing imageId param");
}
int imageId = Integer.parseInt(imageIdParam);
List<ImagePullStat> imagePullStats = imageDelegate.fetchImagePullHistory(imageId, getGroupMode(request));
final ImageKey lookupKey = ImageKey.makeForLookup(Integer.parseInt(imageIdParam));
List<ImagePullStat> imagePullStats = imageDelegate.fetchImagePullHistory(lookupKey, getGroupMode(request));
Image image = imageDelegate.fetchImage(imageId);
Image image = imageDelegate.fetchImage(lookupKey);
return new ApiResponse<>("OK", ApiImagePullHistory.fromPullStats(image, imagePullStats));
}

View File

@ -21,6 +21,7 @@ import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.model.internal.Image;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import io.linuxserver.fleet.model.key.ImageKey;
import spark.Request;
import spark.Response;
import spark.Route;
@ -40,7 +41,7 @@ public class ManageImageApi implements Route {
int imageId = Integer.parseInt(request.queryParams("imageId"));
Image image = imageDelegate.fetchImage(imageId);
Image image = imageDelegate.fetchImage(ImageKey.makeForLookup(imageId));
if (null == image) {
response.status(404);

View File

@ -21,6 +21,7 @@ import io.linuxserver.fleet.delegate.RepositoryDelegate;
import io.linuxserver.fleet.model.internal.Repository;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import io.linuxserver.fleet.model.key.RepositoryKey;
import spark.Request;
import spark.Response;
import spark.Route;
@ -40,7 +41,7 @@ public class ManageRepositoryApi implements Route {
int repositoryId = Integer.parseInt(request.queryParams("repositoryId"));
Repository repository = repositoryDelegate.fetchRepository(repositoryId);
Repository repository = repositoryDelegate.fetchRepository(new RepositoryKey(repositoryId));
if (null == repository) {
response.status(404);

View File

@ -34,7 +34,7 @@
<tbody>
<#list repositories as repository>
<tr data-repository-id="#{repository.id}">
<tr data-repository-id="#{repository.key.id}">
<td>
${repository.name}
</td>

View File

@ -10,7 +10,7 @@
<ul class="nav nav--repositories" id="all-repositories-tablist" role="tablist">
<#list populatedRepositories as populatedRepository>
<li class="nav-item">
<a class="nav-link <#if populatedRepository?index == 0>active</#if>" data-toggle="tab" role="tab" aria-controls="repository_#{populatedRepository.repository.id}" id="repository-tab_#{populatedRepository.repository.id}" href="#repository_#{populatedRepository.repository.id}">${populatedRepository.repository.name}</a>
<a class="nav-link <#if populatedRepository?index == 0>active</#if>" data-toggle="tab" role="tab" aria-controls="repository_#{populatedRepository.repository.key.id}" id="repository-tab_#{populatedRepository.repository.key.id}" href="#repository_#{populatedRepository.repository.key.id}">${populatedRepository.repository.name}</a>
</li>
</#list>
</ul>
@ -21,7 +21,7 @@
<div class="tab-content" id="repository-tab-content">
<#list populatedRepositories as populatedRepository>
<div class="tab-pane<#if populatedRepository?index == 0> show active</#if>" id="repository_#{populatedRepository.repository.id}" role="tabpanel" aria-labelledby="repository-tab_#{populatedRepository.repository.id}">
<div class="tab-pane<#if populatedRepository?index == 0> show active</#if>" id="repository_#{populatedRepository.repository.key.id}" role="tabpanel" aria-labelledby="repository-tab_#{populatedRepository.repository.key.id}">
<div class="container">
<#if populatedRepository.everyImageStable>
@ -48,15 +48,15 @@
<div class="col-md-8">
<div class="input-group input-group-sm mb-3 mt-3">
<div class="input-group-prepend">
<span class="input-group-text" id="searchLabel_#{populatedRepository.repository.id}"><i class="fas fa-search"></i> Search ${populatedRepository.repository.name}</span>
<span class="input-group-text" id="searchLabel_#{populatedRepository.repository.key.id}"><i class="fas fa-search"></i> Search ${populatedRepository.repository.name}</span>
</div>
<input type="text" class="form-control image-search" id="search_#{populatedRepository.repository.id}" data-repository-id="#{populatedRepository.repository.id}" aria-describedby="searchLabel_#{populatedRepository.repository.id}">
<input type="text" class="form-control image-search" id="search_#{populatedRepository.repository.key.id}" data-repository-id="#{populatedRepository.repository.key.id}" aria-describedby="searchLabel_#{populatedRepository.repository.key.id}">
</div>
</div>
<div class="col-md-2"></div>
<div class="col-12">
<div class="table-responsive" id="#{populatedRepository.repository.id}_images">
<div class="table-responsive" id="#{populatedRepository.repository.key.id}_images">
<table class="table table--sortable">
<thead>
<tr>
@ -73,7 +73,7 @@
<tbody>
<#list populatedRepository.images as image>
<#if !image.hidden || __AUTHENTICATED_USER?has_content>
<tr class="<#if image.hidden>hidden-image</#if><#if image.deprecated>deprecated-image</#if>" data-image-id="#{image.id}" data-image-name="${image.name}">
<tr class="<#if image.hidden>hidden-image</#if><#if image.deprecated>deprecated-image</#if>" data-image-id="#{image.key.id}" data-image-name="${image.name}">
<td class="image-name">
<span class="image-name--repository">${populatedRepository.repository.name} /</span> <a target="_blank" href="https://hub.docker.com/r/${populatedRepository.repository.name}/${image.name}"><span class="image-name__image">${image.name}</span></a>
<#if image.deprecated>
@ -105,10 +105,10 @@
<#if __AUTHENTICATED_USER?has_content>
<td class="admin-actions">
<div class="dropdown">
<button class="btn btn-info btn-xsm dropdown-toggle" type="button" id="admin-actions_#{populatedRepository.repository.id}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-info btn-xsm dropdown-toggle" type="button" id="admin-actions_#{populatedRepository.repository.key.id}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="admin-actions_#{populatedRepository.repository.id}">
<div class="dropdown-menu" aria-labelledby="admin-actions_#{populatedRepository.repository.key.id}">
<#if image.hidden>
<button type="button" class="image--show dropdown-item btn-clickable"><i class="fas fa-eye"></i> Show in list</button>
@ -124,7 +124,7 @@
<#if image.deprecated>
<button type="button" class="image--remove-deprecation-notice dropdown-item btn-clickable"><i class="fas fa-thumbs-up"></i> Remove deprecation notice</button>
<#else>
<button id="deprecate-image_#{image.id}" type="button" class="dropdown-item btn-clickable" data-toggle="modal" data-target="#update-image-deprecation"><i class="fas fa-exclamation-circle"></i> Mark as deprecated</button>
<button id="deprecate-image_#{image.key.id}" type="button" class="dropdown-item btn-clickable" data-toggle="modal" data-target="#update-image-deprecation"><i class="fas fa-exclamation-circle"></i> Mark as deprecated</button>
</#if>
</div>
</div>