2 * Sone - Album.java - Copyright © 2011–2013 David Roden
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package net.pterodactylus.sone.data;
20 import static com.google.common.base.Preconditions.checkArgument;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.base.Preconditions.checkState;
23 import static java.util.Arrays.asList;
25 import java.util.ArrayList;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.List;
30 import java.util.UUID;
32 import com.google.common.base.Function;
33 import com.google.common.base.Optional;
34 import com.google.common.base.Predicate;
35 import com.google.common.base.Predicates;
36 import com.google.common.collect.Collections2;
37 import com.google.common.collect.FluentIterable;
38 import com.google.common.collect.ImmutableList;
39 import com.google.common.hash.Hasher;
40 import com.google.common.hash.Hashing;
43 * Container for images that can also contain nested {@link Album}s.
45 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
47 public class Album implements Identified, Fingerprintable {
49 /** Compares two {@link Album}s by {@link #getTitle()}. */
50 public static final Comparator<Album> TITLE_COMPARATOR = new Comparator<Album>() {
53 public int compare(Album leftAlbum, Album rightAlbum) {
54 return leftAlbum.getTitle().compareToIgnoreCase(rightAlbum.getTitle());
58 /** Function that flattens the given album and all albums beneath it. */
59 public static final Function<Album, List<Album>> FLATTENER = new Function<Album, List<Album>>() {
62 public List<Album> apply(Album album) {
63 List<Album> albums = new ArrayList<Album>();
65 for (Album subAlbum : album.getAlbums()) {
66 albums.addAll(FluentIterable.from(ImmutableList.of(subAlbum)).transformAndConcat(FLATTENER).toList());
72 /** Function that transforms an album into the images it contains. */
73 public static final Function<Album, List<Image>> IMAGES = new Function<Album, List<Image>>() {
76 public List<Image> apply(Album album) {
77 return album.getImages();
82 * Filter that removes all albums that do not have any images in any album
85 public static final Predicate<Album> NOT_EMPTY = new Predicate<Album>() {
88 public boolean apply(Album album) {
89 /* so, we flatten all albums below the given one and check whether at least one album… */
90 return FluentIterable.from(asList(album)).transformAndConcat(FLATTENER).anyMatch(new Predicate<Album>() {
93 public boolean apply(Album album) {
94 /* …contains any inserted images. */
95 return !album.getImages().isEmpty() && FluentIterable.from(album.getImages()).allMatch(new Predicate<Image>() {
98 public boolean apply(Image input) {
99 return input.isInserted();
107 /** The ID of this album. */
108 private final String id;
110 /** The Sone this album belongs to. */
113 /** Nested albums. */
114 private final List<Album> albums = new ArrayList<Album>();
116 /** The image IDs in order. */
117 private final List<String> imageIds = new ArrayList<String>();
119 /** The images in this album. */
120 private final Map<String, Image> images = new HashMap<String, Image>();
122 /** The parent album. */
123 private Album parent;
125 /** The title of this album. */
126 private String title;
128 /** The description of this album. */
129 private String description;
131 /** The ID of the album picture. */
132 private String albumImage;
135 * Creates a new album with a random ID.
138 this(UUID.randomUUID().toString());
142 * Creates a new album with the given ID.
145 * The ID of the album
147 public Album(String id) {
148 this.id = checkNotNull(id, "id must not be null");
156 * Returns the ID of this album.
158 * @return The ID of this album
160 public String getId() {
165 * Returns the Sone this album belongs to.
167 * @return The Sone this album belongs to
169 public Sone getSone() {
174 * Sets the owner of the album. The owner can only be set as long as the
175 * current owner is {@code null}.
181 public Album setSone(Sone sone) {
182 checkNotNull(sone, "sone must not be null");
183 checkState((this.sone == null) || (this.sone.equals(sone)), "album owner must not already be set to some other Sone");
189 * Returns the nested albums.
191 * @return The nested albums
193 public List<Album> getAlbums() {
194 return new ArrayList<Album>(albums);
198 * Adds an album to this album.
203 public void addAlbum(Album album) {
204 checkNotNull(album, "album must not be null");
205 checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album");
206 album.setParent(this);
207 if (!albums.contains(album)) {
213 * Removes an album from this album.
216 * The album to remove
218 public void removeAlbum(Album album) {
219 checkNotNull(album, "album must not be null");
220 checkArgument(album.sone.equals(sone), "album must belong this album’s Sone");
221 checkArgument(equals(album.parent), "album must belong to this album");
222 albums.remove(album);
223 album.removeParent();
227 * Moves the given album up in this album’s albums. If the album is already
228 * the first album, nothing happens.
231 * The album to move up
232 * @return The album that the given album swapped the place with, or
233 * <code>null</code> if the album did not change its place
235 public Album moveAlbumUp(Album album) {
236 checkNotNull(album, "album must not be null");
237 checkArgument(album.sone.equals(sone), "album must belong to the same Sone as this album");
238 checkArgument(equals(album.parent), "album must belong to this album");
239 int oldIndex = albums.indexOf(album);
243 albums.remove(oldIndex);
244 albums.add(oldIndex - 1, album);
245 return albums.get(oldIndex);
249 * Moves the given album down in this album’s albums. If the album is
250 * already the last album, nothing happens.
253 * The album to move down
254 * @return The album that the given album swapped the place with, or
255 * <code>null</code> if the album did not change its place
257 public Album moveAlbumDown(Album album) {
258 checkNotNull(album, "album must not be null");
259 checkArgument(album.sone.equals(sone), "album must belong to the same Sone as this album");
260 checkArgument(equals(album.parent), "album must belong to this album");
261 int oldIndex = albums.indexOf(album);
262 if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
265 albums.remove(oldIndex);
266 albums.add(oldIndex + 1, album);
267 return albums.get(oldIndex);
271 * Returns the images in this album.
273 * @return The images in this album
275 public List<Image> getImages() {
276 return new ArrayList<Image>(Collections2.filter(Collections2.transform(imageIds, new Function<String, Image>() {
279 @SuppressWarnings("synthetic-access")
280 public Image apply(String imageId) {
281 return images.get(imageId);
283 }), Predicates.notNull()));
287 * Adds the given image to this album.
292 public void addImage(Image image) {
293 checkNotNull(image, "image must not be null");
294 checkNotNull(image.getSone(), "image must have an owner");
295 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
296 if (image.getAlbum() != null) {
297 image.getAlbum().removeImage(image);
299 image.setAlbum(this);
300 if (imageIds.isEmpty() && (albumImage == null)) {
301 albumImage = image.getId();
303 if (!imageIds.contains(image.getId())) {
304 imageIds.add(image.getId());
305 images.put(image.getId(), image);
310 * Removes the given image from this album.
313 * The image to remove
315 public void removeImage(Image image) {
316 checkNotNull(image, "image must not be null");
317 checkNotNull(image.getSone(), "image must have an owner");
318 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
319 imageIds.remove(image.getId());
320 images.remove(image.getId());
321 if (image.getId().equals(albumImage)) {
322 if (images.isEmpty()) {
325 albumImage = images.values().iterator().next().getId();
331 * Moves the given image up in this album’s images. If the image is already
332 * the first image, nothing happens.
335 * The image to move up
336 * @return The image that the given image swapped the place with, or
337 * <code>null</code> if the image did not change its place
339 public Image moveImageUp(Image image) {
340 checkNotNull(image, "image must not be null");
341 checkNotNull(image.getSone(), "image must have an owner");
342 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
343 checkArgument(image.getAlbum().equals(this), "image must belong to this album");
344 int oldIndex = imageIds.indexOf(image.getId());
348 imageIds.remove(image.getId());
349 imageIds.add(oldIndex - 1, image.getId());
350 return images.get(imageIds.get(oldIndex));
354 * Moves the given image down in this album’s images. If the image is
355 * already the last image, nothing happens.
358 * The image to move down
359 * @return The image that the given image swapped the place with, or
360 * <code>null</code> if the image did not change its place
362 public Image moveImageDown(Image image) {
363 checkNotNull(image, "image must not be null");
364 checkNotNull(image.getSone(), "image must have an owner");
365 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
366 checkArgument(image.getAlbum().equals(this), "image must belong to this album");
367 int oldIndex = imageIds.indexOf(image.getId());
368 if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) {
371 imageIds.remove(image.getId());
372 imageIds.add(oldIndex + 1, image.getId());
373 return images.get(imageIds.get(oldIndex));
377 * Returns the album image of this album, or {@code null} if no album image
380 * @return The image to show when this album is listed
382 public Image getAlbumImage() {
383 if (albumImage == null) {
386 return Optional.fromNullable(images.get(albumImage)).or(images.values().iterator().next());
390 * Sets the ID of the album image.
393 * The ID of the album image
396 public Album setAlbumImage(String id) {
397 this.albumImage = id;
402 * Returns whether this album contains any other albums or images.
404 * @return {@code true} if this album is empty, {@code false} otherwise
406 public boolean isEmpty() {
407 return albums.isEmpty() && images.isEmpty();
411 * Returns whether this album is an identitiy’s root album.
413 * @return {@code true} if this album is an identity’s root album, {@code
416 public boolean isRoot() {
417 return parent == null;
421 * Returns the parent album of this album.
423 * @return The parent album of this album, or {@code null} if this album
424 * does not have a parent
426 public Album getParent() {
431 * Sets the parent album of this album.
434 * The new parent album of this album
437 protected Album setParent(Album parent) {
438 this.parent = checkNotNull(parent, "parent must not be null");
443 * Removes the parent album of this album.
447 protected Album removeParent() {
453 * Returns the title of this album.
455 * @return The title of this album
457 public String getTitle() {
462 * Sets the title of this album.
465 * The title of this album
468 public Album setTitle(String title) {
469 this.title = checkNotNull(title, "title must not be null");
474 * Returns the description of this album.
476 * @return The description of this album
478 public String getDescription() {
483 * Sets the description of this album.
486 * The description of this album
489 public Album setDescription(String description) {
490 this.description = checkNotNull(description, "description must not be null");
495 // FINGERPRINTABLE METHODS
502 public String getFingerprint() {
503 Hasher hash = Hashing.sha256().newHasher();
504 hash.putString("Album(");
505 hash.putString("ID(").putString(id).putString(")");
506 hash.putString("Title(").putString(title).putString(")");
507 hash.putString("Description(").putString(description).putString(")");
508 if (albumImage != null) {
509 hash.putString("AlbumImage(").putString(albumImage).putString(")");
512 /* add nested albums. */
513 hash.putString("Albums(");
514 for (Album album : albums) {
515 hash.putString(album.getFingerprint());
520 hash.putString("Images(");
521 for (Image image : getImages()) {
522 if (image.isInserted()) {
523 hash.putString(image.getFingerprint());
529 return hash.hash().toString();
540 public int hashCode() {
541 return id.hashCode();
548 public boolean equals(Object object) {
549 if (!(object instanceof Album)) {
552 Album album = (Album) object;
553 return id.equals(album.id);