7Cogs is Dead! Long Live 7Cogs!

Ding dong! The witch is dead! Which old witch? The 7Cogs witch!

[EDIT: Part II covers additional stuff beyond this post!).

This isn't news anymore - Starting with 6.1, Liferay replaced the sample data in the bundled Liferay download with a much leaner set of pages and web content that didn't overrun your database with data that was generally the first thing to be removed when starting a new Liferay project. It's not that the sample data was bad, it was just... demo data that shotgunned itself all over your database, and required a database reset to remove.

But there are many gems to be found within the ashes of the good ole 7Cogs. It demonstrated how to programmatically create a lot of useful things in Liferay, so in a series of posts starting with this one I'd like to take a look at some of them, and show how I revived the 7Cogs' corpse and re-used it for something a little more interesting. While I won't cover 100% of what 7Cogs did, I hope you find some of these code snippets useful for your Liferay projects, and the resulting application fun and instructive.  

What did 7Cogs do?

7Cogs consisted of a theme and a hook (along with dependencies on other Liferay plugins like some of the social networking portlets). The main guts of the interesting bits are in the hook. When you first started Liferay 6.0, the hook would determine if it had been run before, and if not, go about its business.

If you look at the hook code from which these snippets evolved, you can see that it did a lot of stuff:

  • Added some Web Content Structures and Templates to the Guest Site
  • Inject a bunch of images from the plugin into the Document Library
  • Configured a custom Theme
  • Add a bunch of pages and portlets to the Guest Site public pages, with friendly URLs
  • Added some Organizations
  • Added a bunch of vocabularies, categories, and tags
  • Added a Mobile theme
  • Added a bunch of Roles and associated Permissions
  • Added several users assigned to those Roles and configured their profile pages
  • Added several documents to each user's document library
  • Caused several of them to befriend each other, and others to have outstanding friend requests
  • Added some Message Board threads, wiki pages, blogs, etc
  • Configured Web Content to be workflow'd with Kaleo
  • Faked a bunch of Social Equity data
  • Link all of the above into the semblance of a "real" live site that you just happened upon during the install

The last bit was what got me interested in this in the first place. At the time, I was interested in showing how Liferay could be used to build a community, but most of my work started on a blank site, and I quickly tired of making users and roles and sites and social features and so forth. 7Cogs was busy being retired into the annals of Liferay history, so I decided to try and refresh it to work on the latest 6.1 release and wrote some helper code around it to give some GUI control over what it was doing. My Frankenstein demo was alive, and I could use it to populate a real site, develop some interesting visualizations around social data, and even watch in real time as things were being generated.

First Things First

Over the course of several blog posts, I will build on previous blog posts with additional snippets, eventually building a system that can create a nice looking site with real-looking users and user-generated content from the web, nicely linked with tags and social activity data, and a couple of interesting visualizations that you can use to drive community site participation in your projects.  A few notes before we get started:

  • This should all work in the latest Liferay 6.1 CE and EE releases.
  • Not a lot of error checking here. If it fails it doesn't end up blowing up, but also doesn't continue. I leave it to your trusty development skills to fix this.
  • A lot of this is hard-coded or assumes the English language is being used. You can undo this with basic development skills as well!
  • Several Liferay APIs have many arguments that I don't explain in excruciating detail.  For the major service calls I tried to link to the javadoc, which can be your friend. Most of the time.

Getting Contents of a File from a plugin

public static byte[] getBytes(String path) throws Exception {
    return FileUtil.getBytes(getInputStream(path));
}

public static InputStream getInputStream(String path) throws Exception {
    Class<?> clazz = SocialDriverUtil.class;

    return clazz.getResourceAsStream("/resources" + path);
}

public static String getString(String path) throws Exception {
    return new String(getBytes(path));
}

Pretty simple. It just grabs a resource as a stream under the /resources directory in the plugin, and gives you a stream of bytes (getBytes) and can also convert it to a String (getString). These are used later on to retrieve content from the plugin that are housed in external files (resources) within the plugin.

Adding Resources to a Layout

