The 5.7 branch of Magnolia reached End-of-Life on December 31, 2023, as specified in our End-of-life policy. This means the 5.7 branch is no longer maintained or supported. Please upgrade to the latest Magnolia release. By upgrading, you will get the latest release of Magnolia featuring significant improvements to the author and developer experience. For a successful upgrade, please consult our Magnolia 6.2 documentation. If you need help, please contact info@magnolia-cms.com.

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.

Read Content app with a String ItemId and single container  first. Classes used here are similar to classes in the flat container example.

Overview

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.
  • thumbnailUrl: In the Content app with a String ItemId and single container we saw that putting the thumbnail URL in the ItemId saves many Flickr API calls. We set the thumbnail URL only when required.

Interface

The FlickrItemId interface contains a static class  UuidParser  which provides some convenience methods to create and parse a UUID.

info.magnolia.flickr.browser.app.item.FlickrItemId
public interface FlickrItemId {

    String getUuid();
    String getPhotoId();
    String getPhotosetId();
    FlickrItemId getParent();
    boolean isForAlbum();
    String getThumbnailUrl();
    void setThumbnailUrl(String thumbnailUrl);

    public static class UuidParser {
        private static final String SET = "@set";
        private static final String PHOTO = "@photo";
        private static final String SUFFIX = "s@@@";
        protected static final String NO_ALBUM_CONTEXT = "-niac-";
        private static final String TYPE_PHOTO = "@is" + SUFFIX;
        private static final String TYPE_ALBUM = "@ia" + SUFFIX;
        private String photoId;
        private String albumId;
        private boolean isForAlbum;
        /*
            PATTERN:  SET+<photoSetId>+PHOTO+<photo_id>+typeSUFFIX
        */
        private UuidParser(String uuid) {
            if (isValidUrlFragment(uuid)) {
                isForAlbum = uuid.endsWith(TYPE_ALBUM);
                String tmp = uuid.substring(uuid.indexOf(SET) + SET.length());
                tmp = tmp.substring(0, tmp.length() - TYPE_ALBUM.length());
                String[] split = tmp.split(PHOTO);
                if (split != null &amp;&amp; split.length == 2) {
                    photoId = split[1];
                    albumId = split[0];
                }
            }
        }

        protected String getAlbumId() {
            return albumId;
        }
        protected String getPhotoId() {
            return photoId;
        }
        protected boolean isForAlbum() {
            return isForAlbum;
        }
        protected static UuidParser parse(String uuid) {
            return new UuidParser(uuid);
        }

        protected static boolean isValidUrlFragment(String urlFragment) {
            return (StringUtils.isNotBlank(urlFragment) &amp;&amp; urlFragment.startsWith(SET) 
					&amp;&amp; urlFragment.endsWith(SUFFIX) &amp;&amp; urlFragment.contains(PHOTO));
        }
        protected static String createUuid(Photo photo, String parentPhotoSetId) {
            if (StringUtils.isBlank(parentPhotoSetId) || photo == null || StringUtils.isBlank(photo.getId())) {
                return null;
            }
            return SET + parentPhotoSetId + PHOTO + photo.getId() + TYPE_PHOTO;
        }
        protected static String createUuid(Photo photo) {
            if (photo == null || StringUtils.isBlank(photo.getId())) {
                return null;
            }
            return SET + NO_ALBUM_CONTEXT + PHOTO + photo.getId() + TYPE_PHOTO;
        }
        protected static String createUuid(Photoset photoset) {
            if (photoset == null || StringUtils.isBlank(photoset.getId())) {
                return null;
            }
            return SET + photoset.getId() + PHOTO + photoset.getPrimaryPhoto().getId() + TYPE_ALBUM;
        }
    }
}

Implementation

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 exampleFlickrItem extends BasicFlickrItem but it has additional properties.

snippet of info.magnolia.flickr.browser.app.item.FlickrItem
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:

Map Map<String, Class> properties = SimpleFlickrItem.IDs.getIDs();

Implementation

We extend the Vaadin class PropertysetItem for the item implementation. The implementation is similar to what was done in  SimpleFlickrPropertysetItem .

info.magnolia.flickr.browser.app.item.FlickrPropertysetItem
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, i n both the container and the content connector. Here we implement a utility class which provides methods to use in both.

info.magnolia.flickr.browser.app.item.FlickrItemUtil
public class FlickrItemUtil {

    public static FlickrItemId createIdByItem(FlickrItem item, FlickrService flickrService) {
        if (item instanceof FlickrItem) {
            String uuid = item.getUuid();
            // album
            if (FlickrItem.FLICKRITEMTYPE_ALBUM.equals(item.getItemProperty(FlickrItem.PROPERTY_FLICKR_ITEM_TYPE).getValue())) {
                return new FlickrItemIdImpl(uuid, false, null);
            }
            // photo
            else {
                return new FlickrItemIdImpl(uuid, true, flickrService);
            }
        }
        return null;
    }

