Magnolia 5.7 reached extended end of life on May 31, 2022. Support for this branch is limited, see End-of-life policy. Please note that to cover the extra maintenance effort, this EEoL period is a paid extension in the life of the branch. Customers who opt for the extended maintenance will need a new license key to run future versions of Magnolia 5.7. If you have any questions or to subscribe to the extended maintenance, please get in touch with your local contact at Magnolia.
The flickr-browser module is an example of how to build a custom content app. The module installs an app that allows you to browse photos and albums on Flickr. We use a POJO as ItemId, which is more versatile than the String used in the simpler example. Using an object as ItemId has the advantage that we can tell the difference between Flickr photos and albums. We also make fewer Flickr API calls which makes the app perform better. In this example the tree view and the thumbnail view use different Containers.
On this example we will implement the following custom classes:
ItemId
Item
ContentConnector
Tree Container
ImageProvider
Presenter
We also add two views to the workbench:
Tree view
Thumbnail view
Using a POJO as ItemId makes hierarchy possible
The module needs a tree view to show both albums and photos. We need an ItemId that carries sufficient information about its corresponding Item. The ItemId should know things such as:
isForAlbum: Is the item an album or a photo?
parentId: Parent album of a photo. A photo can be in more than one album. However, within the tree container it should know its parent.
photoId: Photo ID. An album also carries information about a photo. Each album has a cover photo. The image provider should display the cover photo when the album is selected in the container.
albumId: Album ID. If the item is a photo it carries the albumId of its parent album.
uuid: A String representation of the ItemId to use for URL fragments. The UUID should carry sufficient information to instantiate the ItemId again by the given UUID.
The implementation FlickrItemIdImpl has different constructors to build instances by:
Photoset (album)
Photo
Photo and parent ItemId
URL fragment
Creating an Item
Interface
Similar to SimpleFlickrItem in the simple example, FlickrItem extends BasicFlickrItem but it has additional properties.
snippet of info.magnolia.flickr.browser.app.item.FlickrItemExpand source
public interface FlickrItem extends BasicFlickrItem {
public static String PROPERTY_UUID = "uuid";
public static String PROPERTY_NUMBEROFPHOTOS = "numberOfPhotos";
public static String PROPERTY_FLICKR_ITEM_TYPE = "flickrItemType";
public static String FLICKRITEMTYPE_ALBUM = "album";
public static String FLICKRITEMTYPE_PHOTO = "photo";
public String getUuid();
public class IDs {
private static Map<String, Class> properties = BasicFlickrItem.IDs.getIDs();
static {
properties.put(PROPERTY_FLICKR_ITEM_TYPE, String.class);
properties.put(PROPERTY_NUMBEROFPHOTOS, Integer.class);
properties.put(PROPERTY_UUID, String.class);
}
public static Map<String, Class> getIDs() {
return properties;
}
}
}
Property IDs:
uuid: A String representation of the ItemId to use for URL fragments. The same as used in the FlickrItemId. This will be helpful when an ItemId must be constructed by a given item.
numberOfPhotos: The number of photos in an album.
flickrItemType: Whether the item has been derived from a photo or from an album.
In addition, we inherit property IDs title, description and photoId from BasicFlickrItem.
Similar to the SimpleFlickrItem interface, the FlickrItem interface created here also has a static class that provides a map of all the properties and their types:
public class FlickrPropertysetItem extends PropertysetItem implements FlickrItem {
protected FlickrPropertysetItem(Photo photo, String uuid){
String photoId = photo.getId();
String title = StringUtils.isNotBlank(photo.getTitle()) ? photo.getTitle() : photoId;
String description = StringUtils.isNotBlank(photo.getDescription()) ? photo.getDescription() : "";
addItemProperty(PROPERTY_UUID, new ObjectProperty(uuid));
addItemProperty(PROPERTY_PHOTOID, new ObjectProperty(photoId));
addItemProperty(PROPERTY_FLICKR_ITEM_TYPE, new ObjectProperty(FLICKRITEMTYPE_PHOTO));
addItemProperty(PROPERTY_TITLE, new ObjectProperty(title));
addItemProperty(PROPERTY_DESCRIPTION, new ObjectProperty(description));
}
protected FlickrPropertysetItem(Photo photo){
String photoId = photo.getId();
String title = StringUtils.isNotBlank(photo.getTitle()) ? photo.getTitle() : photoId;
String description = StringUtils.isNotBlank(photo.getDescription()) ? photo.getDescription() : "";
addItemProperty(PROPERTY_PHOTOID, new ObjectProperty(photoId));
addItemProperty(PROPERTY_FLICKR_ITEM_TYPE, new ObjectProperty(FLICKRITEMTYPE_PHOTO));
addItemProperty(PROPERTY_TITLE, new ObjectProperty(title));
addItemProperty(PROPERTY_DESCRIPTION, new ObjectProperty(description));
}
protected FlickrPropertysetItem(Photoset photoset){
String photoId = photoset.getPrimaryPhoto().getId();
String uuid = FlickrItemId.UuidParser.createUuid(photoset);
String title = StringUtils.isNotBlank(photoset.getTitle()) ? photoset.getTitle() : uuid;
String description = StringUtils.isNotBlank(photoset.getDescription()) ? photoset.getDescription() : "";
addItemProperty(PROPERTY_UUID, new ObjectProperty(uuid));
addItemProperty(PROPERTY_PHOTOID, new ObjectProperty(photoId));
addItemProperty(PROPERTY_FLICKR_ITEM_TYPE, new ObjectProperty(FLICKRITEMTYPE_ALBUM));
addItemProperty(PROPERTY_TITLE, new ObjectProperty(title));
addItemProperty(PROPERTY_DESCRIPTION, new ObjectProperty(description));
addItemProperty(PROPERTY_NUMBEROFPHOTOS, new ObjectProperty(new Integer(photoset.getPhotoCount())));
}
public String getUuid() {
return (String) getItemProperty(PROPERTY_UUID).getValue();
}
}
Item utility
We use two different containers for the tree and thumbnail views. For this reason we cannot couple the container and the content connector as we did in SimpleFlickrFlatContainer. We need methods to fetch an Item by ItemId and vice versa, in both the container and the content connector. Here we implement a utility class which provides methods to use in both.
In this example we implement a lazy loading tree container. At first, the view should render only albums as top level items. When a user clicks an album the view expands the subtree and loads the photos of the album. To achieve this, our custom container implements Collapsible and Container.Indexed as well as Container, Container.Hierarchical and Container.Ordered which are superinterfaces. We also implement Refreshable.
public class FlickrCustomTreeContainer extends AbstractContainer implements Refreshable, Collapsible, Container.Indexed {
private static Logger logger = LoggerFactory.getLogger(FlickrCustomTreeContainer.class);
private boolean RESTRICT = false;
private int RESTRICT_TO = 3;
private final FlickrService flickrService;
private Map<Object, Class> properties = new HashMap<Object, Class>();
private LinkedList<FlickrItemId> albumIds = null;
private Map<FlickrItemId, FlickrItem> all = new HashMap<FlickrItemId, FlickrItem>();
private Map<FlickrItemId, LinkedList<FlickrItemId>> childrenIDsMap = new HashMap<FlickrItemId, LinkedList<FlickrItemId>>();
private Set<FlickrItemId> openFolders = new HashSet<FlickrItemId>();
private List<FlickrItemId> preorderItemKeys = null;
public FlickrCustomTreeContainer(FlickrService flickrService) {
this.flickrService = flickrService;
configure();
}
public void refresh() {
removeAllItems();
}
public Collection<?> getChildren(Object itemId) {
if (itemId instanceof FlickrItemId) {
if (!((FlickrItemId) itemId).isForAlbum()) {
return new LinkedList<FlickrItemId>();
} else {
LinkedList<FlickrItemId> children = childrenIDsMap.get(itemId);
if (children == null) {
children = loadAlbumPhotos((FlickrItemId) itemId);
}
return children;
}
}
return new LinkedList<FlickrItemId>();
}
public Object getParent(Object itemId) {
return itemId != null && itemId instanceof FlickrItemId ? ((FlickrItemId) itemId).getParent() : null;
}
public Collection<?> rootItemIds() {
if (albumIds == null) {
albumIds = loadAccountAlbums();
}
return albumIds;
}
public boolean areChildrenAllowed(Object itemId) {
if (!(itemId instanceof FlickrItemId)) {
return false;
} else {
return ((FlickrItemId) itemId).isForAlbum();
}
}
public boolean isRoot(Object itemId) {
return albumIds.contains(itemId);
}
public boolean hasChildren(Object itemId) {
return isRoot(itemId);
}
public FlickrItemId nextItemId(Object itemId) {
int indexOf = getPreorder().indexOf(itemId) + 1;
if (indexOf == size()) {
return null;
}
return getPreorder().get(indexOf);
}
public FlickrItemId prevItemId(Object itemId) {
int indexOf = getPreorder().indexOf(itemId) - 1;
if (indexOf < 0) {
return null;
}
return getPreorder().get(indexOf);
}
public Object firstItemId() {
FlickrItemId first = null;
if (albumIds != null) {
first = albumIds.getFirst();
}
return first;
}
public FlickrItemId lastItemId() {
return getPreorder().get(size() - 1);
}
public boolean isFirstId(Object itemId) {
return itemId.equals(firstItemId());
}
public boolean isLastId(Object itemId) {
return itemId.equals(lastItemId());
}
public Item getItem(Object itemId) {
return itemId instanceof FlickrItemId ? all.get(itemId) : null;
}
public Collection<?> getContainerPropertyIds() {
return FlickrItem.IDs.getIDs().keySet();
}
public Class<?> getType(Object propertyId) {
return FlickrItem.IDs.getIDs().get(propertyId);
}
public Collection<?> getItemIds() {
return Collections.unmodifiableCollection(rootItemIds());
}
public Property getContainerProperty(Object itemId, Object propertyId) {
Item item = getItem(itemId);
if (item != null) {
Property property = item.getItemProperty(propertyId);
if (property != null) {
return property;
}
}
return null;
}
public int size() {
return getPreorder().size();
}
public boolean containsId(Object itemId) {
return all.containsKey(itemId);
}
//--------------------------------------------------------------------------------------------------
// Collapsible
//
public void setCollapsed(Object itemId, boolean collapsed) {
if (collapsed) {
openFolders.remove(itemId);
} else {
openFolders.add((FlickrItemId) itemId);
}
preorderItemKeys = null;
}
public boolean isCollapsed(Object itemId) {
return !openFolders.contains(itemId);
}
// --------------------------------------------------------------------------------------------------
// Container.Indexed
//
public int indexOfId(Object itemId) {
return getPreorder().indexOf(itemId);
}
public FlickrItemId getIdByIndex(int index) {
return getPreorder().get(index);
}
public List<FlickrItemId> getItemIds(int startIndex, int numberOfItems) {
ArrayList<FlickrItemId> rangeOfIds = new ArrayList<FlickrItemId>();
int endIndex = startIndex + numberOfItems;
if (endIndex > size()) {
endIndex = size();
}
for (int i = startIndex; i < endIndex; i++) {
FlickrItemId idByIndex = getIdByIndex(i);
rangeOfIds.add(idByIndex);
}
return Collections.unmodifiableList(rangeOfIds);
}
public Object addItemAt(int index) throws UnsupportedOperationException {
throw new UnsupportedOperationException("addItemAt is not supported!");
}
public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
throw new UnsupportedOperationException("addItemAt is not supported!");
}
public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException {
properties.put(propertyId, type);
return properties.containsKey(propertyId);
}
public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException {
properties.remove(propertyId);
return !properties.containsKey(propertyId);
}
public boolean removeAllItems() throws UnsupportedOperationException {
all.clear();
childrenIDsMap.clear();
albumIds = null;
openFolders.clear();
preorderItemKeys = null;
return all.size() == 0 && childrenIDsMap.size() == 0 && albumIds == null && openFolders.size() == 0 && preorderItemKeys == null;
}
private List<FlickrItemId> getPreorder() {
if (preorderItemKeys == null) {
preorderItemKeys = new ArrayList<FlickrItemId>();
for (Object root : rootItemIds()) {
preorderItemKeys.add((FlickrItemId) root);
loadVisibleSubtree((FlickrItemId) root);
}
}
return preorderItemKeys;
}
private void loadVisibleSubtree(FlickrItemId root) {
if (!isCollapsed(root)) {
// unsave cast, but it's okay here
Iterator<FlickrItemId> iterator = (Iterator<FlickrItemId>) getChildren(root).iterator();
while (iterator.hasNext()) {
FlickrItemId child = iterator.next();
preorderItemKeys.add(child);
// make recursive for deeper tree
}
}
}
private LinkedList<FlickrItemId> loadAccountAlbums() {
Photosets photosets = flickrService.getPhotosets();
Collection<Photoset> photosetCollection = photosets.getPhotosets();
albumIds = new LinkedList<FlickrItemId>();
logger.info("Loading photosets meta-data.");
int i = 0;
for (Photoset ps : photosetCollection) {
if (ps.getPhotoCount() > 0) {
FlickrItemId itemId = FlickrItemUtil.createIdByPhotoset(ps);
FlickrItem item = FlickrItemUtil.createItemByPhotoSet(ps);
albumIds.add(itemId);
all.put(itemId, item);
i++;
}
if (RESTRICT && i == RESTRICT_TO) {
break;
}
}
logger.info("Loaded {} photosets.", i);
return albumIds;
}
private LinkedList<FlickrItemId> loadAlbumPhotos(FlickrItemId albumId) {
String flickr_photosetId = albumId.getPhotosetId();
LinkedList<FlickrItemId> childrenOfAnAlbum = new LinkedList<FlickrItemId>();
PhotoList<Photo> photoList = flickrService.getPhotosFromAlbum(flickr_photosetId);
logger.info("Loading a photo list (with {} photos) for album with photoset_id [{}]. ", photoList.size(), flickr_photosetId);
int i = 0;
for (Photo photo : photoList) {
FlickrItemId itemId = FlickrItemUtil.createIdByPhotoWithParent(albumId, photo);
FlickrItem item = FlickrItemUtil.createItemByPhotoWithParent(itemId, photo);
childrenOfAnAlbum.add(itemId);
all.put(itemId, item);
i++;
if (RESTRICT && i == RESTRICT_TO) {
break;
}
}
logger.info("Loaded {} photos", i);
childrenIDsMap.put(albumId, childrenOfAnAlbum);
return childrenOfAnAlbum;
}
private void configure() {
Map<String, Class> props = FlickrItem.IDs.getIDs();
for (String id : props.keySet()) {
addContainerProperty(id, props.get(id), null);
}
}
}
Notes:
You should study FlickrCustomTreeContainer#getChildrento understand how lazy loading is implemented.
List<FlickrItemId> preorderItemKeys is used in many methods. It represents the list of IDs that are currently visible.
When a user clicks a folder (album), it expands or collapses. This triggers #setCollapsed via an event. preorderItemKeys is set to null and must be assigned the next time #getPreorder is used.
Methods throwing UnsupportedOperationException are not shown above for better readability.
Tree presenter and definition
The definition class extends TreePresenterDefinition:
public class FlickrBrowserTreePresenterDefinition extends TreePresenterDefinition {
public FlickrBrowserTreePresenterDefinition() {
setImplementationClass(FlickrBrowserTreePresenter.class);
}
}
public class FlickrBrowserTreePresenter extends TreePresenter {
private final FlickrService flickrService;
private UiContext uiContext;
@Inject
public FlickrBrowserTreePresenter(TreeView view, ComponentProvider componentProvider, FlickrService flickrService, UiContext uiContext) {
super(view, componentProvider);
this.flickrService = flickrService;
this.uiContext = uiContext;
}
@Override
protected Container initializeContainer() {
return new FlickrCustomTreeContainer(flickrService, uiContext);
}
@Override
public String getIcon(Item item) {
if (item instanceof FlickrItem) {
if (FlickrItem.FLICKRITEMTYPE_ALBUM.equals(item.getItemProperty(FlickrItem.PROPERTY_FLICKR_ITEM_TYPE).getValue())) {
return "icon-folder-l";
} else {
return "icon-file-image";
}
}
return super.getIcon(item);
}
}
As we already know from others containers, #initializeContainer must be implemented to set the container. What is new compared to containers in previous examples, #getIcon sets the icon depending on the underlying Item.
Tree configuration
Configuring the tree view is similar to the list view in the simple example. The definition class is FlickrBrowserTreePresenterDefinition. For the columns we use again FlickrBrowserItemColumnDefinition as class and FlickrBrowserItemColumnFormatter as formatterClass. The fields description and numberOfPhotos are not shown here.
The module also has an ImageProvider. Image provider is a component that renders images used in apps. It generates the portrait image at the bottom of the action bar and the thumbnails for the thumbnail view.
Implementation
FlickrBrowserPreviewImageProvider is different from SimpleFlickrBrowserPreviewImageProvider used the simple example. In the #getThumbnailResource(Object itemId, String generator) method, FlickrItemId already knows the thumbnail URL. This way we do not need to call the Flickr API again and can save time and network resources.
Creating and configuring a thumbnail view and container
Thumbnail container
Unlike in Creating a flat container, the thumbnail view in the hierarchical example has its own Container. We extend ThumbnailContainer which is provided by Magnolia.
public class FlickrThumbnailContainer extends ThumbnailContainer {
public FlickrThumbnailContainer(ImageProvider imageProvider, FlickrService flickrService, UiContext uiContext) {
super(imageProvider, new FlickrFlatIdProvider(flickrService));
}
}
Now we have to provide an implementation of ThumbnailContainer.IdProvider:
public class FlickrThumbnailPresenterDefinition extends ThumbnailPresenterDefinition {
public FlickrThumbnailPresenterDefinition() {
setImplementationClass(FlickrThumbnailPresenter.class);
}
}