// add a resource to a layout with the portletId.  Yes, this comment is completely worthless.
public static void addResources(Layout layout, String portletId)
    throws Exception {

    String rootPortletId = PortletConstants.getRootPortletId(portletId);

    String portletPrimaryKey = PortletPermissionUtil.getPrimaryKey(
        layout.getPlid(), portletId);

    ResourceLocalServiceUtil.addResources(
        layout.getCompanyId(), layout.getGroupId(), 0, rootPortletId,
        portletPrimaryKey, true, true, true);
}

This method simply adds the resources associated with a portlet to a layout (presumably because you are adding the portlet to the layout, which we are doing later on). Read the section in the documentation regarding Resources and how they interact with the Permissioning system of Liferay.

Adding a Layout (Page) to a Site

// create a new (blank) page for the given group/s public or private layoutset
public static Layout addLayout(
    Group group, String name, boolean privateLayout, String friendlyURL,
    String layoutTemplateId)
    throws Exception {

    ServiceContext serviceContext = new ServiceContext();

    Layout layout = LayoutLocalServiceUtil.addLayout(
        group.getCreatorUserId(), group.getGroupId(), privateLayout,
        LayoutConstants.DEFAULT_PARENT_LAYOUT_ID, name, StringPool.BLANK,
        StringPool.BLANK, LayoutConstants.TYPE_PORTLET, false, friendlyURL,
        serviceContext);

    LayoutTypePortlet layoutTypePortlet =
        (LayoutTypePortlet) layout.getLayoutType();

    layoutTypePortlet.setLayoutTemplateId(0, layoutTemplateId, false);

    addResources(layout, PortletKeys.DOCKBAR);

    return layout;
}

