Archive

Archive for the ‘resources’ Category

Resources: The Code for Faceted Search Navigation

December 28, 2011 Leave a comment

In my last post, I discussed the data-capture portion of our new Resources page on our corporate site. Now I’d like to get into the code for the page to show you how things are done.

The main template for the page is pretty much a standard STK template:

Template Definition

Template Definition

There are a couple of interesting things going on in the mainArea and extrasArea, though. Have a look:

Auto-Generated Paragraphs

Auto-Generated Paragraphs

You can see that, in both sections, we’re adding an automatically-generated paragraph. The main area adds the paragraph that’s responsible for rendering search results, and the extras area automatically includes the search navigation. Because we continue to rely on the normal structure of STK pages, we don’t have to do a lot more to build the page structure. The one remaining important piece we do need to put in place, however, is custom CSS and Javascript for the page template. (We’re using the excellent PrettyPhoto jQuery lightbox script and need to include its supporting files.) Here’s how we include the extra files in the page template:

Including CSS and Javascript files

Including CSS and Javascript files

As you can see, we can easily specify whether or not to use Far Future Caching (which improves caching performance significantly) and what media the CSS should apply to.

Now that our page template is built up, let’s take a look at the navigation paragraph. This paragraph’s job is to present a list of all of the different categories for which we have assets, and to allow site visitors to choose from that list. It’s dynamic, in that if a category is already selected, we only display the categories that have items with both that category and the selected category. Here’s the code from the Freemarker template:

    <ul>
        [#list model.categories as category]
            <li class="category">${category.displayName!category}</li>
            [#if category?children?size > 0]
                <ul>
                    [#list category?children?sort as subCategory]
                        [#if model.isCategoryActive( subCategory )]
                            <li class="selected">
                                ${subCategory} (${model.getCategoryCount( subCategory )})
                                <a href="${model.getClearCategoryLink( subCategory )}">${unselectCharacters}</a>
                            </li>
                        [#else]
                            [#if model.getCategoryCount( subCategory ) > 0]
                                <li>
                                    <a href="${model.getCategoryLink( subCategory )}">[#if subCategory.displayName?has_content]${subCategory.displayName}[#else]${subCategory}[/#if] (${model.getCategoryCount( subCategory )})</a>
                                </li>
                            [/#if]
                        [/#if]
                    [/#list]
                </ul>
            [/#if]
        [/#list]
    </ul>

You should be able to see that the Freemarker template gets a list of categories from the model object and iterates through them. It then iterates through the subcategories, displaying each as a link (if it’s not yet selected), or as plain text with a cancel option (if it’s already selected). But obviously, the model class is doing a fair bit of work under the covers to generate the data that Freemarker uses. Let’s take a look at some of the code there. The first thing that the template relies on the list of categories:


    public List<Content> getCategories() throws AccessDeniedException, PathNotFoundException, RepositoryException {
        if ( this.categories == null ) {
            Content getCategoriesFromNode = content;
            //The model is called form the searchResult paragraph, so the categories are on the auto generated navigation paragraph in extras.
            if(!content.hasNodeData("categories")) {
                //Getting the searchNavigation paragraph's node in extras
                getCategoriesFromNode = getRoot().getContent().getContent("extras").getContent("extras1").getContent("extrasItem0");
            }
            this.categories = CategoryUtil.getCategories( getCategoriesFromNode, "categories" );
        }
        return this.categories;
    }

We use lazy initialization for all of the model’s properties, which keeps us from having to compute things unnecessarily or too often. We then use the CategoryUtil class (which is part of the Categorization module) to get the list of Categories that have been specified in the search configuration dialog and return that list.

Next up, we want to determine if a particular subcategory is active — that is, whether the site visitor has already chosen to search on that category. Here’s the code for that:

public boolean isCategoryActive( Content category ) {
    String existingCategories = FCSSelectorUtil.getSelectorValue( CATEGORY_PARAMETER );
    if ( !StringUtils.isEmpty( existingCategories ) ) {
        List<String> categoriesList = Arrays.asList( StringUtils.split( existingCategories, CATEGORY_DELIMITER ) );
        return categoriesList.contains( getAbbreviatedPath( category ) );
    }
    return false;
}

The FCKSelectorUtil class is what we use to extract search parameters from the current URL string. It’s a bit outside the scope of this article, but we built it to allow us to use Selectors instead of query strings to determine what results to show. (Using Selectors allows us to cache the results, while using query strings turns off caching by default in Magnolia.)

Next up, we need to count the number of results available for a given category. This is pretty database-intensive, as we have to count the number of results for videos, slideshows, DMS documents, and webpages, and then add them all together. Note that we have here used Openmind’s very nice Criteria API to simplify the queries as much as possible:

    public int getCategoryCount( Content category ) {
        Criteria websiteCriteria = getWebsiteCriteria();
        websiteCriteria = addCurrentRestrictions( websiteCriteria );
        websiteCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult websiteResult = websiteCriteria.execute();

        Criteria dmsCriteria = getDmsCriteria();
        dmsCriteria = addCurrentRestrictions( dmsCriteria );
        dmsCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult dmsResult = dmsCriteria.execute();

        Criteria videoCriteria = getVideoCriteria();
        videoCriteria = addCurrentRestrictions( videoCriteria );
        videoCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult videoResult = videoCriteria.execute();

        Criteria slideshowCriteria = getSlideshowCriteria();
        slideshowCriteria = addCurrentRestrictions( slideshowCriteria );
        slideshowCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult slideshowResult = slideshowCriteria.execute();

        return websiteResult.getTotalSize() + dmsResult.getTotalSize() + videoResult.getTotalSize() + slideshowResult.getTotalSize();
    }

This method relies on a couple of supporting utility methods. One set is responsible for setting up the basics of a certain type of query. For example:

private Criteria getDmsCriteria() {
    Criteria criteria = JCRCriteriaFactory.createCriteria()
    .setWorkspace("dms")
    .setBasePath( getSearchRoot() )
    .add( Restrictions.eq( Criterion.JCR_PRIMARYTYPE, "mgnl:contentNode" ) );
    return criteria;
}

And the addCurrentRestrictions() method modifies the query to only return results from the currently-selected categories:

private Criteria addCurrentRestrictions(Criteria criteria) {
    String categoriesString = FCSSelectorUtil.getSelectorValue( CATEGORY_PARAMETER );
    if ( !StringUtils.isEmpty( categoriesString ) ) {
        HierarchyManager hm = MgnlContext.getHierarchyManager("data");
        for ( String category : StringUtils.split( categoriesString, CATEGORY_DELIMITER ) ) {
            Content categoryContent;
            try {
                categoryContent = hm.getContent( getFullPath( category ) );
                criteria.add( Restrictions.contains( "@categories", categoryContent.getUUID() ) );
            } catch (RepositoryException e) {
                log.warn( "Unable to locate category: " + category );
                criteria.add( Restrictions.contains( "@categories", "NO_MATCH" ) );
            }
        }
    }
    return criteria;
}

There are some more details, but that should give you a basic understanding of how the navigation paragraph works. In the next post, we’ll take a look at the search results paragraph and how it works.

Categories: magnolia-cms, resources

Resources: Capturing Data for Faceted Search in Magnolia

September 27, 2011 Leave a comment

As you Magnolia-watchers have doubtless noticed, there’s a new section on the Magnolia corporate website: Resources, a faceted search directory of media assets that are available through our site. The new page makes it easy to plow through a ton of available content to find what you want to see, and we’re pretty proud of it.

Since there’s some interesting stuff going on under the covers to allow us to aggregate slideshows, DMS documents, webpages, and videos into a common collection of content, I thought it might be worth sharing some of the details here.

We decided to use the Categorization Module to assign the media assets to particular facets. Because the module supports hierarchical categories, it makes it pretty straightforward to organize these how we like. In addition, we can often reuse these assignments just like we would normal categories — to generate category pages, RSS feeds, etc.

Once we’d set up our hierarchy of categories, we had to make it possible to capture all of the content we wanted to include. The Data Module allowed us to create data types for externally hosted videos (blip.tv or youtube) and slideshows (slideshare.net). This is pretty standard stuff, except for the fact that, in order to support multiple categories within the data workspace, we had to tweak the nodetype definitions. (This change is documented on the Categorization page.)

Next up, we want to be able to assign Categories to documents in the DMS. “But wait!” I hear you cry. “The DMS doesn’t support categorization!” With a little bit of Magnolia-Fu, however, it can. The Categorization module makes a tab control available for sharing, so all we have to do is add a reference to that control to our DMS dialog:

Finally, STK-based web pages already support Categorization, so we didn’t have to do anything there — they’re already good to go.

So with these few additions, we’re ready to start capturing data on media assets to show in our Resources section. Watch for my next blog post where I’ll get into the code behind the scenes.

Categories: magnolia-cms, resources