Supporting Content Type Viewing and Creation

Version 1

    This topic describes how you can add support to let people view and create instances of your content type.

     

    The topic uses a simple content type example to show how you can make it possible for people to view and create instances of your content type. While it's a good usability practice to have your content type behave in a way similar to other content types in the application, there are obviously many ways you can go.

     

    Note: The content type API is still a new feature that might change as developers provide feedback about it.

     

    The following illustration describes the layered way that your content type components interact to shuttle content type instance data between users and the database. Here's an overview of the processing path taken through your code:

     

    1. Someone requests to view an instance of the content type, such as by clicking a link in a list of instances on a space's browse page.
    2. The URL invokes a Struts action (mapped through Struts configuration), passing the instance's ID.
    3. The action calls a method of the manager class, requesting the instance by its ID.
    4. The manager class calls a method of the DAO class, which knows the SQL query needed to get data for the instance.
    5. Data is used to create an instance of your Jive object class.
    6. content_type_data_view_edit.png
    7. The Jive object is passed back up the chain to the action, where it is available to a FreeMarker template for display to the person who requested it.

     

    You'll likely write very similar code to support viewing and creating, but the execution paths are of course different. Here are the high-level development steps for each. You'll find more about each in the sections below.

     

    For viewing and displaying content type instances:

    • Define the database schema that will be needed to support your content type.
    • Write a DAO class to interact with the database to create, get, update, and delete instances.
    • Write a manager class that knows how to use application resources to handle instances.
    • Write action classes that create and retrieve instances.
    • Create FreeMarker templates for user interface.
    • Configure the actions with Struts XML that links them to the UI.
    • Add code that invokes the actions.

     

    Note: The code in this topic is from the Memo sample content type. You'll find the sample in the Jive public sample Subversion repository.

     

    Defining a Schema for Content Type Instance Data

    You can add the tables your content type will need by defining them in a schema.xml file you include in your plugin. By using the XML shape expected by SQLGen, a tool in the application, you can have your tables created when you deploy the plugin. For more information, see Accessing the Database from a Plugin.

     

    Here's an example of a SQLGen XML file that defines tables needed by the memo content type whose code is used in the following sections.

    <schema name="Jive Memo Schema">
    
        <table name="jiveMemo" description="Container table for memo data.">
            <column name="memoID" type="bigint" nullable="false" description="Memo ID."/>
            <column name="userID" type="bigint" nullable="false" description="User ID."/>
            <column name="subject" type="varchar" size="255" nullable="false" unicode="true"
                    description="Memo subject."/>
            <column name="body" type="varchar" size="1000" nullable="true" index_none="true"
                    unicode="true" description="Memo description."/>
            <column name="creationDate" type="bigint" nullable="false"
                    description="Create date of community."/>
            <column name="modificationDate" type="bigint" nullable="false"
                    description="Last modification date of community."/>
            <column name="containerType" type="int" nullable="false" description="Container Type."/>
            <column name="containerID" type="bigint" nullable="false" description="Container ID."/>
    
            <index type="primary" name="jiveMemo_pk" column="memoID"/>
        </table>
    </schema>
    

    Handling Content Type Instance Data

    You create a data access object (DAO) class to have a single place for nitty gritty database-related code -- SQL queries, transaction markup, and so on. From your DAO, you expose methods that require your manager class to have only the data it receives from the action context. That might include data from which to create a new instance, a unique identifier for an existing instance, an ID for the space (community) that contains instances, an ID for the user who created instances, and so on.

     

    The code in this section implements best practice suggestions that your own DAO should try to follow:

     

    • Ensure that all database actions that change data -- such as by creating, updating, or deleting rows -- execute in the context of a transaction. You can ensure this by adding the @Transactional annotation to those methods as shown in the following example. Given how important the integrity of database operations is, you shouldn't assume that transaction context will flow from calling methods. Instead, ensure that the DAO's work is in a transaction by adding the annotation.
    • Extend the JiveJdbcDaoSupport class to streamline your own code. This Jive class itself extends SimpleJdbcDaoSupport, which is part of Spring's JDBC abstraction framework. The framework wraps database actions, handling typical JDBC details such as connecting, preparing a statement, handling transactions, and so on. You need only provide your SQL statement and the parameter values it needs.

     

    The following example code is from a simple DAO class, and includes only functionality for viewing and creating one instance. The code here is designed to show the kinds of things you can do and the parts of the API that can make the job easier. Needless to say, your own code will become more complex the more functionality you want to support. This could include getting instances for a particular user or community, deleting instances, and so on.

     

    • Extend JiveJdbcDaoSupport and apply the @Transactional annotation.
    /**
     * A data access object (DAO) class for memo-related database interaction.
     */
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public class MemoDAO extends JiveJdbcDaoSupport {
    
        // Code follows.
    
    }
    • Add the SQL statement and method for getting a content type instance. The row mapper class is a handy way to instantiate the content type instance from query results. That code for that class follows in this section.
    // A constant for the SQL statement that gets an existing instance. The memoID
    // param will be provided in the method body.   
    private static final String GET_MEMO = "SELECT memoID, containerType, containerID, subject, body, "
        + "userID, creationDate, modificationDate FROM jiveMemo WHERE memoID = ?";
    
    /**
     * Gets the memo instance whose ID is specified.
     *
     * @param memoID The ID of the memo to get.
     * @return A Memo instance representing the specified memo.
     */
    public Memo getMemo(long memoID) {
        Memo memo;
        try {
            // Use a JDBC template to take advanced of the Spring framework API to wrap JDBC
            // details. Spring will use the GET_MEMO statement (along with the memoID parameter)
            // to prepare and make the query. It will use the row mapper (defined in this class),
            // to instantiate a Memo instance with data from the result set.
            memo = getSimpleJdbcTemplate().queryForObject(GET_MEMO, new MemoRowMapper(), memoID);
        }
        catch (EmptyResultDataAccessException e) {
            Log.warn("No memo with ID " + memoID + " found");
            return null;
        }
        catch (DataAccessException e) {
            Log.error(e);
            throw new DAOException(e);
        }
        return memo;
    }
    
    • Add the SQL statement and method for creating a content type instance. Notice that this method, which makes a change to the database, must be part of a transaction. Also, each of the arguments to the update method after the SQL statement will be used as values in the SQL INSERT statement.
    // A constant for the SQL statement that creates a memo instance.
    private static final String CREATE_MEMO = "INSERT INTO jiveMemo (memoID, containerType, containerID, "
         + "subject, body, userID, creationDate, modificationDate) " + "VALUES (?,?,?,?,?,?,?,?)";
    
    /**
     * Creates a memo instance in the database, using data from the specified
     * Memo Jive object instance.
     *
     * @param memo The Memo instance with data to insert.
     * @return The new instance.
     */
    @Transactional(readOnly = false, propagation = Propagation.MANDATORY)
    Memo create(Memo memo) {
        try {
            // Use the SequenceDAO instance to get a new ID in the sequence
            // of memo IDs.
            long id = sequenceDAO.nextID(MemoObjectType.MEMO_TYPE_ID);
            // Set creation and modification dates to now for the new instance.
            Date now = new Date();
            memo.setCreationDate(now);
            memo.setModificationDate(now);
            memo.setID(id);
            // Use a JdbcTemplate instance to do the update.
            getSimpleJdbcTemplate().update(CREATE_MEMO, id, memo.getContainerType(),
                    memo.getContainerID(),
                    memo.getSubject(), JAXPUtils.toXmlString(memo.getBody()),
                    memo.getUserId(),
                    memo.getCreationDate().getTime(),
                    memo.getModificationDate().getTime());
        }
        catch (DataAccessException e) {
            Log.error(e);
            throw new DAOException(e);
        }
        return memo;
    }
    • Add a way to get the SimpleJdbcTemplate. Also, define a row mapper for mapping query results into the content type instance. ParameterizedRowMapper is from the Spring framework.
    /**
     * Gets an instance of the template class that wraps common JDBC actions such as
     * opening a connection, preparing a statement, handling transactions, and so on.
     */
    public SimpleJdbcTemplate getSimpleJdbcTemplate() {
        return new JiveJdbcOperationsTemplate(super.getSimpleJdbcTemplate());
    }
    
    /**
     * A row mapper to map the parts of a query result set to parts of a
     * Memo instance. The mapRow method is called by the Spring framework
     * for each row in the result set.
     */
    private class MemoRowMapper implements ParameterizedRowMapper<Memo> {
    
        /**
         * Maps the columns in a result set row to methods in a Memo instance.
         * Parts of the result set are identified by the number of their
         * order in the SQL statement.
         */
        public Memo mapRow(ResultSet rs, int rowNum) throws SQLException {
            DbMemo memo = new DbMemo();
            memo.setID(rs.getLong(1));
            memo.setContainerType(rs.getInt(2));
            memo.setContainerID(rs.getLong(3));
            memo.setSubject(rs.getString(4));
            String bodyText = rs.getString(5);
            memo.setBody(WikiContentHelper.unknownContentToJiveDoc(bodyText));
            memo.setUserId(rs.getLong(6));
            memo.setCreationDate(new Date(rs.getLong(7)));
            memo.setModificationDate(new Date(rs.getLong(8)));
            return memo;
        }
    }
    

    Managing Content Type Instances

    A manager is the go-to class when it comes to content type instances. It knows where to go to find them, count them, create them, and delete them. It knows how to bring in other resources when needed, including content filters, caches, and your DAO class. A manager class does all of this on request, ideally without being aware of where the request is coming from; for example, it needn't know that it's one of your action classes that's asking for a list of the instances in a particular project.

     

    • Implement JiveManager and your own manager interface. Add code through which Spring can inject dependencies you'll need.
    /**
     * A class to manage memo instances, delegating database, cache, and event work.
     */
    @Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
    public class MemoManagerImpl implements JiveManager, MemoManager {
    
        private static final Logger log = Logger.getLogger(MemoManagerImpl.class);
    
        private MemoDAO memoDAO;
        protected QueryCacheManager queryCacheManager;
        private CommunityManager communityManager;
        private InvocableInterceptorManager interceptorManager;
        private EventDispatcher eventDispatcher;
    
        public MemoManagerImpl() {
        }
    
        // The following set methods are called by Spring to inject
        // dependencies.
    
        @Required
        public void setMemoDAO(MemoDAO memoDAO) {
            this.memoDAO = memoDAO;
        }
    
        @Required
        public final void setQueryCacheManager(QueryCacheManager queryCacheManager) {
            this.queryCacheManager = queryCacheManager;
        }
    
        @Required
        public void setCommunityManager(CommunityManager communityManager) {
            this.communityManager = communityManager;
        }
    
        @Required
        public void setInterceptorManager(InvocableInterceptorManager interceptorManager) {
            this.interceptorManager = interceptorManager;
        }
    
        @Required
        public void setEventDispatcher(EventDispatcher eventDispatcher) {
            this.eventDispatcher = eventDispatcher;
        }
    
    // Code follows.
    
    }
    
    • Write the method that your action will use to get an instance by its ID. This one's pretty simple, merely delegating to the corresponding method in the DAO class.
    /**
     * Gets the memo specified by the ID.
     *
     * @return The Memo instance corresponding to the ID.
     */
    public Memo getMemo(Long id) {
        return memoDAO.getMemo(id);
    }
    
    • Write the method that creates a single instance from data collected from the user by the action. This method collects the data it got and feeds it to the DAO class in a form it can use. Notice that the @Transactional annotation requires that a transaction context be propagated from this method -- including to the DAO's create method. That's a good practice to ensure data integrity through the changes.

    This one's a little more complex because of the work it does with interceptors and caches. Interceptors are installed by application administrators with the assumption that all of the content will be intercepted. So it's a conscientious practice to invoke those interceptors in your own content type instances. Application caches improve performance by caching the results of queries, reducing the number of trips to the database. But you're adding new data here (potentially changing those results), so you should clear the cache to make room for fresh query data.

    /**
     * Creates a new memo instance, using the container, subject, and body as
     * instance data. This method runs interceptors to
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public Memo createMemo(JiveContainer container, String subject, String body) throws RejectedException {
        DbMemo memo = new DbMemo();
        memo.setSubject(subject);
        memo.setBody(WikiContentHelper.unknownContentToJiveDoc(body));
        memo.setContainerID(container.getID());
        memo.setContainerType(container.getObjectType());
        memo.setUserId( JiveApplication.getEffectiveContext().getAuthenticationProvider().getJiveUser().getID() );
    
        // Interceptors check content for particular qualities, such as certain text.
      
        // A pre interceptor checks the content before it's added to the system, while
        // a post interceptor checked it afterward. If a pre interceptor finds what
        // it's looking for, it throws RejectedException. This method throws that exception
        // up to its caller (such as the CreateMemoAction).
        interceptorManager.invokeInterceptors(memo, JiveInterceptor.Type.TYPE_PRE);
      
        // Use the DAO class to create the memo in the database. The class returns
        // the new instance.
        Memo newMemo = memoDAO.create(memo);
      
        // A post interceptor typically acts on the content itself, such as to remove profanity.
        interceptorManager.invokeInterceptors(memo,JiveInterceptor.Type.TYPE_POST);
      
        // Adding a new memo could of course change the result of queries for memos,
        // or queries for the content in a container that includes the memo.
        // Clearing the query cache here ensures that users get fresh data.
        queryCacheManager.clearContainerCaches(container);
        queryCacheManager.getQueryCache().remove(MemoObjectType.MEMO_TYPE_ID, -1);
    
        //
        fireEvent(new MemoEvent(MemoEvent.Type.ADDED, new EntityDescriptor(newMemo), container, null));
    
        return newMemo;
    }
    

    Configuring Spring-Injected Dependencies

    To configure Spring to inject the dependencies you need, be sure to add those to the spring.xml file in your plugin.

    <bean id="memoManagerImpl" class="com.jivesoftware.clearspace.plugin.test_dynamic.MemoManagerImpl"
          parent="jiveManager">
        <property name="memoDAO" ref="memoDAO"/>
        <property name="queryCacheManager" ref="queryCacheManager"/>
        <property name="communityManager" ref="communityManagerImpl"/>
        <property name="interceptorManager" ref="interceptorManagerImpl"/>
        <property name="eventDispatcher" ref="jiveEventDispatcher"/>
    </bean>
    

    Handling User Actions

    In your action classes, you'll provide a way for the UI to connect to the DAO and manager code you've written. Your user interface will use the action code, setting and getting values that are passed to and from the manager by the action.

     

    The sections here focus on the UI for viewing and creating content type instances. The actions that display these can be invoked from any of a number of places. There's an example of one way to invoke them later in this topic.

    Providing Support to View Content

    To give users a way to view instance content, you'll write a Struts action class that can retrieve instance data (via the manager and DAO). Using Struts configuration, you'll connect your user interface FTL file so that it can display retrieved data.

    Creating the Action Class

    The view action class needs a way to receive the ID representing the requested instance -- a setter that Struts can use to pass in the value. The action will use that ID to ask your manager class for the memo instance, then make the instance available for display in your user interface.

     

    The following code also includes a setter for Spring to use to inject an instance of the manager class. There's no entry in the spring.xml file for this action class, and there needn't be -- for actions in the application's Struts context, a setter alone is all that's needed to support "autowiring".

    /**
     * Provides logic to view an instance of a memo content type.
     */
    public class ViewMemoAction extends JiveActionSupport {
    
        private static final Logger log = LogManager.getLogger(ViewMemoAction.class);
    
        // Fields to hold instances from Spring and Struts.
        private MemoManager memoManager;
        private Long memoID;
        private Memo memo;
    
        /**
         * Called by Spring to inject an instance of the memo manager class.
         * Classes defined as Spring beans are automatically injected
         * into an action class configured for Struts. This action class
         * needn't be listed in the spring.xml file in order to have
         * this manger instance injected -- it needs only provide the setter.
         *
         * @param memoManager The injected instance.
         */
        public void setMemoManager(MemoManager memoManager) {
            this.memoManager = memoManager;
        }
    
        /**
         * Called to assign the ID for the requested memo from the
         * execution context.
         *
         * @param memoID The requested memo's ID.
         */
        public void setMemoID(Long memoID) {
            this.memoID = memoID;
        }
    
        /**
         * Gets the memo whose content should be displayed to the user. This
         * instance is used in the FTL file to display the memo's subject
         * and ID.
         *
         * @return The requested memo instance.
         */
        public Memo getMemo() {
            if (memo == null) {
                if (memoManager != null) {
                    memo = memoManager.getMemo(memoID);
                }
            }
            return memo;
        }
    
        /**
         * Gets the body for this memo instance. Here, the body content is cleaned
         * up a bit so that it can be displayed well via the FTL file for
         * viewing a memo.
         *
         * @return The body content as a string.
         */
        public String getBody() {
            return RenderUtils.renderToText(memo, getGlobalRenderManager());      
        }
    
        /**
         * Gets the container for this instance.
         *
         * @return The container instance.
         */
        public JiveContainer getContainer() {
            try {
                return jiveObjectLoader.getJiveContainer(memo.getContainerType(), memo.getContainerID());
            }
            catch (NotFoundException e) {
                log.error("Could not load container with type: " + memo.getContainerType() +
                          " and id: " + memo.getContainerID());
                return null;
            }
        }
    }
    

    Adding User Interface

    The user interface for displaying the instance's content will use the get methods you defined in your action to retrieve the values it needs. The following code is edited from the sample itself in order to keep things simple. But some of the removed code is important for rendering this content type in a way that's similar to others. Be sure to look at the sample.

    <html>
    <head>
        <title>Memorandums</title>
      
        <#-- Include FTLs that have code needed later in the template. -->
        <#include "/template/global/include/comment-macros.ftl" />
        <#include "/template/global/include/tag-macros.ftl" />
    
    <!-- BEGIN header & intro  -->
    <#-- A simple boilerplate heading using CSS style rules defined in memo.css. -->
    <div id="jive-body-intro">
        <div id="jive-body-intro-content" class="jive-memo-header">
            <h1>Memorandum</h1>
        </div>
    </div>
    <!-- END header & intro -->
    
    <!-- BEGIN main body -->
    <#-- div elements that echo the structure of other Jive content display pages. -->
    <div id="jive-body-main">
    
        <!-- BEGIN main body column -->
         <div id="jive-body-maincol-container">
    
            <div id="jive-body-maincol">
    
                <#include "/template/global/include/form-message.ftl"/>
    
                <!-- BEGIN Memo view -->
    
                <#--
                    Display memo content retrieved from the ViewMemoAction class. Simply
                    displaying the values inside a pre tag is probably the simplest way.
                    Instance data can of course be structured much more elaborately.
                -->
                <pre>
    
        ID: ${memo.getID()?c}
        Subject: ${memo.subject?html}
        Body: ${body?html}
    
    
                  
                </pre>
    
                <!-- END Memo view -->
    
            </div>
        </div>
    </div>
    
    </body>
    </html>
    

    Configuring the Action

    You connect the action class and FreeMarker template through your Struts configuration file (struts.xml in your plugin JAR). Here's a simple version. When the view-memo action is called, Struts executes action class code. The execution result is passed into the FTL file for display to the user.

    <action name="view-memo"
        class="com.jivesoftware.clearspace.plugin.test_dynamic.action.ViewMemoAction">
        <result name="success">/plugins/memo-type-example/resources/view-memo.ftl</result>
    </action>
    

    Providing Support to Add Content

    To give users a way to create new instances, you'll write a Struts action class that can receive data entered by the user (via your FreeMarker user interface) and pass it to your manager and DAO classes to be added to the database. Using Struts configuration, you'll connect your user interface FTL file so that it can shuttle data to your action class.

    Creating the Action Class

    The create action class includes setters that Struts can use to pass in values set by the user. The action will pass those values to your manager class to create the memo instance. Along the way the code validates the filters the values, ensuring that what was entered by the user is suitable for the content type and for the community. Successfully creating the instance will redirect the user back to viewing the newly created instance (the action described above).

     

    As with the view action, this one uses autowiring to have Spring inject the manager instance.

    public class CreateMemoAction extends JiveActionSupport implements CreateEditTaggableTypeAction {
    
        protected static final Logger log = LogManager.getLogger(CreateMemoAction.class.getName());
    
        /* request parameters */
        private String subject;
        private String body;
        private String cancel;
    
        private JiveContainer container;
        private int containerType;
        private long containerID;
        private MemoManager memoManager;
    
        private String refererURL;
    
        /**
         * Called by Spring to inject an instance of the memo manager class.
         * Classes defined as Spring beans are automatically injected
         * into an action class configured for Struts. This action class
         * needn't be listed in the spring.xml file in order to have
         * this manger instance injected -- it needs only provide the setter.
         *
         * @param memoManager The injected instance.
         */
        public final void setMemoManager(MemoManager memoManager) {
            this.memoManager = memoManager;
        }
    
        public JiveContainer getContainer() {
            return container;
        }
        public void setContainer(JiveContainer container) {
            this.container = container;
        }
    
        public int getContainerType() {
            return containerType;
        }
        public void setContainerType(int containerType) {
            this.containerType = containerType;
        }
    
        public long getContainerID() {
            return containerID;
        }
        public void setContainerID(long containerID) {
            this.containerID = containerID;
        }
        // Accessors called by Struts to pass in values
        // entered by the user in the user interface.
    
        public String getSubject() {
            return subject;
        }
        public void setSubject(String subject) {
            this.subject = subject;
        }
    
        public String getBody() {
            return body;
        }
        public void setBody(String body) {
            this.body = body;
        }
    
        // Accessors called by Struts to handle the user's
        // canceling instance creation.
    
        public String getCancel() {
            return cancel;
        }
        public void setCancel(String cancel) {
            this.cancel = cancel;
        }
    
        /**
         * Validates each of the field values entered by the user (subject, body, and tags). For
         * subject and body, this method merely checks to see if there's a non-empty string in the
         * field. For the tags, it delegates to the TagActionUtil class, which splits the space-separated
         * list of tags into a Set.
         *
         * Where field errors are found, they're added to the execution context, where they can be
         * retrieved in the FTL file for display to the user. Note that the error text defined here
         * is specified in the plugin_i18n.properties file.
         */
        public void validate() {
            if ( !StringUtils.isEmpty( getCancel() ) ) {
                return;
            }
    
            if (StringUtils.isEmpty(subject)) {
                addFieldError("subject", getText("memo.create.error.subject" ));
            }
    
            if (StringUtils.isEmpty(body)) {
                addFieldError("body", getText("memo.create.error.body" ));
            }
        }
    
        @Override
        public String execute() {
    
             // If the user canceled, set the referrer URL to point to the
             // container in which this instance was being saved. The user
             // will be redirected there.
            if ( !StringUtils.isEmpty( cancel ) ) {
                refererURL = JiveResourceResolver.getJiveObjectURL( container, false );
                return "cancel";
            }
    
            Memo memo;
    
            try {
                // Using the information received from the user via the
                // FTL, create an instance in the database.
                memo = memoManager.createMemo(container, subject, body);
            }
            // A problem was found by one of the PRE interceptors, preventing the
            // from being added to the database.
            catch (RejectedException e) {
                log.error("Memo " + subject + " rejected for create", e);
                return UNAUTHORIZED;
            }
    
            // Set the referrer URL to one that will display the newly created
            // instance.
            refererURL = "/view-memo.jspa?memoID=" + memo.getID();
    
            return SUCCESS;
        }
    
        @Override
        public String cancel() {
            refererURL = JiveResourceResolver.getCurrentBaseUrl();
            return CANCEL;
        }
    
        public String getRefererURL() {
            return refererURL;
        }
    }
    

    Adding User Interface

    The user interface for creating an instance will use the set methods you defined in your action to pass in values the user enters. Again, this is edited code. Be sure to look at the sample for the full story, including best practices.

    <#import "/template/global/include/jive-form-elements.ftl" as jiveform/>
    
    <#include "/template/global/include/tag-macros.ftl" />
    
    <html>
    <head>
    
        <#--
            Give the page a title and use CSS style rules included with the application.
        -->
        <title>Create Memo<#if container?exists><@s.text name="global.colon"/> ${container.name?html}</#if></title>
        <script type='text/javascript'>
            function doCancel() {
                document.getElementById('cancel').value='true';
                <!--alert(document.getElementById('cancel').value);-->
                document.forms['form-create'].submit();
            }
        </script>
    </head>
    
    <body class="jive-body-formpage jive-body-formpage-project">
    
    
    <!-- BEGIN header & intro  -->
    <#--
        Build a heading that includes the container name, such
        as "Create new Memo in Human Resources."
    -->
    <div id="jive-body-intro">
        <div id="jive-body-intro-content">
            <h1><@s.text name="memo.create_new.gtitle"><@s.param>${container.name?html}</@s.param></@s.text></h1>
    
            <p><@s.text name="memo.create.info" /></p>
        </div>
    </div>
    <!-- END header & intro -->
    
    
    <!-- BEGIN main body -->
    <div id="jive-body-main">
    
        <!-- BEGIN main body column -->
        <div id="jive-body-maincol-container">
            <div id="jive-body-maincol">
    
                <#-- Include the template used to display error messages. -->
                <#include "/template/global/include/form-message.ftl"/>
    
                <!-- BEGIN form container block -->
                <div class="jive-standard-formblock-container">
                    <div class="jive-standard-formblock">
    
                    <#if (container?exists)>
                    <form action="<@s.url action='memo-create'><@s.param name='container' value='${container.ID?c}'/><@s.param name='containerType' value='${containerType?c}'/></@s.url>" method="post" name="form-create">
                    <#else>
                    <form action="<@s.url action='memo-create'/>" method="post" name="form-create">
                    </#if>
                    <@jive.token name="memo.create.${containerType?c}.${containerID?c}" />
                        <input type="hidden" name="cancel" id="cancel"/>
    
                        <!-- BEGIN jive-form -->
                        <div class="jive-form">
    
                            <#--
                                Create form rows to hold the memo's subject and body. The
                                macroFieldErrors macro included from Jive's form-message.ftl
                                file will retrieve field errors added by the action during
                                validation and display them to the user.
                            -->
                            <div class="jive-form-row">
                                <#--
                                <div class="jive-form-label">
                                    <label for="create-subject"><@s.text name="memo.form.subject.label" /></label>
                                    <@macroFieldErrors name="subject"/>
                                </div>
                                <div class="jive-form-element">
                                    <input id="create-subject" type="text" name="subject" class="jive-memo-create-subject" value="${subject!?html}" size="20" />
                                    <p><@s.text name="memo.form.subject.text" /> <i>(required)</i></p>
                                </div>
                            </div>
    
                            <div class="jive-form-row">
                                <div class="jive-form-label">
                                    <label for="create-body"><@s.text name="memo.form.body.label" /></label>
                                    <@macroFieldErrors name="body"/>
                                </div>
                                <div class="jive-form-element">
                                    <textarea id="create-body" name="body" cols="50" rows="5" class="jive-memo-create-body">${body!?html}</textarea>
                                    <p><@s.text name="memo.form.body.text" /> <i>(required)</i></p>
                                </div>
                            </div>
    
                            <#--
                                Create a form button for submitting the new memo content.
                            -->
                            <div class="jive-form-row">
                                <div class="jive-form-label">   </div>
                                <div class="jive-form-element">
                                    <input type="submit" name="save" value="<@s.text name="global.save" />"/>
                                    <button type="button" onclick="javascript:doCancel();">Cancel</button>
    
                                    <#if !action.isCreating()>
                                    <input type="submit" name="method:actionDelete" value="<@s.text name='memo.form.delete'/>"/>
                                    </#if>
                                </div>
                            </div>
    
                        </div>
                        <!-- END jive-form -->
                    </form>
                    </div>
                </div>
                <!-- END form container block -->
            </div>
        </div>
        <!-- END main body column -->
    </div>
    <!-- END main body -->
    </body>
    </html>
    

    Configuring the Action

    An action stanza in your struts.xml file connects your create action with the UI. In the following example, the memo-create action, Struts executes action class code. Based on this configuration, Struts invokes your action and redirects the user's browser based on the response. The first invocation displays your create UI, while cancellation and success redirect to the referrer URL set in the action class.

    <action name="memo-create"
        class="com.jivesoftware.clearspace.plugin.test_dynamic.action.CreateMemoAction">
        <interceptor-ref name="defaultStack"/>
        <result name="input">/plugins/memo-type-example/resources/create-memo.ftl</result>
        <result name="cancel" type="redirect">${refererURL}</result>
        <result name="success" type="redirect">${refererURL}</result>
    </action>
    

    Invoking the Actions from the User Interface

    Of course, all of this is pointless if you don't give the user a way to get at it. You can (and will) do that in many ways -- through lists of content (recent and popular), through the New menu, and so on. The following example is from code to display a list of instances on the content type's tab for a space or sub-space.

    <div class="jive-content-block-container jive-content-block-docapproval">
        <#--
            Display a heading such as "All Memos in Human Resources"
        -->
        <h3 class="jive-content-block-header"><@s.text name="community.tab.memo.header"/> <@s.text name="global.in"/> ${ action.community.name }</h3>
    
        <#-- Create a variable from the list of memos provided by the action. -->
        <#assign memos = action.contentInfoProvider.getMemos(action.community) >
        <#--
            If there are memo instances to list, then use the
            jiveContentList macro to list them here. That macro
            assumes that your content type implements the RecentContentInfoProvider
            interface to return the action URL for viewing an instance.
        -->
        <#if memos?exists && memos.hasNext()>
            <@jive.jiveContentList content=memos />
    
        <#--
            If there aren't any memo instances to list, then display a
            way to create one. The jiveEmptyContentList macro is designed
            for cases when, yes, the content list is empty. The macro
            assumes that your Jive object type implements ContentObjectType
            and ContentObjectTypeInfoProvider. That interface's
            getCreateNewFormRelativeURL method returns the action URL for
            creating a new instance of your content type.
        -->
        <#else>
            <!-- BEGIN content list -->
            <div class="jive-content-block">
    
                <!-- BEGIN content results -->
                <div id="jive-content-results">
                    <#-- Provide a link for creating a new instance. -->
                    <@jive.jiveEmptyContentList container=community showTypeExclusively="memo"/>
    
                </div>
                <!-- BEGIN content results -->
    
            </div>
            <!-- END content list -->
        </#if>
    </div>