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

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

Content Delivery Monitoring in AWS with CloudWatch

This post describes a way of monitoring a Tridion 9 combined Deployer by sending the health checks into a custom metric in CloudWatch in AWS. The same approach can also be used for other Content Delivery services. Once the metric is available in CloudWatch, we can create alarms in case the service errors out or becomes unresponsive. The overall architecture is as follows: Content Delivery service sends heartbeat (or exposes HTTP endpoint) for monitoring Monitoring Agent checks heartbeat (or HTTP health check) regularly and stores health state AWS lambda function: runs regularly reads the health state from Monitoring Agent pushes custom metrics into CloudWatch I am running the Deployer ( installation docs ) and Monitoring Agent ( installation docs ) on a t2.medium EC2 instance running CentOS on which I also installed the Systems Manager Agent (SSM Agent) ( installation docs ). In my case I have a combined Deployer that I want to monitor. This consists of an Endpoint and a

Debugging a Tridion 2011 Event System

OK, so you wrote your Tridion Event System. Now it's time to debug it. I know this is a hypothetical situtation -- your code never needs any kind of debugging ;) but indulge me... Recently, Alvin Reyes ( @nivlong ) blogged about being difficult to know how exactly to debug a Tridion Event System. More exactly, the question was " What process do I attach to for debugging even system code? ". Unfortunately, there is no simple or generic answer for it. Different events are fired by different Tridion CM modules. These modules run as different programs (or services) or run inside other programs (e.g. IIS). This means that you will need to monitor (or debug) different processes, based on which events your code handles. So the usual suspects are: dllhost.exe (or dllhost3g.exe ) - running as the MTSUser is the SDL Tridion Content Manager COM+ application and it fires events on generic TOM objects (e.g. events based on Tridion.ContentManager.Extensibility.Events.CrudEven