Skip to main content

Label Service in DD4T Java

Many times over we need to implement a Label service mechanism in our projects. This post shows one such approach for DD4T Java.

The idea is to have a Labels Page published from Tridion containing the Labels Component on it. The labels are simply multi-valued Key - Value pairs represented by an Embedded Schema.

The Labels page is read in DD4T and the labels on it are parsed and loaded into a java.util.Map. The front end code employs a custom JSP tag to read the label map and display the associated value for a given key.

Content Manager Part

We use Schema Label with a field 'label' of type Embedded Schema, multi-valued.

The Embedded Schema is 'Embedded label' and contains two field, both single value text fields: 'key' and 'value'.

We create a Component 'Labels' and populate it with our labels. This Component can be localized and translated in sub-Publications, as the Label Service can use context-aware Components to load the set of labels in the right locale.


Further, we place the Labels Component on a Page and publish it with DD4T Component and Page Templates.

DD4T Content Delivery Part

Everything starts with an interface - LabelService, which we define in the dd4t-api project, in package org.dd4t.core.resolvers:

public abstract interface LabelService {
    public Map<String, String> load(int publicationId) throws IOException;
    public String getLabel(String key) throws IOException;
    public String getLabel(String key, int publicationId) throws IOException;
}

The method load is responsible for loading the label Map. The other two getLabel method retrieve a label for a given key potentially in a given Publication context.

An implementation of the interface is in class LabelServiceBase, part of project dd4t-core, in package org.dd4t.core.resolvers.impl:

public abstract class LabelServiceBase implements LabelService {
    @Autowired
    protected PublicationResolver publicationProvider;
    @Autowired
    protected CacheProvider cacheProvider;

    @Override
    public String getLabel(String key) throws IOException {
        return getLabel(key, publicationProvider.getPublicationId());
    }

    @Override
    public String getLabel(String key, int publicationId) throws IOException {
        String mapKey = getMapKey(publicationId);
        Map<String, String> labelMap = (Map<String, String>) cacheProvider.loadFromLocalCache(mapKey);
        if (labelMap == null) {
            synchronized (mapKey) {
                labelMap = (Map<String, String>) cacheProvider.loadFromLocalCache(mapKey);
                if (labelMap == null) {
                    labelMap = load(publicationId);
                    if (labelMap == null) {
                        return key;
                    }
                }
            }
        }
        String result = labelMap.get(key);
        return result == null ? key : result;
    }

    protected String getMapKey(int publicationId) {
        return "LabelMap-" + publicationId;
    }
}

The base implementation makes use of a PublicationResolver that provides the current Publication context, when a Publication is not specified. It also uses a CacheProvider (from package org.dd4t.providers) that provides a generic way to store the label maps. These objects are provided using Spring's Dependency Injection mechanism.

The main method in the class - getLabel tries to retrieve a particular label, and if not found in cache, it attempts to (re-)load the labels for the given context Publication.

The implementation of the load method is left to be specified by the actual DD4T web application layer. Such an implementation makes use of a properties file to read the URL of the Labels page. Then the PageFactory reads the page model and we extract the Label key--value pairs from it.

public class MyLabelService extends LabelServiceBase {

    private static final MyLabelService _instance = new MyLabelService();
    private final String labelPageURL;

    private MyLabelService() throws IOException {
        InputStream input = LabelServiceBase.class.getClassLoader().getResourceAsStream("myapp.properties");
        Properties properties = new Properties();
        properties.load(input);

        labelPageURL = properties.getProperty("labelpage.url");
    }

    public static MyLabelService getInstance() {
        return _instance;
    }

