Page tree
Skip to end of metadata
Go to start of metadata

The flickr-simple-browser module is an example of how to build a custom content app. The module installs an app that allows you to browse photos on Flickr. We use a String as ItemId, which is easy to implement but has the disadvantage that we cannot tell the difference between photos and albums. This example also shows how to use an external Web service as the data source. The list view and the thumbnail view use the same Container coupled to a ContentConnector. See also Content app with an Object ItemId and different containers for an example of a hierarchical container.


Limitation: using a String as an ItemId

The example app can only display photos from Flickr, not albums. We have this limitation because the container uses a simple String as ItemId. The string we use is the photo_id from the Flickr API.

Using a string provided by the data source is often sufficient. A string simplifies the code but it can also have downsides. In this case we can only display photos from Flickr, no albums. Look at these two IDs which originate from Flickr: 72157648362070329 and 15355595988. One is a photo ID, the other one is an album ID also known as a photo set. It is impossible to tell which is which just by looking at the string. We could use the Flickr API to check whether it's a photo or photoset by calling the appropriate methods and checking the return value. But such a test is not very elegant.

For the purposes of this example we accept the limitation and use a string. However, in the second tutorial Content app with an Object ItemId and different containers we use a POJO for the ItemId. There we can display photos and albums.

Overview

In this example we implement the following custom classes:

  • Item
  • ContentConnector
  • Container
  • ImageProvider
  • Presenter

We also add two views to the workbench:

  • Thumbnail view
  • List view

Setting up the module and app

We assume you already know how to set up a new module. We also assume that you also know the Parts of a content app.

name: flickr-simple-browser
appClass: info.magnolia.ui.contentapp.ContentApp
subApps:
  browser:
    subAppClass: info.magnolia.ui.contentapp.browser.BrowserSubApp
    class: info.magnolia.ui.contentapp.browser.BrowserSubAppDescriptor
    # contentConnector:
    # workbench:
    # imageProvider:
Node nameValue

 flickr-simple-browser

 

 apps

 

 flickr-simple-browser

 

 subApps

 

 browser

 

 contentConnector

 

 workbench

 

 imageProvider

 

 class

info.magnolia.ui.contentapp.browser.BrowserSubAppDescriptor

 subAppClass

info.magnolia.ui.contentapp.browser.BrowserSubApp

 appClass

info.magnolia.ui.contentapp.ContentApp

Properties:

flickr-simple-browser

required

App

subapps

required

 

browser

required

Subapp

class

required

BrowserSubAppDescriptor

subAppClass

required

BrowserSubApp

appClass

required

Must be  ContentApp  or a subclass.

For the details of contentConnector, workbench and imageProvider definition, see below.

Creating items

Interface

An Item has properties and every property is identified by its property ID. Define the properties in an interface. In this module we define the interface  SimpleFlickrItem .

info.magnolia.flickr.simplebrowser.app.item.SimpleFlickrItem
public interface SimpleFlickrItem extends BasicFlickrItem {

}

The interface extends  BasicFlickrItem  in the  flickr-integration  module which is the basic item interface that other submodules extend.  SimpleFlickrItem  has no other properties.

info.magnolia.flickr.app.item.BasicFlickrItem
public interface BasicFlickrItem extends Item {
    public static String PROPERTY_TITLE = "title";
    public static String PROPERTY_DESCRIPTION = "description";
    public static String PROPERTY_PHOTOID = "photoId";

    public class IDs {
        private static Map<String, Class> properties = new HashMap<String, Class>();
        static {
            properties.put(PROPERTY_TITLE, String.class);
            properties.put(PROPERTY_DESCRIPTION, String.class);
            properties.put(PROPERTY_PHOTOID, String.class);
        }
        public static Map<String, Class> getIDs() {
            return properties;
        }
    }
}

Our interface has the following property IDs:

  • photoId: The ID of the photo given by Flickr.
  • title: The title given to the photo. If the photo has no title we use the photo ID as a title.
  • description: The description of the photo. Often photos have no descriptions on Flickr.

The interface contains also a static class which provides a map of all the properties and their types. We use the properties map later to configure the container.

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

Implementation

The implementation class allows us to handle a set of identified properties.  SimpleFlickrPropertysetItem  extends PropertysetItem, one of the implementations provided by Vaadin.

info.magnolia.flickr.simplebrowser.app.item.SimpleFlickrPropertysetItem
public class SimpleFlickrPropertysetItem extends PropertysetItem implements SimpleFlickrItem {
    public SimpleFlickrPropertysetItem(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_TITLE, new ObjectProperty(title));
        addItemProperty(PROPERTY_DESCRIPTION, new ObjectProperty(description));
    }
}

