diff --git a/photoalbum-bootstrap/pom.xml b/photoalbum-bootstrap/pom.xml index a8a04bb..99668ba 100644 --- a/photoalbum-bootstrap/pom.xml +++ b/photoalbum-bootstrap/pom.xml @@ -19,6 +19,57 @@ UTF-8 + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + com.mgu.photoalbum.bootstrap.Bootstrap + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + + + + + + + com.mgu.photoalbum diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/domain/Photo.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/domain/Photo.java index b7de23d..330b97e 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/domain/Photo.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/domain/Photo.java @@ -176,4 +176,8 @@ public void untag() { public static PhotoBuilder create() { return new PhotoBuilder(); } + + public boolean anyTagMatches(final List tags) { + return tags.isEmpty() || getTags().stream().anyMatch(tag -> tags.contains(tag)); + } } \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumCommandService.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumCommandService.java index 14d4e42..28de4a0 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumCommandService.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumCommandService.java @@ -5,4 +5,6 @@ public interface AlbumCommandService { String createAlbum(String ownerId, String title); void deleteAlbum(String id); + + void removePhotoFromAlbum(String albumId, String photoId); } diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumService.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumService.java index 97ae640..abdc159 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumService.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/AlbumService.java @@ -4,7 +4,10 @@ import com.mgu.photoalbum.domain.Album; import com.mgu.photoalbum.identity.IdGenerator; import com.mgu.photoalbum.storage.AlbumRepository; +import org.ektorp.UpdateConflictException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; @@ -54,6 +57,20 @@ public void deleteAlbum(final String id) { repository.remove(album); } + @Override + public void removePhotoFromAlbum(String albumId, String photoId) { + if (!repository.contains(albumId)) { + return; + } + final Album album = repository.get(albumId); + album.dissociatePhoto(photoId); + try { + repository.update(album); + } catch (UpdateConflictException e) { + throw new UnableToUpdateAlbumException(albumId, e); + } + } + @Override public Album albumById(final String id) { if (!repository.contains(id)) { @@ -64,7 +81,9 @@ public Album albumById(final String id) { } @Override - public List albumsByOwner(String ownerId) { - return null; + public List albumsByOwner(final String ownerId) { + final List albumsByOwner = new ArrayList<>(); + albumsByOwner.addAll(repository.getAllByOwner(ownerId)); + return Collections.unmodifiableList(albumsByOwner); } } \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoQueryService.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoQueryService.java index 39fe113..79b674f 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoQueryService.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoQueryService.java @@ -6,13 +6,11 @@ public interface PhotoQueryService { - byte[] originalById(String id); // byte[] originalById(PhotoId identity) + byte[] originalById(String id); byte[] thumbnailById(String id); Photo photoById(String id); - List photosByAlbumAndTags(String albumId, List tags); - List search(String albumId, List tags, int offset, int pageSize); } \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoService.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoService.java index 40c74c1..16a71bd 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoService.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/PhotoService.java @@ -16,6 +16,7 @@ import java.nio.file.Path; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; public class PhotoService implements PhotoCommandService, PhotoQueryService { @@ -23,6 +24,8 @@ public class PhotoService implements PhotoCommandService, PhotoQueryService { private final PhotoRepository repository; + private final AlbumCommandService albumCommandService; + private final PathScheme pathScheme; private final PathAdapter pathAdapter; @@ -35,6 +38,7 @@ public class PhotoService implements PhotoCommandService, PhotoQueryService { @Inject public PhotoService( + final AlbumCommandService albumCommandService, final PhotoRepository repository, final PathScheme pathScheme, final PathAdapter pathAdapter, @@ -42,11 +46,12 @@ public PhotoService( final IdGenerator idGenerator, final ImageScaler scaler) { this.repository = repository; + this.albumCommandService = albumCommandService; this.pathAdapter = pathAdapter; this.inputStreamAdapter = inputStreamAdapter; this.pathScheme = pathScheme; this.scaler = scaler; - this.photoIdGenerator = () -> idGenerator.generateId("AL", 14); + this.photoIdGenerator = () -> idGenerator.generateId("PH", 14); } @Override @@ -100,6 +105,7 @@ public void deletePhoto(final String photoId) { final Path photoFolder = pathScheme.constructPathToPhotoFolder(photo.getOwnerId(), photo.getAlbumId(), photoId); pathAdapter.deleteDirectory(photoFolder); repository.remove(photo); + albumCommandService.removePhotoFromAlbum(photo.getAlbumId(), photoId); } @Override @@ -114,7 +120,7 @@ public void updateMetadata(final String photoId, final String description, final try { repository.update(photo); } catch (UpdateConflictException e) { - throw new UnableToUpdateMetadata(photoId, description, tags); + throw new UnableToUpdateMetadataException(photoId, description, tags); } } @@ -160,13 +166,14 @@ public Photo photoById(final String photoId) { return repository.get(photoId); } - @Override - public List photosByAlbumAndTags(final String albumId, final List tags) { - return null; - } - @Override public List search(final String albumId, final List tags, final int offset, final int pageSize) { - return null; + return repository + .getAllByAlbum(albumId) + .stream() + .filter(photo -> photo.anyTagMatches(tags)) + .skip(offset) + .limit(pageSize) + .collect(Collectors.toList()); } } \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateAlbumException.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateAlbumException.java new file mode 100644 index 0000000..468e954 --- /dev/null +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateAlbumException.java @@ -0,0 +1,17 @@ +package com.mgu.photoalbum.service; + +import com.mgu.photoalbum.exception.PhotoalbumException; + +public class UnableToUpdateAlbumException extends PhotoalbumException { + + private static final String ERROR_MESSAGE = "Unable to update the album with ID %s."; + + public UnableToUpdateAlbumException(final String albumId, final Throwable t) { + super(String.format(ERROR_MESSAGE, albumId)); + } + + @Override + public String getErrorCode() { + return "P005"; + } +} \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadata.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadataException.java similarity index 71% rename from photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadata.java rename to photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadataException.java index 4f91ba0..360747d 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadata.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/service/UnableToUpdateMetadataException.java @@ -5,11 +5,11 @@ import java.util.List; -public class UnableToUpdateMetadata extends PhotoalbumException { +public class UnableToUpdateMetadataException extends PhotoalbumException { private static final String ERROR_MESSAGE = "Unable to set description to %s and replace existing tags by %s for photo with ID %s."; - public UnableToUpdateMetadata(final String photoId, final String description, final List tags) { + public UnableToUpdateMetadataException(final String photoId, final String description, final List tags) { super(String.format(ERROR_MESSAGE, description, StringUtils.join(tags, ","), photoId)); } diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/AlbumRepository.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/AlbumRepository.java index 567c0ee..91c68ac 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/AlbumRepository.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/AlbumRepository.java @@ -4,7 +4,16 @@ import com.mgu.photoalbum.adapter.couchdb.DocumentRepository; import com.mgu.photoalbum.domain.Album; import org.ektorp.CouchDbConnector; +import org.ektorp.ViewQuery; +import org.ektorp.support.View; +import org.ektorp.support.Views; +import java.util.List; + +@Views({ + @View(name = "all", map = "function(doc) { if (doc.type == '" + Album.DOCUMENT_TYPE + "') emit(null, doc._id)}"), + @View(name = "by_owner", map = "function(doc) { if ('type' in doc && doc.type === 'album') { if ('ownerId' in doc) { emit(doc.ownerId, doc); } } }") +}) public class AlbumRepository extends DocumentRepository { @Inject @@ -12,4 +21,13 @@ public AlbumRepository(final CouchDbConnector connector) { super(Album.class, connector); initStandardDesignDocument(); } + + public List getAllByOwner(final String ownerId) { + final ViewQuery query = new ViewQuery() + .designDocId("_design/Album") + .viewName("by_owner") + .includeDocs(true) + .key(ownerId); + return connector.queryView(query, Album.class); + } } \ No newline at end of file diff --git a/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/PhotoRepository.java b/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/PhotoRepository.java index 04a6057..83d84a1 100644 --- a/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/PhotoRepository.java +++ b/photoalbum-management/src/main/java/com/mgu/photoalbum/storage/PhotoRepository.java @@ -4,7 +4,16 @@ import com.mgu.photoalbum.adapter.couchdb.DocumentRepository; import com.mgu.photoalbum.domain.Photo; import org.ektorp.CouchDbConnector; +import org.ektorp.ViewQuery; +import org.ektorp.support.View; +import org.ektorp.support.Views; +import java.util.List; + +@Views({ + @View(name = "all", map = "function(doc) { if (doc.type == '" + Photo.DOCUMENT_TYPE + "') emit(null, doc._id)}"), + @View(name = "by_album", map = "function(doc) { if ('type' in doc && doc.type === 'photo') { if ('albumId' in doc) { emit(doc.albumId, doc); } } }") +}) public class PhotoRepository extends DocumentRepository { @Inject @@ -12,4 +21,13 @@ public PhotoRepository(final CouchDbConnector connector) { super(Photo.class, connector); initStandardDesignDocument(); } + + public List getAllByAlbum(final String albumId) { + final ViewQuery query = new ViewQuery() + .designDocId("_design/Photo") + .viewName("by_album") + .includeDocs(true) + .key(albumId); + return connector.queryView(query, Photo.class); + } } \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/PhotoalbumApplication.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/PhotoalbumApplication.java index 6aa6ad3..94b668f 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/PhotoalbumApplication.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/PhotoalbumApplication.java @@ -1,6 +1,7 @@ package com.mgu.photoalbum; import com.hubspot.dropwizard.guice.GuiceBundle; +import com.mgu.photoalbum.config.CrossOriginConfig; import com.mgu.photoalbum.config.ServiceConfig; import com.mgu.photoalbum.config.ServiceModule; import com.mgu.photoalbum.security.Principal; @@ -9,6 +10,11 @@ import io.dropwizard.auth.basic.BasicAuthFactory; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; +import org.eclipse.jetty.servlets.CrossOriginFilter; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import java.util.EnumSet; public class PhotoalbumApplication extends Application { @@ -35,6 +41,16 @@ public void run(final ServiceConfig serviceConfig, final Environment environment environment .jersey() .register(AuthFactory.binder(new BasicAuthFactory<>(authenticator, "Photoalbum Realm", Principal.class))); + + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + + FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORSFilter", CrossOriginFilter.class); + + filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, environment.getApplicationContext().getContextPath() + "*"); + filter.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,PUT,POST,DELETE,OPTIONS"); + filter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); + filter.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "Origin, Content-Type, Accept, Authorization"); + filter.setInitParameter(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, "true"); } @Override diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/config/CrossOriginConfig.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/config/CrossOriginConfig.java new file mode 100644 index 0000000..5f0939b --- /dev/null +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/config/CrossOriginConfig.java @@ -0,0 +1,112 @@ +package com.mgu.photoalbum.config; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jetty.servlets.CrossOriginFilter; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import java.util.Enumeration; +import java.util.Vector; + +@Deprecated +public class CrossOriginConfig implements FilterConfig { + + /** + * CORS-related parameter that represents all allowed methods. + * This is currently hard-wired to GET and POST. + * It is not configurable via configuration file. + */ + private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS"; + + /** + * CORS-related parameter that represents allowed origins. Can + * contain the special value '*' which is a placeholder for every + * origin domain. + */ + private static final String ALLOWED_ORIGINS = "*"; + + /** + * CORS-related parameter that represents a list of comma-separated + * headers. Those headers are allowed. + */ + private static final String ALLOWED_HEADERS = "Origin, Content-Type, Accept, Authorization"; + + /** + * CORS-related parameter which configures the maximum age of + * a pre-flight request. Default value is 1 day. + */ + private static final int PREFLIGHT_MAX_AGE = 1800; + + /** + * CORS-related parameter that determines if credentials are allowed. + * Default is false. + */ + private static final boolean ALLOW_CREDENTIALS = true; + + /** + * CORS-related parameter which holds all exposable headers. + */ + private static final String EXPOSED_HEADERS = StringUtils.EMPTY; + + /** + * CORS-related parameter which configures chaining of pre-flight + * requests. + */ + private static final boolean CHAIN_PREFLIGHT = true; + + @Override + public String getFilterName() { + return "CORS Filter"; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public String getInitParameter(final String name) { + switch (name) { + case CrossOriginFilter.ALLOWED_ORIGINS_PARAM: + return ALLOWED_ORIGINS; + case CrossOriginFilter.ALLOWED_METHODS_PARAM: + return ALLOWED_METHODS; + case CrossOriginFilter.ALLOWED_HEADERS_PARAM: + return ALLOWED_HEADERS; + case CrossOriginFilter.PREFLIGHT_MAX_AGE_PARAM: + return Integer.toString(PREFLIGHT_MAX_AGE); + case CrossOriginFilter.ALLOW_CREDENTIALS_PARAM: + return Boolean.toString(ALLOW_CREDENTIALS); + case CrossOriginFilter.EXPOSED_HEADERS_PARAM: + return EXPOSED_HEADERS; + case CrossOriginFilter.CHAIN_PREFLIGHT_PARAM: + return Boolean.toString(CHAIN_PREFLIGHT); + default: + return null; + } + } + + @Override + public Enumeration getInitParameterNames() { + final Vector parameterNames = new Vector<>(); + parameterNames.add(CrossOriginFilter.ALLOWED_ORIGINS_PARAM); + parameterNames.add(CrossOriginFilter.ALLOWED_METHODS_PARAM); + parameterNames.add(CrossOriginFilter.ALLOWED_HEADERS_PARAM); + parameterNames.add(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM); + parameterNames.add(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM); + parameterNames.add(CrossOriginFilter.EXPOSED_HEADERS_PARAM); + parameterNames.add(CrossOriginFilter.PREFLIGHT_MAX_AGE_PARAM); + return parameterNames.elements(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("CorsFilterConfig[").append("allowCredentials=").append(ALLOW_CREDENTIALS) + .append(", allowedHeaders=[").append(ALLOWED_HEADERS).append("], allowedMethods=[") + .append(ALLOWED_METHODS).append("], allowedOrigins=[").append(ALLOWED_ORIGINS) + .append("], chainPreflight=").append(CHAIN_PREFLIGHT).append(", exposedHeaders=[") + .append(EXPOSED_HEADERS).append("], preflightMaxAge=").append(PREFLIGHT_MAX_AGE); + return sb.toString(); + } +} \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/AlbumShortReprConverter.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/AlbumShortReprConverter.java index f844da1..ad903a1 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/AlbumShortReprConverter.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/AlbumShortReprConverter.java @@ -22,6 +22,7 @@ public AlbumShortRepr convert(final Album album) { return AlbumShortRepr .create() + .id(album.getId()) .title(album.getTitle()) .numberOfPhotos(album.getContainingPhotos().size()) .link( diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoMetadataReprConverter.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoMetadataReprConverter.java index 150a1fa..331da9a 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoMetadataReprConverter.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoMetadataReprConverter.java @@ -40,6 +40,7 @@ public PhotoMetadataRepr convert(final Photo photo) { .href(linkScheme.toPhoto(photo.getAlbumId(), photo.getId())) .method(HttpMethod.GET) .relation("viewPhoto") + .mediaType("image/jpeg") .build() ) .link( @@ -53,7 +54,7 @@ public PhotoMetadataRepr convert(final Photo photo) { .link( LinkRepr .create() - .href(linkScheme.toMetadata(photo.getAlbumId(), photo.getId())) + .href(linkScheme.toPhoto(photo.getAlbumId(), photo.getId())) .method(HttpMethod.PUT) .relation("updateMetadata") .build() diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoShortReprConverter.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoShortReprConverter.java index 99f6cf2..abbc387 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoShortReprConverter.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/converter/PhotoShortReprConverter.java @@ -30,6 +30,7 @@ public PhotoShortRepr convert(final Photo photo) { .href(linkScheme.toPhoto(photo.getAlbumId(), photo.getId())) .method(HttpMethod.GET) .relation("viewPhoto") + .mediaType("image/jpeg") .build() ) .link( @@ -38,12 +39,13 @@ public PhotoShortRepr convert(final Photo photo) { .href(linkScheme.toThumbnail(photo.getAlbumId(), photo.getId())) .method(HttpMethod.GET) .relation("viewThumbnail") + .mediaType("image/jpeg") .build() ) .link( LinkRepr .create() - .href(linkScheme.toMetadata(photo.getAlbumId(), photo.getId())) + .href(linkScheme.toPhoto(photo.getAlbumId(), photo.getId())) .method(HttpMethod.GET) .relation("viewMetadata") .build() @@ -54,6 +56,7 @@ public PhotoShortRepr convert(final Photo photo) { .href(linkScheme.toDownload(photo.getAlbumId(), photo.getId())) .method(HttpMethod.GET) .relation("downloadPhoto") + .mediaType("image/jpeg") .build() ) .build(); diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/AlbumShortRepr.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/AlbumShortRepr.java index db15420..3f8c113 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/AlbumShortRepr.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/AlbumShortRepr.java @@ -4,16 +4,23 @@ import org.apache.commons.lang3.StringUtils; import java.util.Collections; -import java.util.LinkedList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class AlbumShortRepr { public static class AlbumShortReprBuilder { + private Map namedLinks = new HashMap<>(); + private String albumId = StringUtils.EMPTY; private String title = StringUtils.EMPTY; private int numberOfPhotos = 0; - private List links = new LinkedList<>(); + + public AlbumShortReprBuilder id(final String albumId) { + this.albumId = albumId; + return this; + } public AlbumShortReprBuilder title(final String title) { this.title = title; @@ -26,7 +33,7 @@ public AlbumShortReprBuilder numberOfPhotos(final int numberOfPhotos) { } public AlbumShortReprBuilder link(final LinkRepr link) { - this.links.add(link); + this.namedLinks.put(link.getRelation(), link); return this; } @@ -35,6 +42,9 @@ public AlbumShortRepr build() { } } + @JsonProperty("albumId") + private final String albumId; + @JsonProperty("title") private final String title; @@ -42,12 +52,17 @@ public AlbumShortRepr build() { private final int numberOfPhotos; @JsonProperty("links") - private final List links; + private final Map namedLinks; private AlbumShortRepr(final AlbumShortReprBuilder builder) { + this.albumId = builder.albumId; this.title = builder.title; this.numberOfPhotos = builder.numberOfPhotos; - this.links = builder.links; + this.namedLinks = builder.namedLinks; + } + + public String getAlbumId() { + return this.albumId; } public String getTitle() { @@ -58,8 +73,8 @@ public int getNumberOfPhotos() { return numberOfPhotos; } - public List getLinks() { - return Collections.unmodifiableList(links); + public Map getNamedLinks() { + return Collections.unmodifiableMap(namedLinks); } public static AlbumShortReprBuilder create() { diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/LinkRepr.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/LinkRepr.java index c16854c..bf52f80 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/LinkRepr.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/LinkRepr.java @@ -5,6 +5,7 @@ import org.apache.commons.lang3.StringUtils; import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MediaType; import java.net.URI; public class LinkRepr { @@ -14,6 +15,7 @@ public static class LinkReprBuilder { private String relation = StringUtils.EMPTY; private String href = StringUtils.EMPTY; private String method = HttpMethod.GET; + private String mediaType = MediaType.APPLICATION_JSON; public LinkReprBuilder relation(final String relation) { this.relation = relation; @@ -35,6 +37,11 @@ public LinkReprBuilder method(final String method) { return this; } + public LinkReprBuilder mediaType(final String mediaType) { + this.mediaType = mediaType; + return this; + } + public LinkRepr build() { return new LinkRepr(this); } @@ -49,16 +56,21 @@ public LinkRepr build() { @JsonProperty("method") private final String method; - public LinkRepr(final String relation, final String href, final String method) { + @JsonProperty("media") + private final String mediaType; + + public LinkRepr(final String relation, final String href, final String method, final String mediaType) { this.relation = relation; this.href = href; this.method = method; + this.mediaType = mediaType; } private LinkRepr(final LinkReprBuilder builder) { this.relation = builder.relation; this.href = builder.href; this.method = builder.method; + this.mediaType = builder.mediaType; } public String getRelation() { @@ -73,6 +85,10 @@ public String getMethod() { return method; } + public String getMediaType() { + return this.mediaType; + } + public static LinkReprBuilder create() { return new LinkReprBuilder(); } diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/PhotoShortRepr.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/PhotoShortRepr.java index a02969b..ea22951 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/PhotoShortRepr.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/representation/PhotoShortRepr.java @@ -2,8 +2,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; public class PhotoShortRepr { @@ -11,7 +14,7 @@ public static class PhotoShortReprBuilder { private String description; private List tags = new LinkedList<>(); - private List links = new LinkedList<>(); + private Map namedLinks = new HashMap<>(); public PhotoShortReprBuilder description(final String description) { this.description = description; @@ -24,7 +27,7 @@ public PhotoShortReprBuilder tags(final List tags) { } public PhotoShortReprBuilder link(final LinkRepr link) { - this.links.add(link); + this.namedLinks.put(link.getRelation(), link); return this; } @@ -40,12 +43,12 @@ public PhotoShortRepr build() { private final List tags; @JsonProperty("links") - private final List links; + private final Map namedLinks; private PhotoShortRepr(final PhotoShortReprBuilder builder) { this.description = builder.description; this.tags = builder.tags; - this.links = builder.links; + this.namedLinks = builder.namedLinks; } public String getDescription() { @@ -53,11 +56,11 @@ public String getDescription() { } public List getTags() { - return tags; + return Collections.unmodifiableList(tags); } - public List getLinks() { - return links; + public Map getNamedLinks() { + return Collections.unmodifiableMap(namedLinks); } public static PhotoShortReprBuilder create() { diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumResource.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumResource.java index 9a1a8fd..6046b27 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumResource.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumResource.java @@ -11,6 +11,7 @@ import com.mgu.photoalbum.security.Authorization; import com.mgu.photoalbum.security.Principal; import com.mgu.photoalbum.security.UserIsNotAuthorizedException; +import com.mgu.photoalbum.service.AlbumCommandService; import com.mgu.photoalbum.service.AlbumQueryService; import com.mgu.photoalbum.service.PhotoCommandService; import com.mgu.photoalbum.service.PhotoQueryService; @@ -40,6 +41,8 @@ public class AlbumResource { private final AlbumQueryService albumQueryService; + private final AlbumCommandService albumCommandService; + private final PhotoCommandService photoCommandService; private final PhotoQueryService photoQueryService; @@ -52,11 +55,13 @@ public class AlbumResource { @Inject public AlbumResource( + final AlbumCommandService albumCommandService, final AlbumQueryService albumQueryService, final PhotoQueryService photoQueryService, final PhotoCommandService photoCommandService, final Authorization authorization, final AlbumReprConverter albumConverter) { + this.albumCommandService = albumCommandService; this.albumQueryService = albumQueryService; this.photoQueryService = photoQueryService; this.photoCommandService = photoCommandService; @@ -76,7 +81,7 @@ public Response listAlbum( @QueryParam("pageSize") Optional optionalPageSize, @QueryParam("tags") Optional optionalTags) { - Album album = albumQueryService.albumById(albumId); + final Album album = albumQueryService.albumById(albumId); if (!authorization.isAuthorized(principal, album)) { throw new UserIsNotAuthorizedException(principal); @@ -104,11 +109,18 @@ private Optional> parseTags(Optional optionalTags) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Timed public Response uploadPhoto( @Auth Principal principal, @PathParam("albumId") String albumId, UploadPhotoRepr uploadPhotoRepr) { + final Album album = albumQueryService.albumById(albumId); + + if (!authorization.isAuthorized(principal, album)) { + throw new UserIsNotAuthorizedException(principal); + } + final String photoId = photoCommandService.uploadPhoto( principal.getUserId(), albumId, @@ -117,4 +129,33 @@ public Response uploadPhoto( return Response.created(linkScheme.toPhoto(albumId, photoId)).build(); } + + @DELETE + @Timed + public Response deleteAlbum( + @Auth Principal principal, + @PathParam("albumId") String albumId) { + + final Album album = albumQueryService.albumById(albumId); + + if (!authorization.isAuthorized(principal, album)) { + throw new UserIsNotAuthorizedException(principal); + } + + albumCommandService.deleteAlbum(albumId); + return Response.noContent().build(); + } + + @OPTIONS + @Timed + public Response preflight() { + return Response + .ok() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET,POST,DELETE") + .header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + .encoding("UTF-8") + .allow("GET", "POST", "DELETE") + .build(); + } } \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumsResource.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumsResource.java index 6e1aaa3..681ffec 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumsResource.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/AlbumsResource.java @@ -49,11 +49,6 @@ public AlbumsResource( public Response createAlbum( @NotNull CreateAlbumRepr createAlbumRepr, @Auth Principal principal) { - - /*if (createAlbumRepr == null) { - return Response.status(422).build(); - }*/ - final String albumId = commandService.createAlbum(principal.getUserId(), createAlbumRepr.getAlbumName()); return Response.created(linkScheme.toAlbum(albumId)).build(); } @@ -67,4 +62,17 @@ public Response listAlbums( final GalleryRepr galleryRepr = galleryConverter.convert(queryService.albumsByOwner(principal.getUserId())); return Response.ok(galleryRepr).build(); } + + @OPTIONS + @Timed + public Response preflight() { + return Response + .ok() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET,POST") + .header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + .encoding("UTF-8") + .allow("GET", "POST") + .build(); + } } \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/LinkScheme.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/LinkScheme.java index 4858efa..ff01277 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/LinkScheme.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/LinkScheme.java @@ -16,8 +16,6 @@ public class LinkScheme { private static final String THUMBNAIL_URI_TEMPLATE = "/albums/{albumId}/{photoId}/thumbnail"; - private static final String METADATA_URI_TEMPLATE = "/albums/{albumId}/{photoId}/metadata"; - public URI toGallery() { return UriBuilder .fromUri(GALLERY_URI_TEMPLATE) @@ -39,10 +37,6 @@ public URI toThumbnail(final String albumId, final String photoId) { return withAlbumAndPhoto(albumId, photoId, THUMBNAIL_URI_TEMPLATE); } - public URI toMetadata(final String albumId, final String photoId) { - return withAlbumAndPhoto(albumId, photoId, METADATA_URI_TEMPLATE); - } - private URI withAlbumAndPhoto(final String albumId, final String photoId, final String template) { return UriBuilder .fromUri(template) diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/MetadataResource.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/MetadataResource.java deleted file mode 100644 index 26d589f..0000000 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/MetadataResource.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.mgu.photoalbum.resource; - -import com.codahale.metrics.annotation.Timed; -import com.google.inject.Inject; -import com.mgu.photoalbum.converter.PhotoMetadataReprConverter; -import com.mgu.photoalbum.domain.Photo; -import com.mgu.photoalbum.representation.UpdateMetadataRepr; -import com.mgu.photoalbum.security.Authorization; -import com.mgu.photoalbum.security.Principal; -import com.mgu.photoalbum.security.UserIsNotAuthorizedException; -import com.mgu.photoalbum.service.PhotoCommandService; -import com.mgu.photoalbum.service.PhotoQueryService; -import io.dropwizard.auth.Auth; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.PUT; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -public class MetadataResource { - - private final PhotoCommandService commandService; - - private final PhotoQueryService queryService; - - private final Authorization authorization; - - private final PhotoMetadataReprConverter converter; - - @Inject - public MetadataResource( - final PhotoCommandService commandService, - final PhotoQueryService queryService, - final Authorization authorization, - final PhotoMetadataReprConverter converter) { - this.commandService = commandService; - this.queryService = queryService; - this.authorization = authorization; - this.converter = converter; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Timed - public Response viewMetadata( - @Auth Principal principal, - @PathParam("albumId") String albumId, - @PathParam("photoId") String photoId) { - - final Photo photo = queryService.photoById(photoId); - - if (!authorization.isAuthorized(principal, photo)) { - throw new UserIsNotAuthorizedException(principal); - } - - return Response.ok(converter.convert(photo)).build(); - } - - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Timed - public Response updateMetadata( - @Auth Principal principal, - @PathParam("albumId") String albumId, - @PathParam("photoId") String photoId, - UpdateMetadataRepr updateMetadataRepr) { - - final Photo photo = queryService.photoById(photoId); - - if (!authorization.isAuthorized(principal, photo)) { - throw new UserIsNotAuthorizedException(principal); - } - - commandService.updateMetadata(photoId, updateMetadataRepr.getDescription(), updateMetadataRepr.getTags()); - return Response.noContent().build(); - } -} \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/PhotoResource.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/PhotoResource.java index 0f8d35d..7953b99 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/PhotoResource.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/PhotoResource.java @@ -2,7 +2,10 @@ import com.codahale.metrics.annotation.Timed; import com.google.common.base.Optional; +import com.google.inject.Inject; +import com.mgu.photoalbum.converter.PhotoMetadataReprConverter; import com.mgu.photoalbum.domain.Photo; +import com.mgu.photoalbum.representation.UpdateMetadataRepr; import com.mgu.photoalbum.security.Authorization; import com.mgu.photoalbum.security.Principal; import com.mgu.photoalbum.security.UserIsNotAuthorizedException; @@ -10,12 +13,16 @@ import com.mgu.photoalbum.service.PhotoQueryService; import io.dropwizard.auth.Auth; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path("/albums/{albumId}/{photoId}") @@ -25,11 +32,19 @@ public class PhotoResource { private final PhotoQueryService queryService; + private final PhotoMetadataReprConverter converter; + private final Authorization authorization; - public PhotoResource(final PhotoCommandService commandService, final PhotoQueryService queryService, final Authorization authorization) { + @Inject + public PhotoResource( + final PhotoCommandService commandService, + final PhotoQueryService queryService, + final PhotoMetadataReprConverter converter, + final Authorization authorization) { this.commandService = commandService; this.queryService = queryService; + this.converter = converter; this.authorization = authorization; } @@ -65,6 +80,43 @@ public Response viewPhoto( } } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Timed + public Response viewMetadata( + @Auth Principal principal, + @PathParam("albumId") String albumId, + @PathParam("photoId") String photoId) { + + final Photo photo = queryService.photoById(photoId); + + if (!authorization.isAuthorized(principal, photo)) { + throw new UserIsNotAuthorizedException(principal); + } + + return Response.ok(converter.convert(photo)).build(); + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Timed + public Response updateMetadata( + @Auth Principal principal, + @PathParam("albumId") String albumId, + @PathParam("photoId") String photoId, + UpdateMetadataRepr updateMetadataRepr) { + + final Photo photo = queryService.photoById(photoId); + + if (!authorization.isAuthorized(principal, photo)) { + throw new UserIsNotAuthorizedException(principal); + } + + commandService.updateMetadata(photoId, updateMetadataRepr.getDescription(), updateMetadataRepr.getTags()); + return Response.noContent().build(); + } + @DELETE @Timed public Response deletePhoto( @@ -81,4 +133,17 @@ public Response deletePhoto( commandService.deletePhoto(photoId); return Response.noContent().build(); } + + @OPTIONS + @Timed + public Response preflight() { + return Response + .ok() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET,PUT,DELETE") + .header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + .encoding("UTF-8") + .allow("GET", "PUT", "DELETE") + .build(); + } } \ No newline at end of file diff --git a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/ThumbnailResource.java b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/ThumbnailResource.java index 76bbb60..a718e6e 100644 --- a/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/ThumbnailResource.java +++ b/photoalbum-webapp/src/main/java/com/mgu/photoalbum/resource/ThumbnailResource.java @@ -1,6 +1,7 @@ package com.mgu.photoalbum.resource; import com.codahale.metrics.annotation.Timed; +import com.google.inject.Inject; import com.mgu.photoalbum.domain.Photo; import com.mgu.photoalbum.security.Authorization; import com.mgu.photoalbum.security.Principal; @@ -9,6 +10,7 @@ import io.dropwizard.auth.Auth; import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -21,6 +23,7 @@ public class ThumbnailResource { private final Authorization authorization; + @Inject public ThumbnailResource(final PhotoQueryService photoQueryService, final Authorization authorization) { this.queryService = photoQueryService; this.authorization = authorization; @@ -43,4 +46,17 @@ public Response viewThumbnail( final byte[] thumbnailImage = queryService.thumbnailById(photoId); return Response.ok(thumbnailImage, "image/jpeg").header("Content-Length", String.valueOf(thumbnailImage.length)).build(); } + + @OPTIONS + @Timed + public Response preflight() { + return Response + .ok() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET") + .header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization") + .encoding("UTF-8") + .allow("GET") + .build(); + } } \ No newline at end of file diff --git a/photoalbum-webapp/src/main/resources/config.yml b/photoalbum-webapp/src/main/resources/config.yml index e351f19..9f2b9e3 100644 --- a/photoalbum-webapp/src/main/resources/config.yml +++ b/photoalbum-webapp/src/main/resources/config.yml @@ -33,6 +33,7 @@ logging: # sets the log level for our webapp loggers: com.cream: ${log.level} + org.eclipse.jetty.servlets: DEBUG # define log appenders for various log channels (console, file) appenders: # log to console diff --git a/photoalbum-webapp/src/test/java/com/mgu/photoalbum/resource/LinkSchemeTest.java b/photoalbum-webapp/src/test/java/com/mgu/photoalbum/resource/LinkSchemeTest.java index 6d620dd..d08fcfd 100644 --- a/photoalbum-webapp/src/test/java/com/mgu/photoalbum/resource/LinkSchemeTest.java +++ b/photoalbum-webapp/src/test/java/com/mgu/photoalbum/resource/LinkSchemeTest.java @@ -29,11 +29,6 @@ public void toThumbnailShouldYieldUrlWithCorrectlySubstitutedPathParameters() { assertThat(linkScheme.toThumbnail("Aa3b", "Ph0t0").toString(), is("/albums/Aa3b/Ph0t0/thumbnail")); } - @Test - public void toMetadataShouldYieldUrlWithCorrectlySubstitutedPathParameters() { - assertThat(linkScheme.toMetadata("Aa3b", "Ph0t0").toString(), is("/albums/Aa3b/Ph0t0/metadata")); - } - @Test public void toDownloadShouldYieldUrlWithCorrectlySubstitutedPathAndQueryParameters() { assertThat(linkScheme.toDownload("Aa3b", "Ph0t0").toString(), is("/albums/Aa3b/Ph0t0?download=true"));