8238b3fc3391acc55ae1ff62fa8fdc633957d881
[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
25 import java.util.ArrayList;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.UUID;
31
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;
41
42 /**
43  * Container for images that can also contain nested {@link Album}s.
44  *
45  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
46  */
47 public class Album implements Identified, Fingerprintable {
48
49         /** Compares two {@link Album}s by {@link #getTitle()}. */
50         public static final Comparator<Album> TITLE_COMPARATOR = new Comparator<Album>() {
51
52                 @Override
53                 public int compare(Album leftAlbum, Album rightAlbum) {
54                         return leftAlbum.getTitle().compareToIgnoreCase(rightAlbum.getTitle());
55                 }
56         };
57
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>>() {
60
61                 @Override
62                 public List<Album> apply(Album album) {
63                         List<Album> albums = new ArrayList<Album>();
64                         albums.add(album);
65                         for (Album subAlbum : album.getAlbums()) {
66                                 albums.addAll(FluentIterable.from(ImmutableList.of(subAlbum)).transformAndConcat(FLATTENER).toList());
67                         }
68                         return albums;
69                 }
70         };
71
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>>() {
74
75                 @Override
76                 public List<Image> apply(Album album) {
77                         return album.getImages();
78                 }
79         };
80
81         /**
82          * Filter that removes all albums that do not have any images in any album
83          * below it.
84          */
85         public static final Predicate<Album> NOT_EMPTY = new Predicate<Album>() {
86
87                 @Override
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>() {
91
92                                 @Override
93                                 public boolean apply(Album album) {
94                                         /* …contains any inserted images. */
95                                         return !album.getImages().isEmpty() && FluentIterable.from(album.getImages()).allMatch(new Predicate<Image>() {
96
97                                                 @Override
98                                                 public boolean apply(Image input) {
99                                                         return input.isInserted();
100                                                 }
101                                         });
102                                 }
103                         });
104                 }
105         };
106
107         /** The ID of this album. */
108         private final String id;
109
110         /** The Sone this album belongs to. */
111         private Sone sone;
112
113         /** Nested albums. */
114         private final List<Album> albums = new ArrayList<Album>();
115
116         /** The image IDs in order. */
117         private final List<String> imageIds = new ArrayList<String>();
118
119         /** The images in this album. */
120         private final Map<String, Image> images = new HashMap<String, Image>();
121
122         /** The parent album. */
123         private Album parent;
124
125         /** The title of this album. */
126         private String title;
127
128         /** The description of this album. */
129         private String description;
130
131         /** The ID of the album picture. */
132         private String albumImage;
133
134         /**
135          * Creates a new album with a random ID.
136          */
137         public Album() {
138                 this(UUID.randomUUID().toString());
139         }
140
141         /**
142          * Creates a new album with the given ID.
143          *
144          * @param id
145          *            The ID of the album
146          */
147         public Album(String id) {
148                 this.id = checkNotNull(id, "id must not be null");
149         }
150
151         //
152         // ACCESSORS
153         //
154
155         /**
156          * Returns the ID of this album.
157          *
158          * @return The ID of this album
159          */
160         public String getId() {
161                 return id;
162         }
163
164         /**
165          * Returns the Sone this album belongs to.
166          *
167          * @return The Sone this album belongs to
168          */
169         public Sone getSone() {
170                 return sone;
171         }
172
173         /**
174          * Sets the owner of the album. The owner can only be set as long as the
175          * current owner is {@code null}.
176          *
177          * @param sone
178          *            The album owner
179          * @return This album
180          */
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");
184                 this.sone = sone;
185                 return this;
186         }
187
188         /**
189          * Returns the nested albums.
190          *
191          * @return The nested albums
192          */
193         public List<Album> getAlbums() {
194                 return new ArrayList<Album>(albums);
195         }
196
197         /**
198          * Adds an album to this album.
199          *
200          * @param album
201          *            The album to add
202          */
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)) {
208                         albums.add(album);
209                 }
210         }
211
212         /**
213          * Removes an album from this album.
214          *
215          * @param album
216          *            The album to remove
217          */
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();
224         }
225
226         /**
227          * Moves the given album up in this album’s albums. If the album is already
228          * the first album, nothing happens.
229          *
230          * @param album
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
234          */
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);
240                 if (oldIndex <= 0) {
241                         return null;
242                 }
243                 albums.remove(oldIndex);
244                 albums.add(oldIndex - 1, album);
245                 return albums.get(oldIndex);
246         }
247
248         /**
249          * Moves the given album down in this album’s albums. If the album is
250          * already the last album, nothing happens.
251          *
252          * @param album
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
256          */
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))) {
263                         return null;
264                 }
265                 albums.remove(oldIndex);
266                 albums.add(oldIndex + 1, album);
267                 return albums.get(oldIndex);
268         }
269
270         /**
271          * Returns the images in this album.
272          *
273          * @return The images in this album
274          */
275         public List<Image> getImages() {
276                 return new ArrayList<Image>(Collections2.filter(Collections2.transform(imageIds, new Function<String, Image>() {
277
278                         @Override
279                         @SuppressWarnings("synthetic-access")
280                         public Image apply(String imageId) {
281                                 return images.get(imageId);
282                         }
283                 }), Predicates.notNull()));
284         }
285
286         /**
287          * Adds the given image to this album.
288          *
289          * @param image
290          *            The image to add
291          */
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);
298                 }
299                 image.setAlbum(this);
300                 if (imageIds.isEmpty() && (albumImage == null)) {
301                         albumImage = image.getId();
302                 }
303                 if (!imageIds.contains(image.getId())) {
304                         imageIds.add(image.getId());
305                         images.put(image.getId(), image);
306                 }
307         }
308
309         /**
310          * Removes the given image from this album.
311          *
312          * @param image
313          *            The image to remove
314          */
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()) {
323                                 albumImage = null;
324                         } else {
325                                 albumImage = images.values().iterator().next().getId();
326                         }
327                 }
328         }
329
330         /**
331          * Moves the given image up in this album’s images. If the image is already
332          * the first image, nothing happens.
333          *
334          * @param image
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
338          */
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());
345                 if (oldIndex <= 0) {
346                         return null;
347                 }
348                 imageIds.remove(image.getId());
349                 imageIds.add(oldIndex - 1, image.getId());
350                 return images.get(imageIds.get(oldIndex));
351         }
352
353         /**
354          * Moves the given image down in this album’s images. If the image is
355          * already the last image, nothing happens.
356          *
357          * @param image
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
361          */
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))) {
369                         return null;
370                 }
371                 imageIds.remove(image.getId());
372                 imageIds.add(oldIndex + 1, image.getId());
373                 return images.get(imageIds.get(oldIndex));
374         }
375
376         /**
377          * Returns the album image of this album, or {@code null} if no album image
378          * has been set.
379          *
380          * @return The image to show when this album is listed
381          */
382         public Image getAlbumImage() {
383                 if (albumImage == null) {
384                         return null;
385                 }
386                 return Optional.fromNullable(images.get(albumImage)).or(images.values().iterator().next());
387         }
388
389         /**
390          * Sets the ID of the album image.
391          *
392          * @param id
393          *            The ID of the album image
394          * @return This album
395          */
396         public Album setAlbumImage(String id) {
397                 this.albumImage = id;
398                 return this;
399         }
400
401         /**
402          * Returns whether this album contains any other albums or images.
403          *
404          * @return {@code true} if this album is empty, {@code false} otherwise
405          */
406         public boolean isEmpty() {
407                 return albums.isEmpty() && images.isEmpty();
408         }
409
410         /**
411          * Returns whether this album is an identitiy’s root album.
412          *
413          * @return {@code true} if this album is an identity’s root album, {@code
414          *         false} otherwise
415          */
416         public boolean isRoot() {
417                 return parent == null;
418         }
419
420         /**
421          * Returns the parent album of this album.
422          *
423          * @return The parent album of this album, or {@code null} if this album
424          *         does not have a parent
425          */
426         public Album getParent() {
427                 return parent;
428         }
429
430         /**
431          * Sets the parent album of this album.
432          *
433          * @param parent
434          *            The new parent album of this album
435          * @return This album
436          */
437         protected Album setParent(Album parent) {
438                 this.parent = checkNotNull(parent, "parent must not be null");
439                 return this;
440         }
441
442         /**
443          * Removes the parent album of this album.
444          *
445          * @return This album
446          */
447         protected Album removeParent() {
448                 this.parent = null;
449                 return this;
450         }
451
452         /**
453          * Returns the title of this album.
454          *
455          * @return The title of this album
456          */
457         public String getTitle() {
458                 return title;
459         }
460
461         /**
462          * Sets the title of this album.
463          *
464          * @param title
465          *            The title of this album
466          * @return This album
467          */
468         public Album setTitle(String title) {
469                 this.title = checkNotNull(title, "title must not be null");
470                 return this;
471         }
472
473         /**
474          * Returns the description of this album.
475          *
476          * @return The description of this album
477          */
478         public String getDescription() {
479                 return description;
480         }
481
482         /**
483          * Sets the description of this album.
484          *
485          * @param description
486          *            The description of this album
487          * @return This album
488          */
489         public Album setDescription(String description) {
490                 this.description = checkNotNull(description, "description must not be null");
491                 return this;
492         }
493
494         //
495         // FINGERPRINTABLE METHODS
496         //
497
498         /**
499          * {@inheritDoc}
500          */
501         @Override
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(")");
510                 }
511
512                 /* add nested albums. */
513                 hash.putString("Albums(");
514                 for (Album album : albums) {
515                         hash.putString(album.getFingerprint());
516                 }
517                 hash.putString(")");
518
519                 /* add images. */
520                 hash.putString("Images(");
521                 for (Image image : getImages()) {
522                         if (image.isInserted()) {
523                                 hash.putString(image.getFingerprint());
524                         }
525                 }
526                 hash.putString(")");
527
528                 hash.putString(")");
529                 return hash.hash().toString();
530         }
531
532         //
533         // OBJECT METHODS
534         //
535
536         /**
537          * {@inheritDoc}
538          */
539         @Override
540         public int hashCode() {
541                 return id.hashCode();
542         }
543
544         /**
545          * {@inheritDoc}
546          */
547         @Override
548         public boolean equals(Object object) {
549                 if (!(object instanceof Album)) {
550                         return false;
551                 }
552                 Album album = (Album) object;
553                 return id.equals(album.id);
554         }
555
556 }