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 ;-) . |
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
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
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
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
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.
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. |
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:
-
fromHit(), to get a Connectable from a lucene document.
-
fromModel(), to transform a BlogsEntry into a Connectable.
-
fromModelId(), to get a connectable from a BlogsEntry id.
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