This code creates new pages in a given Group, with default permissions such that everyone can view the page, and assigns them a layout template (such as "2_columns_ii" for the ii'th version of the standard 2-Column layout with a particular percentage width for each column). It also adds the necessary resources of the dockbar to the permissioning system so that you can see the dockbar when viewing the page.

Adding a Portlet to a Layout (Page)

// persist an updated layout entity
public static void updateLayout(Layout layout) throws Exception {
    LayoutLocalServiceUtil.updateLayout(
        layout.getGroupId(), layout.isPrivateLayout(), layout.getLayoutId(),
        layout.getTypeSettings());
}

// add a portlet to a layout
public static String addPortletId(
    Layout layout, String portletId, String columnId)
    throws Exception {

    LayoutTypePortlet layoutTypePortlet =
        (LayoutTypePortlet) layout.getLayoutType();

    portletId = layoutTypePortlet.addPortletId(
        0, portletId, columnId, -1, false);

    addResources(layout, portletId);
    updateLayout(layout);

    return portletId;
}

The updateLayout method simply flushes the in-memory Layout entity into the Database. If you don't do this, any updates to the layout won't be persisted.

The addPortletId method will add a new portlet to a Layout (page) based on its id (such as PortletKeys.RECENT_BLOGGERS), and place it at the bottom of the specified column (such as "column-1"). You'll see examples of how this is called in a bit.

Setting a custom Title for a Portlet

// set a custom title for a portlet
public static void configurePortletTitle(
    Layout layout, String portletId, String title)
    throws Exception {

    PortletPreferences portletSetup =
        PortletPreferencesFactoryUtil.getLayoutPortletSetup(
            layout, portletId);

    portletSetup.setValue("portletSetupUseCustomTitle", String.valueOf(Boolean.TRUE));
    portletSetup.setValue("portletSetupTitle_en_US", title);

    portletSetup.store();
}

This method updates the displayed title of the portlet. Before you will see the title, you'll need to ensure the portlet is showing its borders.

Showing a Portlet's Borders

// set a portlet to show its borders
public static void addPortletBorder(Layout layout, String portletId)
    throws Exception {

    PortletPreferences portletSetup =
        PortletPreferencesFactoryUtil.getLayoutPortletSetup(
            layout, portletId);

    portletSetup.setValue(
        "portletSetupShowBorders", String.valueOf(Boolean.TRUE));

    portletSetup.store();
}

This method simply turns on the "Show Borders" option for a potlet on a given layout, so that its title is shown, and the portlet gets a rectilinear border around it. Like its configurePortletTitle cousin, it does this by programmatically setting the Portlet's preferences.

That's it for the basics of constructing pages and adding portlets. Now, let's take a look at how we can programmatically create structured Web Content.

Working with Structured Web Content

My demo requires some web content based on structures and templates. The structure (written in xml) and template (Velocity template) are stored externally (hence the need for the previous methods to retrieve files from the plugin).  You'll see what these look like in a later post, but you can always take any of your own articles and download them via the Web Content Editor GUI to see what they look like.  Here are the relevant bits:

Populating a Localization Map

public static void setLocalizedValue(Map<Locale, String> map, String value) {
    Locale locale = LocaleUtil.getDefault();

    map.put(locale, value);

    if (!locale.equals(Locale.US)) {
        map.put(Locale.US, value);
    }
}

This adds a string into a map based on your default locale, and populates the Locale.US value as well if you're not running in that locale. These maps are used when setting the title of web content and other various places where something can take on different, localized values depending on the runtime locale.

Adding a Web Content Structure

Now we are getting somewhere!

public static JournalStructure addJournalStructure(
    long userId, long groupId, String title, String fileName)
    throws Exception {

    Map<Locale, String> nameMap = new HashMap<Locale, String>();

    setLocalizedValue(nameMap, title);

    Map<Locale, String> descriptionMap = new HashMap<Locale, String>();

    setLocalizedValue(descriptionMap, title);

    String xsd = getString(fileName);

    ServiceContext serviceContext = new ServiceContext();

    serviceContext.setAddGroupPermissions(true);
    serviceContext.setAddGuestPermissions(true);

    try {
        JournalStructureLocalServiceUtil.deleteStructure(groupId, title);
    } catch (Exception ex) {
        System.out.println("Ignoring " + ex.getMessage());
    }
    return JournalStructureLocalServiceUtil.addStructure(
        userId, groupId, title, false, StringPool.BLANK, nameMap,
        descriptionMap, xsd, serviceContext);
}

This adds a new Structure, scoped to the specified Group, created by the specified user, with the specified title, whose contents come from the specified fileName. Note the bit with ServiceContext -- this creates the structure with the default permissions (VIEW) so that you can see it as a guest user or as a member of the group. Also, this method first tries to delete the structure if it already exists (and ignores it if there is a failure in doing so, sorry!).

Adding a Web Content Template

public static JournalTemplate addJournalTemplate(
    long userId, long groupId, String title, String structureId, String fileName)
    throws Exception {

    Map<Locale, String> descriptionMap = new HashMap<Locale, String>();

    setLocalizedValue(descriptionMap, "ATemplate");

    Map<Locale, String> nameMap = new HashMap<Locale, String>();

    setLocalizedValue(nameMap, "ATemplate");

    String xsl = getString(fileName);

    ServiceContext serviceContext = new ServiceContext();

    serviceContext.setAddGroupPermissions(true);
    serviceContext.setAddGuestPermissions(true);

    try {
        JournalTemplateLocalServiceUtil.deleteTemplate(groupId, title);
    } catch (Exception ex) {
        System.out.println("Ignoring " + ex.getMessage());

    }
    return JournalTemplateLocalServiceUtil.addTemplate(
        userId, groupId, title, false, structureId, nameMap,
        descriptionMap, xsl, true, "vm", false, false, StringPool.BLANK,
        null, serviceContext);
}

This one is similar to the structure one. The names used here (like "ATemplate") don't really matter. We do the same ServiceContext shenanigans as in the code for adding structures. Note the "vm" - this specifies that the language of the template is velocity (you can use others, such as freemarker).

Adding a Web Content Article

public static JournalArticle addJournalArticle(
    long userId, long groupId, String title, String fileName,
    String structureId, String templateId,
    ServiceContext serviceContext)
    throws Exception {

    String content = getString(fileName);

    serviceContext.setAddGroupPermissions(true);
    serviceContext.setAddGuestPermissions(true);
    serviceContext.setWorkflowAction(WorkflowConstants.ACTION_PUBLISH);
    Map<Locale, String> titleMap = new HashMap<Locale, String>();

    setLocalizedValue(titleMap, title);
    try {
        JournalArticleLocalServiceUtil.deleteArticle(groupId, title, serviceContext);
    } catch (Exception ex) {
        System.out.println("Ignoring " + ex.getMessage());
    }

    return JournalArticleLocalServiceUtil.addArticle(
        userId, groupId, 0, 0, title, false,
        JournalArticleConstants.VERSION_DEFAULT, titleMap, null,
        content, "general", structureId, templateId, StringPool.BLANK,
        1, 1, 2008, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, 0, 0, 0, true,
        true, false, StringPool.BLANK, null, null, StringPool.BLANK,
        serviceContext);
}

Here we are adding an article based on the specified structure and template. We also do the same "delete first" thing to avoid throwing exceptions. There are a lot of arguments to the addArticle method at the bottom. Please refrain from giggling!  You can read all about it in the javadoc.

Causing a given Web Content Display Portlet to display a Web Content Article

// configure a web content display to show a specific article
public static void configureJournalContent(
    Layout layout, Group group, String portletId, String articleId)
    throws Exception {

    PortletPreferences portletSetup =
        PortletPreferencesFactoryUtil.getLayoutPortletSetup(
            layout, portletId);

    if (group == null) {
        portletSetup.setValue("groupId", String.valueOf(layout.getGroupId()));
    } else {
        portletSetup.setValue("groupId", String.valueOf(group.getGroupId()));
    }
    portletSetup.setValue("articleId", articleId);

    portletSetup.store();
}

Pretty basic, it just set's the Web Content Portlets preferences to show the given article. No error checking to ensure that the portlet is actually a web content article either. Whee!

What's Next?

At this point, we have seen how to programmatically create pages, adding and configuring portlets on those pages, creating web content, and displaying web content. In the next installment of this 7Cogs post-mortem, we'll see how we can have fun creating fake users, great-looking profiles for those users, and very real-looking user-generated content based on actual web content from Wikipedia, and put a simple UI on top. Finally, we'll take a look at how to generate and some interesting visualizations of Liferay's social data and explain how these can be used to drive your own communities using Liferay. Stay tuned!

Blogues
Thanks James for the gret info. I have the problem, that I want to create "Site Templates" first. Add to them Pages, and fill this pages with portlets and some content. Then I want to create some communities based on this template(s). The export/import doesn't help in this case. Can I do all that with Liferay API? It is a lot of work to do it every time in a new installation...

Second Question: Is a hook the best way to implement that? Or could I better use the Services and write an external Java Tool?

Thanks!
Morad.
It should be possible to do this. After all, the UI in Liferay does it, right! Just a matter of digging around the source and extracting the relevant bits. I'll try and include this in a follow-up post. As for the hook: it doesn't really matter whether you do this in a hook or anywhere else. I actually did this in a portlet (and included an ajax ui on top, so I could individual pick what kinds of content to create and click a button and get a progress bar and so forth). That's for a follow-on post.
This looks like it will become my new target for developers that I typically point to old releases to be able to single-step through the 7cogs API calls. I'm really happy about these Frankenstein efforts - saves me from going that way sooner or later.
Thanks Olaf, that's exactly what I hoped people would use it for! I haven't really covered any new ground here yet, but I hope people will find my running commentary more useful than just uncommented code.
What is the SocialDriverUtil.class referring to? Should that be an existing class or did you create it as part of this project? I can't find that one within liferay.
David, you can replace that class's mention with any other class in your plugin - it's just used as a reference with which to grab resources from the same JAR file in which the class resides.

But no, it's not a class within Liferay - it's my own custom util class that contains some util methods which I'll expand upon once I finally do part 3 of this series of blog posts emoticon
Do you know what we should use instead of JournalStructureLocalServiceUtil? It's showing deprecated in 6.2 or should I just ignore those warnings emoticon?
I see the notes in the source now. Guess 6.2 changed this approach:

* @deprecated As of 6.2.0, since Web Content Administration now uses the
* Dynamic Data Mapping framework to handle structures
*/