0

After some discussions with a coworker, an idea took shape for a Velocity based alternative to framework loaders such as the JavaScript based RequireJS. The idea is that you can upload common or frequently used files for your templates to your dotCMS host or shared host and pull those en masse with a simple macro. Welcome to the Resource Loader (GitHub project page).

A Web Resource file asset

A Web Resource file asset

First, how it works (jump down the page if you want to see an explanation of how it’s built). When you install the plugin, it does two specific things. First, it creates a macro called loadResource(). Then, it creates a File Asset structure called Web Resource (this is configurable in the plugin.properties file, if you want to change it). From there, you upload files that you want to be loader enabled using that structure, and give them a common resource ID. So, let’s say you use Foundation as the basis for your sites. You can upload the core CSS and JS files for that, and give them all the resource ID ‘foundation.’ If you use the entire Foundation library, that’s at least a couple CSS files and several JS files. Then, in your template, all you have to do is run the code.

## CALL ALL OUR LOCALLY STORED FOUNDATION LIBRARY FILES
#loadResource('foundation')

Even better, out of the box the macro supports any JS library in the Google Hosted Libraries without needing to upload a local version at all. So, want to add jQuery on a fresh site? Just drop in:

## GET JQUERY FROM GOOGLE'S CDN IF IT ISN'T AVAILABLE LOCALLY
#loadResource('jquery')

If you prefer CDN based resources, when you upload your files, just include an external link to the CDN version and set it to default to that link.

There are several other options included as well, which you can just set as Velocity variable prior to calling the macro:

  • $htmlDebug (bool): Prints debug info out on each library as an HTML comment in the source.
  • $commentBreaks (bool): Show HTML comments before and after loading the library files. Good for seeing the start and end of each library, especially if you’re loading several in a row.
  • $resourceCache (int): Cache time for resources, in seconds. By default, once you call a resource, it will use dotcache() to save that for a day.
  • $boxJs(bool): Enables the 3rd party caching proxy/CDN BoxJS. This will take all the JS files for your resource library and combine and minify them, then serve it from their Amazon based CDN as a single file. Only works when used on local files.
    • $boxJsMinify (bool): Enabled by default. Set to false to disable minification if it causes issues for your file.
    • $boxJsCache (bool):  Enabled by default. Set to false to disable caching on the BoxJS CDN.
    • $boxJsVersion (str): Allows you to create versioned packages on their CDN if you ever want to roll back for something.
    • $boxJsAppend (bool): Enabled by default. Set to false to spit out the JS right where you place the macro (otherwise it’s added to the end of the page).
    • $boxJsDefer (bool): Set to true if you want manual control over when, where, or how your JS is brought into the page. Requires you to manually call box.get(); in JavaScript on the page where you want the code at.
  • $boxCss(bool): Like BoxJS above, this enables the 3rd party caching proxy/CDN BoxCSS. This will take all the CSS files for your resource library and combine and minify them, then serve it from their Amazon based CDN as a single file. Only works when used on local files.
    • $boxJsMinify (bool): Enabled by default. Set to false to disable minification if it causes issues for your file.
    • $boxJsCache (bool):  Enabled by default. Set to false to disable caching on the BoxCSS CDN.
    • $boxJsVersion (int)Allows you to create versioned packages on their CDN if you ever want to roll back for something.

So knowing all this, a more advanced example might be:

## ENABLE DEBUGGING
#set($htmlDebug = true)
## DISABLE COMMENT BREAKS
#set($commentBreaks = false)
## TURN DOWN CACHE TIME TO ONE HOUR
#set($resourceCache = 3600)
## RUN ALL OUR CSS THROUGH BOXCSS
#set($boxCss = true)
## PULL IN OUR CUSTOM CSS LIBRARIES FOR THE SITE
#loadResource('all-the-css')

That would be a good way, if you had a dozen common CSS files you use, to create a common shared library that gets combined and cached for an hour so that the end user ends up with just a single HTTP request for all twelve files.

How It’s Built

For those of you interested in doing something like this, the basic setup for the plugin is pretty easy. The hardest part is deploying a structure programatically, and even that’s pretty easy once you’ve seen the code. There are four files specifically you need to pay attention to for the most part on this:

  1. MANIFEST.MF
  2. conf/plugin.properties
  3. conf/macros-ext.vm
  4. src/com/learndotcms/plugins/resourceloader/ResourceLoaderPluginDeployer.java

The only thing to remember for the MANIFEST.MF file is line two, where you need to define the deployer class that needs to run when the plugin is installed. In this case, the class that builds our Web Resource structure.