    public static FlickrItem createItemById(FlickrItemId itemId, FlickrService flickrService) {
        if (itemId != null &amp;&amp; itemId instanceof FlickrItemId) {
            String uuid = itemId.getUuid();
            if (FlickrItemId.UuidParser.isValidUrlFragment(uuid)) {
                // Album
                if (itemId.isForAlbum()) {
                    String albumId = itemId.getPhotosetId();
                    Photoset photoset = flickrService.getPhotoset(albumId);
                    if (photoset != null) {
                        return createItemByPhotoSet(photoset);
                    }
                }
                // Photo
                else {
                    String photoId = itemId.getPhotoId();
                    Photo photo = flickrService.getPhoto(photoId);
                    if (photo != null) {
                        return createItemByPhoto(photo);
                    }
                }
            }
        }
        return null;
    }

    public static FlickrItem createItemByPhoto(Photo photo) {
        return new FlickrPropertysetItem(photo);
    }

    public static FlickrItem createItemByPhotoWithParent(FlickrItemId itemId, Photo photo) {
        return new FlickrPropertysetItem(photo, itemId.getUuid());
    }

    public static FlickrItem createItemByPhotoSet(Photoset photoset) {
        return new FlickrPropertysetItem(photoset);
    }

    public static FlickrItemId createIdByPhotoset(Photoset photoSet) {
        return new FlickrItemIdImpl(photoSet);
    }

    public static FlickrItemId createIdByPhotoWithParent(FlickrItemId parentItemId, Photo photo) {
        return new FlickrItemIdImpl(photo, parentItemId);
    }

    public static FlickrItemId createIdByPhotoNoParent(Photo photo) {
        return new FlickrItemIdImpl(photo);
    }

    public static FlickrItemId createIdByUrlFragment(String urlFragment, FlickrService flickrService, boolean addParent) {
        return new FlickrItemIdImpl(urlFragment, addParent, flickrService);
    }
}

Creating and configuring a content connector

Implementation

To implement the ContentConnector we use FlickrItemUtil.

info.magnolia.flickr.browser.app.contentconnector.FlickrBrowserContentConnector
public class FlickrBrowserContentConnector implements ContentConnector {
    private FlickrItemId defaultItemId;
    private final FlickrService flickrService;

    @Inject
    public FlickrBrowserContentConnector(FlickrService flickrService) {
        this.flickrService = flickrService;
    }


    public String getItemUrlFragment(Object itemId) {
        if (itemId == null) {
            return null;
        } else if (itemId instanceof String) {
            return (String) itemId;
        } else if (itemId instanceof FlickrItemId) {
            return ((FlickrItemId) itemId).getUuid();
        } else {
            return null;
        }
    }
    public Object getItemIdByUrlFragment(String urlFragment) {
        return FlickrItemUtil.createIdByUrlFragment(urlFragment, flickrService, true);
    }
    public Object getDefaultItemId() {
        if(defaultItemId==null){
            Photo recentPhoto = flickrService.getDefaultPhoto();
            if(recentPhoto!=null){
                defaultItemId = FlickrItemUtil.createIdByPhotoNoParent(recentPhoto);
            }
        }
        return defaultItemId;
    }
    public Item getItem(Object itemId) {
        if (itemId != null &amp;&amp; itemId instanceof FlickrItemId) {
            return FlickrItemUtil.createItemById((FlickrItemId) itemId, flickrService);
        }
        return null;
    }
    public Object getItemId(Item item) {
        if (item instanceof FlickrItem) {
            return FlickrItemUtil.createIdByItem((FlickrItem)item, flickrService);
        }
        return null;
    }
    public boolean canHandleItem(Object itemId) {
        return itemId != null &amp;&amp; itemId instanceof FlickrItemId;
    }
}

Configuration

browser:
  contentConnector:
    class: info.magnolia.flickr.browser.app.contentconnector.FlickrBrowserContentConnectorDefinition
Node nameValue

 
browser


 
contentConnector


 
class

info.magnolia.flickr.browser.app.contentconnector.FlickrBrowserContentConnectorDefinition

Creating and configuring a tree view

Tree container

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 .

info.magnolia.flickr.browser.app.container.FlickrCustomTreeContainer
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 &amp;&amp; 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 &amp;&amp; childrenIDsMap.size() == 0 &amp;&amp; albumIds == null &amp;&amp; openFolders.size() == 0 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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:

