Merge branch 'release-0.8.7'
[Sone.git] / src / main / java / net / pterodactylus / sone / data / Album.java
1 /*
2  * Sone - Album.java - Copyright © 2011–2013 David Roden
3  *
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.
8  *
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.
13  *
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/>.
16  */
17
18 package net.pterodactylus.sone.data;
19
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;
24 import static java.util.Collections.emptyList;
25
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.UUID;
33 import javax.annotation.Nonnull;
34
35 import com.google.common.base.Function;
36 import com.google.common.base.Optional;
37 import com.google.common.base.Predicate;
38 import com.google.common.base.Predicates;
39 import com.google.common.collect.Collections2;
40 import com.google.common.collect.FluentIterable;
41 import com.google.common.collect.ImmutableList;
42 import com.google.common.hash.Hasher;
43 import com.google.common.hash.Hashing;
44
45 /**
46  * Container for images that can also contain nested {@link Album}s.
47  *
48  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
49  */
50 public class Album implements Identified, Fingerprintable {
51
52         /** Compares two {@link Album}s by {@link #getTitle()}. */
53         public static final Comparator<Album> TITLE_COMPARATOR = new Comparator<Album>() {
54
55                 @Override
56                 public int compare(Album leftAlbum, Album rightAlbum) {
57                         return leftAlbum.getTitle().compareToIgnoreCase(rightAlbum.getTitle());
58                 }
59         };
60
61         /** Function that flattens the given album and all albums beneath it. */
62         public static final Function<Album, List<Album>> FLATTENER = new Function<Album, List<Album>>() {
63
64                 @Override
65                 @Nonnull
66                 public List<Album> apply(Album album) {
67                         if (album == null) {
68                                 return emptyList();
69                         }
70                         List<Album> albums = new ArrayList<Album>();
71                         albums.add(album);
72                         for (Album subAlbum : album.getAlbums()) {
73                                 albums.addAll(FluentIterable.from(ImmutableList.of(subAlbum)).transformAndConcat(FLATTENER).toList());
74                         }
75                         return albums;
76                 }
77         };
78
79         /** Function that transforms an album into the images it contains. */
80         public static final Function<Album, List<Image>> IMAGES = new Function<Album, List<Image>>() {
81
82                 @Override
83                 @Nonnull
84                 public List<Image> apply(Album album) {
85                         return (album != null) ? album.getImages() : Collections.<Image>emptyList();
86                 }
87         };
88
89         /**
90          * Filter that removes all albums that do not have any images in any album
91          * below it.
92          */
93         public static final Predicate<Album> NOT_EMPTY = new Predicate<Album>() {
94
95                 @Override
96                 public boolean apply(Album album) {
97                         /* so, we flatten all albums below the given one and check whether at least one album… */
98                         return FluentIterable.from(asList(album)).transformAndConcat(FLATTENER).anyMatch(new Predicate<Album>() {
99
100                                 @Override
101                                 public boolean apply(Album album) {
102                                         /* …contains any inserted images. */
103                                         return !album.getImages().isEmpty() && FluentIterable.from(album.getImages()).allMatch(new Predicate<Image>() {
104
105                                                 @Override
106                                                 public boolean apply(Image input) {
107                                                         return input.isInserted();
108                                                 }
109                                         });
110                                 }
111                         });
112                 }
113         };
114
115         /** The ID of this album. */
116         private final String id;
117
118         /** The Sone this album belongs to. */
119         private Sone sone;
120
121         /** Nested albums. */
122         private final List<Album> albums = new ArrayList<Album>();
123
124         /** The image IDs in order. */
125         private final List<String> imageIds = new ArrayList<String>();
126
127         /** The images in this album. */
128         private final Map<String, Image> images = new HashMap<String, Image>();
129
130         /** The parent album. */
131         private Album parent;
132
133         /** The title of this album. */
134         private String title;
135
136         /** The description of this album. */
137         private String description;
138
139         /** The ID of the album picture. */
140         private String albumImage;
141
142         /**
143          * Creates a new album with a random ID.
144          */
145         public Album() {
146                 this(UUID.randomUUID().toString());
147         }
148
149         /**
150          * Creates a new album with the given ID.
151          *
152          * @param id
153          *            The ID of the album
154          */
155         public Album(String id) {
156                 this.id = checkNotNull(id, "id must not be null");
157         }
158
159         //
160         // ACCESSORS
161         //
162
163         /**
164          * Returns the ID of this album.
165          *
166          * @return The ID of this album
167          */
168         public String getId() {
169                 return id;
170         }
171
172         /**
173          * Returns the Sone this album belongs to.
174          *
175          * @return The Sone this album belongs to
176          */
177         public Sone getSone() {
178                 return sone;
179         }
180
181         /**
182          * Sets the owner of the album. The owner can only be set as long as the
183          * current owner is {@code null}.
184          *
185          * @param sone
186          *            The album owner
187          * @return This album
188          */
189         public Album setSone(Sone sone) {
190                 checkNotNull(sone, "sone must not be null");
191                 checkState((this.sone == null) || (this.sone.equals(sone)), "album owner must not already be set to some other Sone");
192                 this.sone = sone;
193                 return this;
194         }
195
196         /**
197          * Returns the nested albums.
198          *
199          * @return The nested albums
200          */
201         public List<Album> getAlbums() {
202                 return new ArrayList<Album>(albums);
203         }
204
205         /**
206          * Adds an album to this album.
207          *
208          * @param album
209          *            The album to add
210          */
211         public void addAlbum(Album album) {
212                 checkNotNull(album, "album must not be null");
213                 checkArgument(album.getSone().equals(sone), "album must belong to the same Sone as this album");
214                 album.setParent(this);
215                 if (!albums.contains(album)) {
216                         albums.add(album);
217                 }
218         }
219
220         /**
221          * Removes an album from this album.
222          *
223          * @param album
224          *            The album to remove
225          */
226         public void removeAlbum(Album album) {
227                 checkNotNull(album, "album must not be null");
228                 checkArgument(album.sone.equals(sone), "album must belong this album’s Sone");
229                 checkArgument(equals(album.parent), "album must belong to this album");
230                 albums.remove(album);
231                 album.removeParent();
232         }
233
234         /**
235          * Moves the given album up in this album’s albums. If the album is already
236          * the first album, nothing happens.
237          *
238          * @param album
239          *            The album to move up
240          * @return The album that the given album swapped the place with, or
241          *         <code>null</code> if the album did not change its place
242          */
243         public Album moveAlbumUp(Album album) {
244                 checkNotNull(album, "album must not be null");
245                 checkArgument(album.sone.equals(sone), "album must belong to the same Sone as this album");
246                 checkArgument(equals(album.parent), "album must belong to this album");
247                 int oldIndex = albums.indexOf(album);
248                 if (oldIndex <= 0) {
249                         return null;
250                 }
251                 albums.remove(oldIndex);
252                 albums.add(oldIndex - 1, album);
253                 return albums.get(oldIndex);
254         }
255
256         /**
257          * Moves the given album down in this album’s albums. If the album is
258          * already the last album, nothing happens.
259          *
260          * @param album
261          *            The album to move down
262          * @return The album that the given album swapped the place with, or
263          *         <code>null</code> if the album did not change its place
264          */
265         public Album moveAlbumDown(Album album) {
266                 checkNotNull(album, "album must not be null");
267                 checkArgument(album.sone.equals(sone), "album must belong to the same Sone as this album");
268                 checkArgument(equals(album.parent), "album must belong to this album");
269                 int oldIndex = albums.indexOf(album);
270                 if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
271                         return null;
272                 }
273                 albums.remove(oldIndex);
274                 albums.add(oldIndex + 1, album);
275                 return albums.get(oldIndex);
276         }
277
278         /**
279          * Returns the images in this album.
280          *
281          * @return The images in this album
282          */
283         public List<Image> getImages() {
284                 return new ArrayList<Image>(Collections2.filter(Collections2.transform(imageIds, new Function<String, Image>() {
285
286                         @Override
287                         @SuppressWarnings("synthetic-access")
288                         public Image apply(String imageId) {
289                                 return images.get(imageId);
290                         }
291                 }), Predicates.notNull()));
292         }
293
294         /**
295          * Adds the given image to this album.
296          *
297          * @param image
298          *            The image to add
299          */
300         public void addImage(Image image) {
301                 checkNotNull(image, "image must not be null");
302                 checkNotNull(image.getSone(), "image must have an owner");
303                 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
304                 if (image.getAlbum() != null) {
305                         image.getAlbum().removeImage(image);
306                 }
307                 image.setAlbum(this);
308                 if (imageIds.isEmpty() && (albumImage == null)) {
309                         albumImage = image.getId();
310                 }
311                 if (!imageIds.contains(image.getId())) {
312                         imageIds.add(image.getId());
313                         images.put(image.getId(), image);
314                 }
315         }
316
317         /**
318          * Removes the given image from this album.
319          *
320          * @param image
321          *            The image to remove
322          */
323         public void removeImage(Image image) {
324                 checkNotNull(image, "image must not be null");
325                 checkNotNull(image.getSone(), "image must have an owner");
326                 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
327                 imageIds.remove(image.getId());
328                 images.remove(image.getId());
329                 if (image.getId().equals(albumImage)) {
330                         if (images.isEmpty()) {
331                                 albumImage = null;
332                         } else {
333                                 albumImage = images.values().iterator().next().getId();
334                         }
335                 }
336         }
337
338         /**
339          * Moves the given image up in this album’s images. If the image is already
340          * the first image, nothing happens.
341          *
342          * @param image
343          *            The image to move up
344          * @return The image that the given image swapped the place with, or
345          *         <code>null</code> if the image did not change its place
346          */
347         public Image moveImageUp(Image image) {
348                 checkNotNull(image, "image must not be null");
349                 checkNotNull(image.getSone(), "image must have an owner");
350                 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
351                 checkArgument(image.getAlbum().equals(this), "image must belong to this album");
352                 int oldIndex = imageIds.indexOf(image.getId());
353                 if (oldIndex <= 0) {
354                         return null;
355                 }
356                 imageIds.remove(image.getId());
357                 imageIds.add(oldIndex - 1, image.getId());
358                 return images.get(imageIds.get(oldIndex));
359         }
360
361         /**
362          * Moves the given image down in this album’s images. If the image is
363          * already the last image, nothing happens.
364          *
365          * @param image
366          *            The image to move down
367          * @return The image that the given image swapped the place with, or
368          *         <code>null</code> if the image did not change its place
369          */
370         public Image moveImageDown(Image image) {
371                 checkNotNull(image, "image must not be null");
372                 checkNotNull(image.getSone(), "image must have an owner");
373                 checkArgument(image.getSone().equals(sone), "image must belong to the same Sone as this album");
374                 checkArgument(image.getAlbum().equals(this), "image must belong to this album");
375                 int oldIndex = imageIds.indexOf(image.getId());
376                 if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) {
377                         return null;
378                 }
379                 imageIds.remove(image.getId());
380                 imageIds.add(oldIndex + 1, image.getId());
381                 return images.get(imageIds.get(oldIndex));
382         }
383
384         /**
385          * Returns the album image of this album, or {@code null} if no album image
386          * has been set.
387          *
388          * @return The image to show when this album is listed
389          */
390         public Image getAlbumImage() {
391                 if (albumImage == null) {
392                         return null;
393                 }
394                 return Optional.fromNullable(images.get(albumImage)).or(images.values().iterator().next());
395         }
396
397         /**
398          * Sets the ID of the album image.
399          *
400          * @param id
401          *            The ID of the album image
402          * @return This album
403          */
404         public Album setAlbumImage(String id) {
405                 this.albumImage = id;
406                 return this;
407         }
408
409         /**
410          * Returns whether this album contains any other albums or images.
411          *
412          * @return {@code true} if this album is empty, {@code false} otherwise
413          */
414         public boolean isEmpty() {
415                 return albums.isEmpty() && images.isEmpty();
416         }
417
418         /**
419          * Returns whether this album is an identitiy’s root album.
420          *
421          * @return {@code true} if this album is an identity’s root album, {@code
422          *         false} otherwise
423          */
424         public boolean isRoot() {
425                 return parent == null;
426         }
427
428         /**
429          * Returns the parent album of this album.
430          *
431          * @return The parent album of this album, or {@code null} if this album
432          *         does not have a parent
433          */
434         public Album getParent() {
435                 return parent;
436         }
437
438         /**
439          * Sets the parent album of this album.
440          *
441          * @param parent
442          *            The new parent album of this album
443          * @return This album
444          */
445         protected Album setParent(Album parent) {
446                 this.parent = checkNotNull(parent, "parent must not be null");
447                 return this;
448         }
449
450         /**
451          * Removes the parent album of this album.
452          *
453          * @return This album
454          */
455         protected Album removeParent() {
456                 this.parent = null;
457                 return this;
458         }
459
460         /**
461          * Returns the title of this album.
462          *
463          * @return The title of this album
464          */
465         public String getTitle() {
466                 return title;
467         }
468
469         /**
470          * Sets the title of this album.
471          *
472          * @param title
473          *            The title of this album
474          * @return This album
475          */
476         public Album setTitle(String title) {
477                 this.title = checkNotNull(title, "title must not be null");
478                 return this;
479         }
480
481         /**
482          * Returns the description of this album.
483          *
484          * @return The description of this album
485          */
486         public String getDescription() {
487                 return description;
488         }
489
490         /**
491          * Sets the description of this album.
492          *
493          * @param description
494          *            The description of this album
495          * @return This album
496          */
497         public Album setDescription(String description) {
498                 this.description = checkNotNull(description, "description must not be null");
499                 return this;
500         }
501
502         //
503         // FINGERPRINTABLE METHODS
504         //
505
506         /**
507          * {@inheritDoc}
508          */
509         @Override
510         public String getFingerprint() {
511                 Hasher hash = Hashing.sha256().newHasher();
512                 hash.putString("Album(");
513                 hash.putString("ID(").putString(id).putString(")");
514                 hash.putString("Title(").putString(title).putString(")");
515                 hash.putString("Description(").putString(description).putString(")");
516                 if (albumImage != null) {
517                         hash.putString("AlbumImage(").putString(albumImage).putString(")");
518                 }
519
520                 /* add nested albums. */
521                 hash.putString("Albums(");
522                 for (Album album : albums) {
523                         hash.putString(album.getFingerprint());
524                 }
525                 hash.putString(")");
526
527                 /* add images. */
528                 hash.putString("Images(");
529                 for (Image image : getImages()) {
530                         if (image.isInserted()) {
531                                 hash.putString(image.getFingerprint());
532                         }
533                 }
534                 hash.putString(")");
535
536                 hash.putString(")");
537                 return hash.hash().toString();
538         }
539
540         //
541         // OBJECT METHODS
542         //
543
544         /**
545          * {@inheritDoc}
546          */
547         @Override
548         public int hashCode() {
549                 return id.hashCode();
550         }
551
552         /**
553          * {@inheritDoc}
554          */
555         @Override
556         public boolean equals(Object object) {
557                 if (!(object instanceof Album)) {
558                         return false;
559                 }
560                 Album album = (Album) object;
561                 return id.equals(album.id);
562         }
563
564 }