Manifest-Version: 1.0
Deploy-Class: com.learndotcms.plugins.resourceloader.ResourceLoaderPluginDeployer
Plugin-Name: Resource Loader Plugin
Plugin-Version: 1.0.121022
Author: Michael Fienen ( fienen@gmail.com - http://learndotcms.com/ )

For our properties, we added three user configurable values, for the shared host ID, and the structure name and Velocity variable name.

##The default for the reload.force is true.  If it is set to false the properties will only load once from the file.
##After that they will be stored and maintained in the database.  If true every time the server restart the properties
##will be cleared and reloaded.
reload.force = true

## Set the Structure and Velocity variable name for the resource loader's File Asset structure (DO NOT LEAVE BLANK)
webResourceName = Web Resource
webResourceVarName = WebResource

## Set the identifier for your shared host if used for sharing common elements between hosts (Uncomment it and give it a value if needed)
## sharedHostId =

Now, due to its size, I’m not going to source the entire macros-ext.vm file here. Instead, you’re welcome to download it or view it on GitHub. But, the basic structure holds true for it as with all macros. Also, I take into account the shared host ID if necessary. That uses a special viewtool ($pluginapitool) to get the property value from the plugin, which you can see on line 5 below.

## BASIC MACRO STRUCTURE
## BLAH, BLAH, BLAH, COMMENTS EXPLAINING MACRO AND USAGE
#macro(loadResource $library)
    ## SET UP THE SHARED HOST FOR THE FILE QUERY, IF PROVIDED
    #if($UtilMethods.isSet($pluginapitool.loadProperty("com.learndotcms.plugins.resourceloader","sharedHostId")))
        #set($sharedHost = " conhost:${pluginapitool.loadProperty('com.learndotcms.plugins.resourceloader','sharedHostId')}")
    #end

    ...do stuff using the passed variable $library when someone calls #loadResource('foobar')...
#end

Lastly, the deployer class. Like with the macro, I’m just going to sample this for you. The full code is here.

// This is all the overhead and such for this particular plugin
package com.learndotcms.plugins.resourceloader;

import com.dotmarketing.cache.FieldsCache;
import com.dotmarketing.business.DotStateException;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotRuntimeException;
import com.dotmarketing.plugin.business.PluginAPI;
import com.dotmarketing.plugin.business.PluginAPIImpl;
import com.dotmarketing.plugin.PluginDeployer;
import com.dotmarketing.portlets.structure.model.Field;
import com.dotmarketing.portlets.structure.model.Field.DataType;
import com.dotmarketing.portlets.structure.model.Field.FieldType;
import com.dotmarketing.portlets.structure.model.Structure;
import com.dotmarketing.portlets.structure.factories.FieldFactory;
import com.dotmarketing.portlets.structure.factories.StructureFactory;
import com.dotmarketing.util.Logger;

// Set up the deployer class name
public class ResourceLoaderPluginDeployer implements PluginDeployer {
  // This is what we do when the plugin is deployed
  public boolean deploy() {
    // Set up our Web Resource Structure. Note we're using the pluginAPI again for the property value.
    String structureName = pluginAPI.loadProperty("com.learndotcms.plugins.resourceloader", "webResourceName");
    String structureVarName = pluginAPI.loadProperty("com.learndotcms.plugins.resourceloader", "webResourceVarName");
    Structure st = new Structure();
    st.setName(structureName);
    st.setDescription("Common CSS/JS/VTL Files used for sharing and building templates.");
    st.setFixed(false);
    st.setSystem(false);
    st.setStructureType(Structure.STRUCTURE_TYPE_FILEASSET);
    st.setVelocityVarName(structureVarName);
	try {
		StructureFactory.saveStructure(st);
	} catch (Exception e) {
		Logger.error(this.getClass(), e.getMessage(), e);
		throw new DotRuntimeException(e.getMessage(), e);
	}

	Field field1 = new Field("Host or Folder", Field.FieldType.HOST_OR_FOLDER, Field.DataType.TEXT, st, true, false, true, 1, "", "", "", true, false, true);
	field1.setVelocityVarName("hostFolder");
	try {
		FieldFactory.saveField(field1);
	} catch (Exception e) {
		Logger.error(this.getClass(), e.getMessage(), e);
		throw new DotRuntimeException(e.getMessage(), e);
	}

	Field field2 = new Field("File Asset", Field.FieldType.BINARY, Field.DataType.BINARY, st, true, false, false, 2, "", "", "", true, false, false);
	field2.setVelocityVarName("fileAsset");
	try {
		FieldFactory.saveField(field2);
	} catch (Exception e) {
		Logger.error(this.getClass(), e.getMessage(), e);
		throw new DotRuntimeException(e.getMessage(), e);
	}

        // And we go on like this for the other eleven fields in the structure

        // Clear the field cache in dotCMS, just in case
	FieldsCache.clearCache();

    return true;
  }

  // We don't need to do anything when we redeploy
  public boolean redeploy(String version) {
    return true;
  }
}

That’s the basic story here. All it needs is to be uploaded and deployed then, and you’re off and running. You can deploy any of the four core structure types (content, widget, form, file) this way, and include any of the standard field types.

No comments yet.

Leave a Reply