info.magnolia.flickr.browser.app.workbench.FlickrBrowserTreePresenterDefinition
public class FlickrBrowserTreePresenterDefinition extends TreePresenterDefinition {
    public FlickrBrowserTreePresenterDefinition() {
        setImplementationClass(FlickrBrowserTreePresenter.class);
    }
}

The custom tree presenter extends TreePresenter

info.magnolia.flickr.browser.app.workbench.FlickrBrowserTreePresenter
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 formatterClassThe fields description and numberOfPhotos are not shown here.

 workbench:     
   contentViews:
     - name: tree
       class: info.magnolia.flickr.browser.app.workbench.FlickrBrowserTreePresenterDefinition
       columns:
         - name: title
           formatterClass: info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnFormatter
           propertyName: title
           class: info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnDefinition
Node nameValue

 
browser


 
workbench


 
contentViews


 
tree


 
columns


 
title


 
class

info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnDefinition

 
formatterClass

info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnFormatter

 
propertyName

title

 
class

info.magnolia.flickr.browser.app.workbench.FlickrBrowserTreePresenterDefinition

Image provider

The module also has an ImageProviderImage 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.

info.magnolia.flickr.browser.app.imageprovider.FlickrBrowserPreviewImageProvider
public class FlickrBrowserPreviewImageProvider implements ImageProvider {
    private final FlickrService flickrService;
    @Inject
    public FlickrBrowserPreviewImageProvider(FlickrService flickrService) {
        this.flickrService = flickrService;
    }
    @Override
    public String getPortraitPath(Object itemId) {
        return null;
    }
    @Override
    public String getThumbnailPath(Object itemId) {
        return null;
    }
    @Override
    public String resolveIconClassName(String mimeType) {
        return null;
    }

    @Override
    public Object getThumbnailResource(Object itemId, String generator) {
        if (itemId instanceof FlickrItemId) {
            // IDs in the flat container of the thumpnailView might have the url for the thumpnail already set
            if( StringUtils.isNotBlank(((FlickrItemId)itemId).getThumbnailUrl()) ){
                return new ExternalResource(((FlickrItemId)itemId).getThumbnailUrl());
            }
            String photoId = ((FlickrItemId) itemId).getPhotoId();
            return getThumbnailResource(photoId);
        }
        return null;
    }
    /**
     * Returns a {@link Resource} by a given flickr photo_id.
     */
    private Resource getThumbnailResource(String photoId) {
        Resource resource = null;
        if (StringUtils.isNotBlank(photoId)) {
            Photo photo = flickrService.getPhoto(photoId);
            if (photo != null) {
                resource = new ExternalResource(photo.getSmall320Url());
            }
        }
        return resource;
    }
    /**
     * Returns a {@link Resource} by a given flickr photo_id.
     */
    public Resource getSmallThumbnailResource(String photoId) {
        Resource resource = null;
        if (StringUtils.isNotBlank(photoId)) {
            Photo photo = flickrService.getPhoto(photoId);
            if (photo != null) {
                resource = new ExternalResource(photo.getSmallUrl());
            }
        }
        return resource;
    }
}

Configuration

An image provider must be configured per subapp.  

browser:
  imageProvider:
    class: info.magnolia.ui.imageprovider.definition.ConfiguredImageProviderDefinition
    imageProviderClass: info.magnolia.flickr.browser.app.imageprovider.FlickrBrowserPreviewImageProvider
Node nameValue

 
browser


 
imageProvider


 
class

info.magnolia.ui.imageprovider.definition.ConfiguredImageProviderDefinition

 
imageProviderClass

info.magnolia.flickr.browser.app.imageprovider.FlickrBrowserPreviewImageProvider

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.

info.magnolia.flickr.browser.app.container.FlickrThumbnailContainer
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 :

info.magnolia.flickr.browser.app.container.FlickrFlatIdProvider
public class FlickrFlatIdProvider implements ThumbnailContainer.IdProvider {
    private static Logger log = LoggerFactory.getLogger(FlickrFlatIdProvider.class);
    private boolean RESTRICT = false;
    private int RESTRICT_TO = 3;
    private int MAX_CAPACITY = 500;
    private boolean RESTRICT_MAX_CAPACITY = false;

    private final boolean ADD_UNBOUND_PHOTOS = false;
    private final FlickrService flickrService;
    private List<FlickrItemId> ids = null;

    protected FlickrFlatIdProvider(FlickrService flickrService) {
        this.flickrService = flickrService;
    }

