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:
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 name | Value |
---|
| |
| |
| |
| |
| |
| |
| |
| |
| info.magnolia.ui.contentapp.browser.BrowserSubAppDescriptor |
| info.magnolia.ui.contentapp.browser.BrowserSubApp |
| info.magnolia.ui.contentapp.ContentApp |
Properties:
flickr-simple-browser
| required App |
subapps
| required |
browser
| required Subapp |
class
| required $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
BrowserSubAppDescriptor
|
subAppClass
| required $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
BrowserSubApp
|
appClass
| required Must be $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
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
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
SimpleFlickrItem
.
public interface SimpleFlickrItem extends BasicFlickrItem {
}
The interface extends
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
BasicFlickrItem
in the flickr-integration
module which is the basic item interface that other submodules extend. SimpleFlickrItem
has no other properties.
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.
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
SimpleFlickrPropertysetItem
extends
PropertysetItem, one of the implementations provided by Vaadin.
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:
public interface SimpleFlickrBrowserContentConnector extends ContentConnector {
Container getContainer();
}
Implementation
Then implement the interface:
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 && ((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
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
FlickrService
interface is injected in the constructor. FlickrService is instantiated by $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
FlickrServiceProvider
which is registered in the flickr-integration
module.
Configuration
Every subapp must configure its own content connector. The
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
ConfiguredSimpleFlickrBrowserContentConnectorDefinition
class extends $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
ConfiguredContentConnectorDefinition
and sets the implementation class $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
SimpleFlickrBrowserContentConnectorImpl
.
browser:
contentConnector:
class: info.magnolia.flickr.simplebrowser.app.contentconnector.ConfiguredSimpleFlickrBrowserContentConnectorDefinition
Node name | Value |
---|
| |
| |
| info.magnolia.flickr.simplebrowser.app.contentconnector.ConfiguredSimpleFlickrBrowserContentConnectorDefinition |
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
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
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
Refreshable
so we can use the refresh mechanism in the Magnolia workbench. We also need the
Container.Indexed
subinterface for lazy loading.
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 && albums.getTotal() > 0) {
sectionCounter = 0;
for (Photoset photoset : albums.getPhotosets()) {
if (photoset != null && 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 && 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
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
ThumbnailPresenter
and initialize the container:
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:
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 name | Value |
---|
| |
| |
| |
| |
| 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
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
ThumbnailContainer
and
$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
ThumbnailItem
and store the URL as a property in the item.
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 name | Value |
---|
| |
| |
| info.magnolia.ui.imageprovider.definition.ConfiguredImageProviderDefinition |
| 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
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
.
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 name | Value |
---|
| |
| |
| |
| |
| |
| |
| info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnDefinition |
| info.magnolia.flickr.app.workbench.FlickrBrowserItemColumnFormatter |
| title |
| 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 $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
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 $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
AbstractColumnFormatter
|
propertyName
| required The name of the property as configured in the container. See $webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources")
SimpleFlickrFlatContainer
#configure and SimpleFlickrItem.IDs#getIDs . |
class
| required Presenter definition class. |
#trackbackRdf ($trackbackUtils.getContentIdentifier($page) $page.title $trackbackUtils.getPingUrl($page))