The parameter Photo  is class from Flickr4Java, the Java API that wraps the REST-based Flickr API.

Creating and configuring a content connector

We use a non-hierarchical (flat) Container for both the list and the thumbnail view. This allows us to use the same container for both views and makes it possible to couple the container and the ContentConnector.

Interface

Define an interface so that the content connector can use the container:

info.magnolia.flickr.simplebrowser.app.contentconnector.SimpleFlickrBrowserContentConnector
public interface SimpleFlickrBrowserContentConnector extends ContentConnector {
    Container getContainer();
}

Implementation

Then implement the interface:

info.magnolia.flickr.simplebrowser.app.contentconnector.SimpleFlickrBrowserContentConnectorImpl
public class SimpleFlickrBrowserContentConnectorImpl implements SimpleFlickrBrowserContentConnector {
    private final FlickrService flickrService;
    private SimpleFlickrFlatContainer container;
    private SimpleFlickrItem defaultItem;

    @Inject
    public SimpleFlickrBrowserContentConnectorImpl(FlickrService flickrService) {
        this.flickrService = flickrService;
        container = new SimpleFlickrFlatContainer(flickrService);
    }

    public Container getContainer() {
        return container;
    }

    public String getItemUrlFragment(Object itemId) {
        return canHandleItem(itemId) ? (String) itemId : null;
    }

    public Object getItemIdByUrlFragment(String urlFragment) {
        return canHandleItem(urlFragment) ? urlFragment : null;
    }

    public Object getDefaultItemId() {
        if (defaultItem == null) {
            Photo photo = flickrService.getDefaultPhoto();
            if (photo != null) {
                defaultItem = new SimpleFlickrPropertysetItem(photo);
            }
        }
        return defaultItem;
    }

    public Item getItem(Object itemId) {
        return canHandleItem(itemId) ? container.getItem(itemId) : null;
    }

    public Object getItemId(Item item) {
        return item instanceof SimpleFlickrItem ? item.getItemProperty(SimpleFlickrItem.PROPERTY_PHOTOID) : null;
    }

    public boolean canHandleItem(Object itemId) {
        return itemId instanceof String &amp;&amp; ((String) itemId).matches("^[0-9]");
    }
}

Methods:

  • getItemUrlFragment(Object itemId) (see line 16) and getItemIdByUrlFragment(String urlFragment) (see line 20) methods can return their input parameters since our ItemId is a string and the URL fragment and the ItemId are identical in this simple example.
  • getDefaultItemId() method does not need to be implemented. It can return null. But since FlickrService provides a default photo we implement the method. In some cases it is helpful to work with a default item.
  • canHandle() should reject invalid ItemIds. The app could fire an event with an ID / and the event would be handled by the ContentConnector but there is no Flickr photo with the ID /.
  • The  FlickrService  interface is injected in the constructor. FlickrService is instantiated by FlickrServiceProvider  which is registered in the flickr-integration module.

Configuration

Every subapp must configure its own content connector. The  ConfiguredSimpleFlickrBrowserContentConnectorDefinition  class extends  ConfiguredContentConnectorDefinition  and sets the implementation class  SimpleFlickrBrowserContentConnectorImpl .

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

 browser

 

 contentConnector

 

 class

info.magnolia.flickr.simplebrowser.app.contentconnector.ConfiguredSimpleFlickrBrowserContentConnectorDefinition

ContentConnectorProvider  creates and provides only one instance of  SimpleFlickrBrowserContentConnector  in our subapp. The content connector can be injected into any class used within the subapp. You can cast it to your own type if required.

Creating a flat container

We use the same container for the tree view and the thumbnail view. In this case it would be sufficient to implement just the base interface Container. However, we also implement  Refreshable  so we can use the refresh mechanism in the Magnolia workbench. We also need the Container.Indexed subinterface for lazy loading.

info.magnolia.flickr.simplebrowser.app.container.SimpleFlickrFlatContainer
public class SimpleFlickrFlatContainer extends AbstractContainer implements Container, Container.Indexed, Refreshable {
    private static Logger log = LoggerFactory.getLogger(SimpleFlickrFlatContainer.class);
    private final FlickrService flickrService;
    private Map<Object, Class> properties = new HashMap<Object, Class>();
    private List<String> ids;
    private LinkedHashMap<String, SimpleFlickrItem> itemsMap = new LinkedHashMap<String, SimpleFlickrItem>();
    
	@Inject
    public SimpleFlickrFlatContainer(FlickrService flickrService) {
        this.flickrService = flickrService;
        configure();
    }

