Developing a custom (non-JCR) content app
This page shows how to create an app that operates on data stored outside the JCR repository. The app uses the content app framework but stores data on the local file system. This is an example of connecting to a custom data source. Editors don’t have to leave Magnolia in order to work on third-party data.
Background
The data that content apps operate on is typically stored in the Java Content Repository (JCR). However, apps can also operate on data that resides outside the JCR.
Editing and viewing custom data is a common requirement. For example, editors may need to access product data in a remote ERP system in order to create content to sell the products. Such remote data can reside in a relational database, on the filesystem, in a Web service, or some other data delivery service. For convenience, you want to make the remote data available inside Magnolia in a familiar environment so editors don’t need to jump from one system to another.
This tutorial requires that you have some Java programming experience. You need a development environment and a basic understanding of Magnolia modules. You will work with interfaces originating from the Vaadin framework used to build the Magnolia UI.
Create a module
The app you are about to develop is called the File System Browser app. It is a simple app which connects to the file system and allows you to edit images from it.
The app needs to be deployed as a Magnolia module. Choose from the following options depending on your skill level.
Option 1: Clone the project in Git
Choose this option if you know how to work with Magnolia projects and Git. You get the module code on your local system and can edit it in your IDE.
-
Clone the fs-browser-app repository.
git clone https://git.magnolia-cms.com/scm/documentation/fs-browser-app.git
-
Import the project into your IDE.
-
Build the project into a JAR and deploy it to a Magnolia instance or run the project in your IDE. To run it in the IDE, add the module as a dependency in the POM file of your bundle, as described in the next option.
Option 2: Add the project as a dependency to your bundle
Choose this option if you want to add the project to your own Magnolia bundle. The module code will not be stored on your local system. Add the following dependency to your bundle:
<dependency>
<groupId>info.magnolia.documentation</groupId>
<artifactId>fs-browser-app</artifactId>
<version>1.3</version> (1)
</dependency>
1 | Should you need to specify the module version, do it using <version> . |
Option 3: Download the module JAR
Choose this option if you are new to Magnolia or don’t have a development environment. You get the complete app and can use it.
-
Download the fs-browser-app-1.3.jar file from Nexus and follow the standard module installation instructions.
-
Copy the JAR into the
<CATALINA_HOME>/webapps/<contextPath>/WEB-INF/lib
folder. Typically, this is<CATALINA_HOME>/webapps/magnoliaAuthor/WEB-INF/lib
. -
Restart your Magnolia instance and run the Web update. This will install the module.
Overview
To create a custom content app, you need to implement:
-
A data source definition, an item resolver, a property set factory and, if required, also an image provider.
-
Custom data providers, for flat and hierarchical views.
-
Custom presenter classes for every view type you want (
ListPresenter
,TreePresenter
,ThumbnailPresenter
and other types). -
Action classes, for manipulation and interaction with content.
-
Custom
AvailabilityRule
classes, if required. -
Configuration.
Creating a data source definition and configuring data source components
Data source definition represents the source of data. The FSDatasourceDefinition
class:
-
Extends
info.magnolia.ui.datasource.BaseDatasourceDefinition
. -
Names the data source as
filesystem
. -
Adds a root path field to configure the relative root of data objects.
public class FSDatasourceDefinition extends BaseDatasourceDefinition {
private String rootFolder = "/";
public FSDatasourceDefinition() {
setName("filesystem");
}
public String getRootFolder() {
return rootFolder;
}
public void setRootFolder(String rootFolder) {
this.rootFolder = rootFolder;
}
}
Next, implement an item resolver component that could return the IDs of items and vice versa.
public class FileItemResolver implements ItemResolver<File> {
@Override
public String getId(File file) {
return file.getPath();
}
@Override
public Optional<File> getItemById(String s) {
return Optional.ofNullable(s)
.map(File::new);
}
}
Also, you need to implement info.magnolia.ui.datasource.PropertySetFactory
to be able to read from (and possibly write into) the beans.
public class FSPropertySetFactory implements PropertySetFactory<File> {
@Override
public PropertySet<File> withProperties(Map<String, Class> map) {
return BeanPropertySet.get(File.class);
}
@Override
public PropertySet<File> fromFieldDefinitions(Collection<FieldDefinition> collection, Locale locale) {
return BeanPropertySet.get(File.class);
}
}
If required, you can implement an image provider to supply icons and action bar previews.
public class FSImageProvider implements PreviewProvider<File> {
@Override
public Optional<Resource> getResource(File file) {
return Optional.of(file)
.filter(File::isFile)
.filter(Exceptions.wrap().predicate(maybeImage -> new Tika().detect(maybeImage).startsWith("image/")))
.map(o -> {
try {
return FileUtils.openInputStream(o);
} catch (IOException e) {
return null;
}
})
.map(fileInputStream -> (StreamResource.StreamSource) () -> fileInputStream)
.map(streamSource -> new StreamResource(streamSource, "image" + System.currentTimeMillis() + ".png"));
}
}
All components must be configured in the module descriptor.
fs-browser-app.xml
(module descriptor, excerpt)<components>
<id>datasource-filesystem</id>
<component>
<type>info.magnolia.ui.datasource.ItemResolver</type>
<implementation>info.magnolia.filesystembrowser.app.data.FileItemResolver</implementation>
</component>
<component>
<type>info.magnolia.ui.contentapp.browser.preview.PreviewProvider</type>
<implementation>info.magnolia.filesystembrowser.app.imageprovider.FSImageProvider</implementation>
</component>
<component>
<type>info.magnolia.ui.datasource.PropertySetFactory</type>
<implementation>info.magnolia.filesystembrowser.app.contentview.FSPropertySetFactory</implementation>
</component>
Creating data providers
Alongside the data source definition and item resolver, you need to implement data source providers to be used within browser presenter classes. Usually, you need to define:
-
A filterable (flat) data provider (
info.magnolia.filesystembrowser.app.contentview.FSDataProvider
) which implements item fetching by query. -
A hierarchical provider which implements fetching of children of a given data element (
info.magnolia.filesystembrowser.app.contentview.HierarchicalFSDataProvider
).
FSDataProvider.java
(excerpt, fetch method)public class FSDataProvider extends AbstractBackEndDataProvider<File, DataFilter> {
...
@Override
public Stream<File> fetchFromBackEnd(Query<File, DataFilter> query) {
return FileUtils.listFiles(new File(datasourceDefinition.getRootFolder()), null, true).stream()
.filter(File::isFile)
.skip(query.getOffset())
.limit(query.getLimit());
}
...
}
HierarchicalFSDataProvider.java
(excerpt, fetch method)public class HierarchicalFSDataProvider extends AbstractHierarchicalDataProvider<File, DataFilter> {
...
@Override
public Stream<File> fetchChildren(HierarchicalQuery<File, DataFilter> query) {
return listFiles(query.getParentOptional().orElseGet(() -> new File(datasourceDefinition.getRootFolder())));
}
private Stream<File> listFiles(File file) {
return Optional.of(file)
.map(File::listFiles)
.map(Arrays::stream)
.map(Stream::sorted)
.orElseGet(Stream::empty);
}
...
}
Content views and presenters
Workbench is a view that displays a list of content items in a workspace. It is typically defined in the browser
subapp.
The workbench contains a list of content views, such as tree
, list
and thumbnail
— the most common view types available in a typical JCR content app. Other view types can be defined as well.
For each view type, you need to implement a custom presenter — a presenter attached to the right data provider. The following are the base presenter classes in the info.magnolia.ui.contentapp.browser
package.
-
ListPresenter
-
TreePresenter
-
ThumbnailPresenter
Once you have implemented the classes, you must provide type-mapping of the presenters. This is done in the module descriptor.
fs-browser-app.xml
(excerpt) <type-mapping>
<type>info.magnolia.ui.contentapp.browser.TreePresenter</type>
<implementation>info.magnolia.filesystembrowser.app.contentview.FSTreePresenter</implementation>
</type-mapping>
<type-mapping>
<type>info.magnolia.ui.contentapp.browser.ListPresenter</type>
<implementation>info.magnolia.filesystembrowser.app.contentview.FSListPresenter</implementation>
</type-mapping>
<type-mapping>
<type>info.magnolia.ui.contentapp.browser.ThumbnailPresenter</type>
<implementation>info.magnolia.filesystembrowser.app.contentview.FSThumbnailPresenter</implementation>
</type-mapping>
</components>
Implementing action classes to operate on custom content
Your data source is now anchored in the content app and is ready to be used. The next step is to create actions and the Action bar so that the user of the app could work with the content items.
Actions and the Action bar are configured through the action definition and action bar definition, respectively. |
You can start with info.magnolia.filesystembrowser.app.action.DuplicateFileAction
, for example. This action execute method takes a single item from value context (that is an item or a file which is selected in the browser) and duplicates it within the filesystem.
public class DuplicateFileAction<D extends DuplicateFileActionDefinition> extends AbstractAction<D> {
private final ValueContext<File> valueContext;
private final DatasourceObservation.Manual datasourceObservation;
@Inject
public DuplicateFileAction(D definition, ValueContext<File> item, DatasourceObservation.Manual datasourceObservation) {
super(definition);
this.valueContext = item;
this.datasourceObservation = datasourceObservation;
}
@Override
public void execute() {
valueContext.getSingle().ifPresent(file -> {
try {
File copy = createFileCopy(file);
if (file.isFile()) {
FileUtils.copyFile(file, copy);
} else {
FileUtils.copyDirectory(file, copy);
}
datasourceObservation.trigger();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
private File createFileCopy(File file) {
String extension = StringUtils.substringAfterLast(file.getPath(), ".");
extension = StringUtils.isEmpty(extension) ? "" : "." + extension;
String newName = file.getPath().replace(extension, "") + " copy";
File copyFile = new File(newName + extension);
int idx = 0;
while (copyFile.exists()) {
copyFile = new File(newName + ++idx + extension);
}
return copyFile;
}
}
Final assembly
All parts of the app are now ready and the app’s configuration can be finalized in the app descriptor. See below a complete YAML definition of the fs-browser-app
:
fs-browser.yaml
(app descriptor)class: info.magnolia.ui.contentapp.configuration.ContentAppDescriptor
appClass: info.magnolia.ui.framework.app.BaseApp
datasource:
class: info.magnolia.filesystembrowser.app.data.FSDatasourceDefinition
rootFolder: /
subApps:
browser:
class: info.magnolia.ui.contentapp.configuration.BrowserDescriptor
actions:
duplicate:
class: info.magnolia.filesystembrowser.app.action.DuplicateFileActionDefinition
icon: icon-duplicate
actionbar:
sections:
item:
groups:
edit:
items:
- name: duplicate
workbench:
contentViews:
tree:
$type: treeView
columns:
- name: name
list:
$type: listView
columns:
- name: name
- name: path
thumbnail:
$type: thumbnailView