    @Override
    public synchronized Map<String, String> load(int publicationId) throws IOException {
        Map<String, String> result = new TreeMap<>();

        try {
            PageFactory pageFactory = PageFactory.getInstance();
            GenericPage labelPage = (GenericPage) pageFactory.findPageByUrl(labelPageURL, publicationProvider.getPublicationId());

            List<ComponentPresentation> componentPresentations = labelPage.getComponentPresentations();
            ComponentPresentation componentPresentation = componentPresentations.get(0);
            GenericComponent labelComponent = componentPresentation.getComponent();

            Label label = (Label) ModelFactory.createInstance(labelComponent);
            String mapKey = getMapKey(publicationId);

            for (EmbeddedLabel embeddedLabel : label.getLabelList()) {
                String key = embeddedLabel.getKey();
                String value = embeddedLabel.getValue();
                result.put(key, value);
            }

            int itemId = new TCMURI(labelPage.getId()).getItemId();
            cacheProvider.storeInItemCache(mapKey, result, publicationId, itemId);
        } catch (ItemNotFoundException | FilterException | ModelException | ParseException | SerializationException e) {
            throw new IOException("Failed to load labels", e);
        }

        return result;
    }
}

The load method uses specialized model view classes Label and EmbeddedLabel:

@TridionModel(rootElementName = "label")
public class Label extends BaseModel {

    @TridionField(fieldName = "label", fieldType = TridionFieldType.Embedded)
    private List<EmbeddedLabel> labelList;

    public List<EmbeddedLabel> getLabelList() {
        return labelList;
    }

    public void setLabelList(List<EmbeddedLabel> labelList) {
        this.labelList = labelList;
    }
}

@TridionModel(rootElementName = "embeddedLabel")
public class EmbeddedLabel extends BaseModel {

    @TridionField
    private String key;
    @TridionField
    private String value;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

Finally, the whole mechanism can be used from a JSP Tag or function like this:

MyLabelService labelService = MyLabelService.getInstance();
String seeDetailsLabel = labelService.getLabel("seedetails");


Comments

Popular posts from this blog

Scaling Policies

This post is part of a bigger topic Autoscaling Publishers in AWS . In a previous post we talked about the Auto Scaling Groups , but we didn't go into details on the Scaling Policies. This is the purpose of this blog post. As defined earlier, the Scaling Policies define the rules according to which the group size is increased or decreased. These rules are based on instance metrics (e.g. CPU), CloudWatch custom metrics, or even CloudWatch alarms and their states and values. We defined a Scaling Policy with Steps, called 'increase_group_size', which is triggered first by the CloudWatch Alarm 'Publish_Alarm' defined earlier. Also depending on the size of the monitored CloudWatch custom metric 'Waiting for Publish', the Scaling Policy with Steps can add a difference number of instances to the group. The scaling policy sets the number of instances in group to 1 if there are between 1000 and 2000 items Waiting for Publish in the queue. It also sets the

Running sp_updatestats on AWS RDS database

Part of the maintenance tasks that I perform on a MSSQL Content Manager database is to run stored procedure sp_updatestats . exec sp_updatestats However, that is not supported on an AWS RDS instance. The error message below indicates that only the sa  account can perform this: Msg 15247 , Level 16 , State 1 , Procedure sp_updatestats, Line 15 [Batch Start Line 0 ] User does not have permission to perform this action. Instead there are several posts that suggest using UPDATE STATISTICS instead: https://dba.stackexchange.com/questions/145982/sp-updatestats-vs-update-statistics I stumbled upon the following post from 2008 (!!!), https://social.msdn.microsoft.com/Forums/sqlserver/en-US/186e3db0-fe37-4c31-b017-8e7c24d19697/spupdatestats-fails-to-run-with-permission-error-under-dbopriveleged-user , which describes a way to wrap the call to sp_updatestats and execute it under a different user: create procedure dbo.sp_updstats with execute as 'dbo' as

Toolkit - Dynamic Content Queries

This post if part of a series about the  File System Toolkit  - a custom content delivery API for SDL Tridion. This post presents the Dynamic Content Query capability. The requirements for the Toolkit API are that it should be able to provide CustomMeta queries, pagination, and sorting -- all on the file system, without the use third party tools (database, search engines, indexers, etc). Therefore I had to implement a simple database engine and indexer -- which is described in more detail in post Writing My Own Database Engine . The querying logic does not make use of cache. This means the query logic is executed every time. When models are requested, the models are however retrieved using the ModelFactory and those are cached. Query Class This is the main class for dynamic content queries. It is the entry point into the execution logic of a query. The class takes as parameter a Criterion (presented below) which triggers the execution of query in all sub-criteria of a Criterio