    public void refresh() {
        removeAllItems();
        loadAll();
    }

    private void loadAll() {
        ids = new ArrayList<String>();
        long start = System.currentTimeMillis();
        int sectionCounter = 0;
        log.info("Loading IDs in {}", this.getClass().getName());
        //
        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 (!ids.contains(photoId)) {
                            ids.add(photoId);
                            SimpleFlickrItem item = new SimpleFlickrPropertysetItem(photo);
                            itemsMap.put(photoId, item);
                            sectionCounter++;
                            photoCounter++;
                        }
                    }
                }
            }
        }
    }

    public Item getItem(Object itemId) {
        return itemsMap.get(itemId);
    }

    public Collection<?> getItemIds() {
        if (ids == null) {
            loadAll();
        }
        return ids;
    }

    public Property getContainerProperty(Object itemId, Object propertyId) {
        SimpleFlickrItem item = (SimpleFlickrItem) getItem(itemId);
        if (item != null) {
            if (item != null) {
                Property property = item.getItemProperty(propertyId);
                if (property != null) {
                    return property;
                }
            }
        }
        return null;
    }

    public Class<?> getType(Object propertyId) {
        return SimpleFlickrItem.IDs.getIDs().get(propertyId);
    }

    public int size() {
        return ids.size();
    }

    public boolean containsId(Object itemId) {
        return ids.contains(itemId);
    }

    public Item addItem(Object itemId) throws UnsupportedOperationException {
        throw new UnsupportedOperationException("Class does NOT SUPPORT this method.");
    }

    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 {
        ids = null;
        itemsMap.clear();
        return ids == null &amp;&amp; itemsMap.size() == 0;
    }

    public Collection<?> getContainerPropertyIds() {
        return SimpleFlickrItem.IDs.getIDs().keySet();
    }

    private void configure() {
        Map<String, Class> props = SimpleFlickrItem.IDs.getIDs();
        for (String id : props.keySet()) {
            addContainerProperty(id, props.get(id), null);
        }
    }

    public int indexOfId(Object itemId) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.indexOf(itemId);
    }

    public Object getIdByIndex(int index) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.get(index);
    }

    public List<?> getItemIds(int startIndex, int numberOfItems) {
        if (itemsMap == null) {
            loadAll();
        }
        return new ArrayList<String>(itemsMap.keySet());
    }

    public Object nextItemId(Object itemId) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        int index = keys.indexOf(itemId);
        if (index < keys.size() - 1) {
            return keys.get(index + 1);
        }
        return null;
    }

    public Object prevItemId(Object itemId) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        int index = keys.indexOf(itemId);
        if (index > 0) {
            return keys.get(index - 1);
        }
        return null;
    }

    public Object firstItemId() {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.get(0);
    }

    public Object lastItemId() {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.get(keys.size() - 1);
    }

    public boolean isFirstId(Object itemId) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.get(0).equals(itemId);
    }

    public boolean isLastId(Object itemId) {
        List<String> keys = new ArrayList<String>(itemsMap.keySet());
        return keys.get(keys.size() - 1).equals(itemId);
    }
}

Notes:

  • The container extends AbstractContainer which provides basic methods for event handling.
  • The code snippet above does not show any methods that throw UnsupportedOperationException or any logging procedures.
  • Inject FlickrService via constructor.
  • The container must be configured. It has to know about the properties of its item. See in #configure (line 111-116) how the properties are set.
  • By implementing Container.Indexed it would be possible to lazy load the Items and ItemIds. To keep the example simple we haven't done that.

Creating a thumbnail view

Thumbnail view is good for displaying visual content such as photos and diagrams. However, you should not use it alone. In this example we pair a thumbnail view with a list view. Both are non-hierarchical.

Presenter

In the presenter class, extend  ThumbnailPresenter  and initialize the container:

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrThumbnailPresenter
public class SimpleFlickrThumbnailPresenter extends ThumbnailPresenter {
    private final SimpleFlickrBrowserContentConnector contentConnector;
    private static int thumbnailSize = 150;

    @Inject
    public SimpleFlickrThumbnailPresenter(ThumbnailView view, ImageProvider imageProvider, ComponentProvider componentProvider, ContentConnector contentConnector) {
        super(view, imageProvider, componentProvider);
        this.contentConnector = (SimpleFlickrBrowserContentConnector) contentConnector;
    }


    protected Container initializeContainer() {
        ThumbnailContainer container = new ThumbnailContainer(getImageProvider(), new ThumbnailContainer.IdProvider() {
            @Override
            public List<Object> getItemIds() {
                return new ArrayList<Object>(contentConnector.getContainer().getItemIds());
            }
        });
        container.setThumbnailHeight(thumbnailSize);
        container.setThumbnailWidth(thumbnailSize);
        return container;
    }


    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;
    }
}

