Introduction

The Content Connector will allow you to establish relations between arbitrary Liferay objects (assuming the portlet is properly configured, of course). You'll be able to relate an event with the journal article that announces it, a forum thread with other relevant items, etc.

This document will show you how to use this portlet to connect items, and how to extend it to support other portal entities.

Note
My english is a bit clunky, so the probability that you find something weird in this document is very high ;-) .

Connecting Items

Let's suppose we have a CalEvent for a scheduled talk, and we want to relate this event with the speaker in some way. As the speaker is one of our portal users, we are going to "connect" the User entity with the CalEvent.

The first thing we need to do is to select the first element of the relation (the primary element). You can select this element from the "Available" or the "Connected" tab.

Figure 1: Main Window

images/main.png

From "Available" you'll be able to select your primary from the set of all available entities in the system. In the "Connected" tab only items with existing relations will appear. In both cases, the search form is exactly the same: you can select the type of item from a select list, and search for a specific item with the textbox.

Figure 2: Selecting a Calendar Event

images/available.png

As we want to connect a CalEvent with a user, we select Calendar Event from the combo box. To edit or view the connections for the item, we click on the View Connections button.

Figure 3: Current Connections

images/current.png

After clicking the button, we find ourselves with a listing of the current connections for this primary. As we've made no connections yet, the listing appears empty.

You can also see at the top of the screen the details of the primary element: Id, name, descripcion and type of element.

Figure 4: Selecting a Secondary

images/available-to-connect.png

To add a new connection to the item, select the "Available" tab, choose the desired element type (in our case User), use the search controls if necessary, and click the Connect link of the desired entity. The items you select in the available tab, which form the other side of the relation, are called secondary elements.

And thats all! Now, if you go to the "Current" tab, you'll see the newly created connection appears in the listing.

images/current-updated.png

You have a couple of available actions for the new connection: you can delete it, and modify its priority (the priority only affects the order in with the connections are retrieved).

Warning
One important thing to note is that connections aren't symmetric. That is, the fact that A is connected with B doesn't automatically mean that B is connected with A.

Developing New Connectors

Right now, the portlet only supports three entities: User, CalEvent and JournalArticle, but its reasonably easy to extend it to support the items you need. In the following sections we'll take a look at the portlet guts and we'll add support for the BlogsEntry entity.

Portlet Internals

When extending the portlet, you need to know about three interfaces:

private static final class UserConnectable extends AbstractConnectable {
        public UserConnectable(Connector<User> connector, User model, long priority) {
                _connector = connector;
                _model = model;
                _priority = priority;
        }

        public String getConnectorId() {
                return _connector.getId();
        }

        public Date getCreateDate() {
                return _model.getCreateDate();
        }

        public String getDescription() {
                return _model.getFullName();
        }

        public Class<?> getEntityClass() {
                return User.class;
        }

        public long getId() {
                return _model.getUserId();
        }

        public String getKeywords() {
                return _model.getFullName();
        }

        public Date getModifiedDate() {
                return _model.getModifiedDate();
        }

        public String getName() {
                return _model.getFullName();
        }

        public long getPriority() {
                return _priority;
        }

        public PortletURL getURL(ConnectorContext ctx)
                        throws ConnectorException {
                return PortalConnectorsUtil.getUserURL(_model, ctx);
        }


        private final Connector<User> _connector;
        private final User _model;
        private final long _priority;
}
public interface Connector<T> {
        public String getId();

        public void initOnRender(RenderRequest request, RenderResponse response);

        public void initOnAction(ActionRequest request, ActionResponse response);

        public SearchResult getAvailable(
                        ConnectorContext ctx, int from, int to, Long... exclude)
                        throws ConnectorException;

        public SearchResult getConnected(
                        ConnectorContext ctx, int from, int to) throws ConnectorException;

        public SearchResult getConnectedTo(
                        ConnectorContext ctx, long classPK, int from, int to)
                        throws ConnectorException;

        public Connectable findEntity(ConnectorContext ctx, long classPK)
                        throws ConnectorException;

        public ConnectableFactory<T> getEntityFactory();

        public Class<T> getEntityClass();

        public String getCustomForm();
}
public interface ConnectableFactory<T> {
        public Connectable fromHit(
                        ConnectorContext ctx, Document doc, long priority)
                        throws PortalException, SystemException;

        public Connectable fromModel(ConnectorContext ctx, T model, long priority);

        public Connectable fromModelId(
                        ConnectorContext ctx, long modelId, long priority)
                        throws PortalException, SystemException;
}

Example: BlogsEntry Connector

As noted before, we need to implement at least three classes to extend the portlet. We'll begin by the connector.

While you can implement directly the Connector interface, it's better to extend the class AbstractConnector. In that case, you'll only need to implement the getAvailable(), getEntityClass() and getEntityFactory() methods. Everything else is handled by AbstractConnector.

The getEntityClass() method should return the class of the model interface:

public Class<BlogsEntry> getEntityClass() {
        return BlogsEntry.class;
}

Until we implement the connectable factory, we'll leave a placeholder for the getEntityFactory() method:

public ConnectableFactory<BlogsEntry> getEntityFactory() {
        return null;
}

The only moderately complex thing we have to do is implement the getAvailable() method. This method should return al available blog entries in the portal:

public SearchResult getAvailable(
       ConnectorContext ctx, int from, int to, Long... excluded)
       throws ConnectorException {

        try {
                PortletRequest request = ctx.getRequest();

                long companyId = PortalUtil.getCompanyId(request);
                long groupId = PortalUtil.getScopeGroupId(request);
                long userId = PortalUtil.getUserId(request);
                long ownerUserId = 0; // any user, we don't care.
                String keywords = getSearchText(ctx);
                int start = from;
                int end = to;

                Hits hits = BlogsEntryLocalServiceUtil.search(
                                companyId, groupId, userId, ownerUserId, keywords,
                                start, end);

                ConnectableFactory<BlogsEntry> factory = getEntityFactory();

                List<Connectable> entities =
                        ConnectableCollectionUtil.asConnectables(
                                        ctx, hits.getDocs(), factory, excluded);

                SearchResult result = new SearchResult(hits.getLength(), entities);

                return result;
        } catch (SystemException e) {
                throw new ConnectorException(e);
        } catch (PortalException e) {
                throw new ConnectorException(e);
        }
}

The ConnectorContext simply allows you to access the PortletRequest and the PortletResponse. From there we obtain everything we need to do the search. The method getSearchText() from the AbstractConnector class returns the text input by the user in the search control.

To transform the BlogsEntry objects into Connectables we use the helper class ConnectableCollectionUtil, that applies the entity factory to each lucene hit and returns the result as a connectable list.

As noted before, the Connectable is simply a wrapper, so its implementation is trivial:

private static final class BlogsEntryConnectable
        extends AbstractConnectable {

        public BlogsEntryConnectable(
                        Connector<BlogsEntry> connector, BlogsEntry model, long priority) {
                _connector = connector;
                _model = model;
                _priority = priority;
        }

        public String getConnectorId() {
                return _connector.getId();
        }

        public Date getCreateDate() {
                return _model.getCreateDate();
        }

        public String getDescription() {
                return StringUtil.shorten(_model.getContent(), 50);
        }

        public Class<?> getEntityClass() {
                return _connector.getEntityClass();
        }

        public long getId() {
                return _model.getEntryId();
        }

        public String getKeywords() {
                return _model.getContent();
        }

        public Date getModifiedDate() {
                return _model.getModifiedDate();
        }

        public String getName() {
                return _model.getTitle();
        }

        public long getPriority() {
                return _priority;
        }

        private final BlogsEntry _model;
        private final Connector<BlogsEntry> _connector;
        private final long _priority;
}

Note that we are extending the AbstractConnectable class instead of implemening the Connectable interface.

The getCreateDate(), getDescription(), getId(), getModifiedDate(), getName() and getPriority() methods are used to show the entity info in the listings. The getKeywords() method should return the keywords to be used by lucene when indexing this entity.

Now for the ConnectableFactory implementation. We have to implement three methods:

public class BlogsEntryConnectableFactory
        implements ConnectableFactory<BlogsEntry> {

        public BlogsEntryConnectableFactory(Connector<BlogsEntry> connector) {
                _connector = connector;
        }

        public Connectable fromHit(ConnectorContext ctx, Document doc, long priority)
                        throws PortalException, SystemException {

                long modelId = Long.parseLong(doc.get(Field.ENTRY_CLASS_PK));

                return fromModelId(ctx, modelId, priority);
        }

        public Connectable fromModel(
                        ConnectorContext ctx, BlogsEntry model, long priority) {

                return new BlogsEntryConnectable(_connector, model, priority);
        }

        public Connectable fromModelId(
                        ConnectorContext ctx, long modelId,     long priority)
                throws PortalException, SystemException {

                BlogsEntry model = BlogsEntryLocalServiceUtil.getBlogsEntry(modelId);

                return fromModel(ctx, model, priority);
        }

        private final Connector<BlogsEntry> _connector;

}

The last step is to implement the getEntityFactory() method in our connector:

public Class<BlogsEntry> getEntityFactory() {
        return new BlogsEntryConnectableFactory(this);
}

The only thing left is to configure the portlet to recognize our new connector. To do that, we have to edit the connector.properties file, located in the root of the portlet source code:

content.connector.supported.types = com.liferay.portlet.journal.model.JournalArticle,\
                                    com.liferay.portal.model.User, \
                                    com.liferay.portlet.calendar.model.CalEvent


com.liferay.portlet.journal.model.JournalArticle.connector.class = \
        com.ceb2b2000.connector.connectors.impl.JournalArticleConnector
com.liferay.portlet.calendar.model.CalEvent.connector.class = \
        com.ceb2b2000.connector.connectors.impl.CalEventConnector
com.liferay.portal.model.User.connector.class = \
        com.ceb2b2000.connector.connectors.impl.UserConnector

Whe have to add our entity class to the content.connector.supported.types property:

content.connector.supported.types = com.liferay.portlet.journal.model.JournalArticle,\
                                    com.liferay.portal.model.User, \
                                    com.liferay.portlet.calendar.model.CalEvent, \
                                    com.liferay.portlet.blogs.model.BlogsEntry

Then, we create a property name <entity class name>.connector.class, pointing to our connector implementation:

com.liferay.portlet.blogs.model.BlogsEntry.connector.class = \
                com.ceb2b2000.connector.connectors.impl.BlogsEntryConnector

Finally, add to the content.Language file a property of the form <entity class name>.connector.name. In our example:

com.liferay.portlet.blogs.model.BlogsEntry.connector.name = Blogs Entry