    public List<?> getItemIds() {
        if (ids == null) {
            refresh();
        }
        return ids;
    }
    private void refresh() {
        long start = System.currentTimeMillis();
        ids = new ArrayList<FlickrItemId>();
        HashSet<String> photoIds = new HashSet<String>();
        int sectionCounter = 0;
        log.info("Loading IDs in {}", this.getClass().getName());
        // the list of all photos which are NOT within an album (aka photoset)
        // unfortunately, (request which require auth.) doesn't work yet.
        if (ADD_UNBOUND_PHOTOS) {
            PhotoList<Photo> unboundPhotos = flickrService.getPhotosNotInAnyAlbum(0, 0);
            if (unboundPhotos != null) {
                for (Photo photo : unboundPhotos) {
                    String photoId = photo.getId();
                    if (!photoIds.contains(photoId)) {
                        photoIds.add(photoId);
                        FlickrItemId itemId = FlickrItemUtil.createIdByPhotoNoParent(photo);
                        if (itemId != null) {
                            photoIds.add(photoId);
                            itemId.setThumbnailUrl(photo.getSmall320Url());
                            ids.add(itemId);
                            sectionCounter++;
                        }
                    }
                }
                log.info("Loaded IDs for {} photos which are not bound to an album.", sectionCounter);
            }
        }
        //
        // now all the photos from all albums. (this can be a lot! Find a possibility to filter!)
        // restrict! requires some filter!
        Photosets albums = flickrService.getPhotosets();
        int albumCount = 0;
        if (albums != null &amp;&amp; albums.getTotal() > 0) {
            sectionCounter = 0;
            for (Photoset photoset : albums.getPhotosets()) {
                if (photoset != null &amp;&amp; photoset.getPhotoCount() > 0) {
                    String albumId = photoset.getId();
                    int photoCounter = 0;
                    PhotoList<Photo> photoList = flickrService.getPhotosFromAlbum(albumId);
                    for (Photo photo : photoList) {
                        String photoId = photo.getId();
                        if (!photoIds.contains(photoId)) {
                            FlickrItemId itemId = FlickrItemUtil.createIdByPhotoNoParent(photo);
                            if (itemId != null) {
                                photoIds.add(photoId);
                                itemId.setThumbnailUrl(photo.getSmall320Url());
                                ids.add(itemId);
                                sectionCounter++;
                                photoCounter++;
                            }
                        }
                        if (RESTRICT &amp;&amp; photoCounter > RESTRICT_TO || RESTRICT_MAX_CAPACITY &amp;&amp; MAX_CAPACITY < photoIds.size()) {
                            break;
                        }
                    }
                }
                albumCount++;
                if (RESTRICT &amp;&amp; albumCount > RESTRICT_TO || RESTRICT_MAX_CAPACITY &amp;&amp; MAX_CAPACITY < photoIds.size()) {
                    break;
                }
            }
        }
    }
}

Thumbnail presenter

The presenter extends ThumbnailPresenter:

info.magnolia.flickr.browser.app.workbench.FlickrThumbnailPresenter
public class FlickrThumbnailPresenter extends ThumbnailPresenter {
    private final FlickrService flickrService;
    private final ImageProvider imageProvider;
    private final UiContext uiContext;
    private static int thumbnailSize = 150;

    @Inject
    public FlickrThumbnailPresenter(ThumbnailView view, ImageProvider imageProvider, ComponentProvider componentProvider, FlickrService flickrService, UiContext uiContext) {
        super(view, imageProvider, componentProvider);
        this.flickrService = flickrService;
        this.imageProvider = imageProvider;
        this.uiContext = uiContext;
    }

    public ThumbnailView start(WorkbenchDefinition workbench, EventBus eventBus, String viewTypeName, ContentConnector contentConnector) {
        ThumbnailView view = (ThumbnailView) super.start(workbench, eventBus, viewTypeName, contentConnector);
        view.setThumbnailSize(thumbnailSize, thumbnailSize);
        return view;
    }

    protected Container initializeContainer() {
        ThumbnailContainer container = new FlickrThumbnailContainer(imageProvider, flickrService, uiContext);
        container.setThumbnailHeight(thumbnailSize);
        container.setThumbnailWidth(thumbnailSize);
        return container;
    }
}

Thumbnail definition

And here is its definition class:

info.magnolia.flickr.browser.app.workbench.FlickrThumbnailPresenterDefinition
public class FlickrThumbnailPresenterDefinition extends ThumbnailPresenterDefinition {
    public FlickrThumbnailPresenterDefinition() {
        setImplementationClass(FlickrThumbnailPresenter.class);
    }
}

Thumbnail configuration

Node nameValue

 
browser


 
workbench


 
contentViews


 
thumbnail


 
class

info.magnolia.flickr.browser.app.workbench.FlickrThumbnailPresenterDefinition
browser:
  workbench:
    contentViews:
      - name: thumbnail
        class: info.magnolia.flickr.browser.app.workbench.FlickrThumbnailPresenterDefinition