Definition

Set the implementation class in the definition class:

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrThumbnailPresenterDefinition
public class SimpleFlickrThumbnailPresenterDefinition extends ThumbnailPresenterDefinition {
    public SimpleFlickrThumbnailPresenterDefinition() {
        setImplementationClass(SimpleFlickrThumbnailPresenter.class);
    }
}

Configuration

A thumbnail view has no columns so its configuration is simple: just add the definition class.

browser:
  workbench:
    contentViews:
      - name: thumbnail
        class: info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrThumbnailPresenterDefinition
Node nameValue

 browser

 

 workbench

 

 contentViews

 

 thumbnail

 

 class

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrThumbnailPresenterDefinition

Creating and configuring an image provider

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

For this simple example it is sufficient to implement just the getThumbnailResource()  method from the ImageProvider interface. The ItemId string is the Flickr photo ID. To get the URL of a Flickr photo we must call the Flickr API with the photo ID for each item. This consumes time and resources.

To improve the performance, we could use a POJO as an ItemId. We do so in the second tutorial  Content app with an Object ItemId and different containers . Alternatively, extend ThumbnailContainer  and ThumbnailItem  and store the URL as a property in the item.

info.magnolia.flickr.simplebrowser.app.imageprovider.SimpleFlickrBrowserPreviewImageProvider
public class SimpleFlickrBrowserPreviewImageProvider implements ImageProvider { private final FlickrService flickrService; @Inject public SimpleFlickrBrowserPreviewImageProvider(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) { Resource resource = null; if (itemId instanceof String) { String photoId = (String)itemId; if (StringUtils.isNotBlank(photoId)) { /** * To grep data from flickr API here is very costly - since a thumbnail container could call this method here many 100s of times. * Here it would make sense to add the smallUrl as a property of a more sophisticated ItemId */ Photo photo = flickrService.getPhoto(photoId); if (photo != null) { String smallPicUrl = photo.getSmallUrl(); if (StringUtils.isNotBlank(smallPicUrl)) { resource = new ExternalResource(smallPicUrl); } } } } return resource; } }

Configuration

An image provider must be configured per subapp.

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

 browser

 

 imageProvider

 

 class

info.magnolia.ui.imageprovider.definition.ConfiguredImageProviderDefinition

 imageProviderClass

info.magnolia.flickr.simplebrowser.app.imageprovider.SimpleFlickrBrowserPreviewImageProvider

Creating and configuring a list view

It's a Magnolia best practice to always pair a thumbnail view with another view. Here we add a list view since both are non-hierarchical.

Definition

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrListPresenterDefinition
public class SimpleFlickrListPresenterDefinition extends ListPresenterDefinition {
    public SimpleFlickrListPresenterDefinition() {
        setImplementationClass(SimpleFlickrListPresenter.class);
    }
}

Implementation

In the presenter, inject the ContentConnector, cast it its specific type, and then use it to return the Container.

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrListPresenter
public class SimpleFlickrListPresenter extends ListPresenter {
    private final SimpleFlickrBrowserContentConnector contentConnector;

    @Inject
    public SimpleFlickrListPresenter(ListView view, ComponentProvider componentProvider, ContentConnector contentConnector) {
        super(view, componentProvider);
        this.contentConnector = (SimpleFlickrBrowserContentConnector) contentConnector;
    }
    @Override
    protected Container initializeContainer() {
        return contentConnector.getContainer();
    }
}

Configuration

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

 browser

 

 workbench

 

 contentViews

 

 list

 

 columns

 

 title

 

  class

info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnDefinition

 formatterClass

info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnFormatter

 propertyName

title

 class

info.magnolia.flickr.simplebrowser.app.workbench.SimpleFlickrListPresenterDefinition

Properties:

list

required

 

columns

required

Column definitions for tree, list and search views. You don't need to define columns for the thumbnail view. 

<column name>

required

You can choose the name of the column. H owever, it is common practice to use same name as in propertyName.

class

required

The column definition class reads the column configuration and displays the column accordingly. The class must implement  ColumnDefinition .

formatterClass

required

Defines how the column's value is displayed in the UI. This is useful for making the raw data more readable or making it adhere to a formatting convention.The formatter class must extend  AbstractColumnFormatter

propertyName

required

The name of the property as configured in the container. See  SimpleFlickrFlatContainer  #configure and  SimpleFlickrItem.IDs#getIDs.

class

 required

Presenter definition class.

  • No labels