[an error occurred while processing this directive]
> developer > web app development
Implementing Saved Searches with the ePortal Content Management API
by Dmitry Goldenberg, Sr. Software Engineer, Novell
Date Created: 2001-06-11 14:14:00.000
  Introduction
  Save search criteria as a named search per user
  List the user's saved searches
  Load a saved search
  Execute a saved search
  Reset/modify a search
  Rename a search
  Delete a search
  Attachment 1
  Attachment 2
introduction
Saved searches are commonplace on most Web sites that deal with large amounts of searchable data, especially if the data can be searched against many criteria. Take for example a real estate site or a movie database. In each of these cases, the data is complex enough to frustrate users if there is no support for saving user searches as named, reloadable entities.

In its present form, the Content Management API offers the necessary means for developers to implement the saved searches mechanism. Let's consider the real estate example as we walk through the design and implementation of this functionality. (The following techniques are also quite handy in dealing with virtually any other document type that you might come across.)

Assume our site must allow users to search various property types. As the site's developers, we should consider the following list of search criteria:

  • State
  • County
  • Town(s) of interest
  • Property type(s) (e.g. single-family, condo/townhouse, mobile home, or land) Price range
  • Minimum number of bedrooms
  • Minimum number of bathrooms
  • Minimum square footage


We should also consider more advanced criteria such as:

  • General features (e.g. property age, handicap features, etc.)
  • Interior features (e.g. central air, gas heat, hardwood floors, etc.)
  • Exterior features (e.g. garage, acreage, etc.)
  • Amenities (e.g. swimming pool, tennis court, etc.)


With the criteria selected, we can now move into the design stage. We'll start by figuring out data representation and storage. Let's assume the following design requirements:

  • Each property will be represented by a document stored in the CM system; the document's type will be the RealEstateProperty doctype.
  • Primary criteria such as state, county, town and property type will be represented by CM categories; each property document will be assigned to these categories.
  • All other criteria will be represented by the following extension metadata fields:
Extension metadata field nameField data type
Number of bedroomsInteger
Hardwood floorsBoolean
Swimming poolBoolean
etc.etc.


We'll also need to implement the following functions to create a robust user interface:



The proposed infrastructure will store each search as an XML document that complies with the ePortal's content_search.dtd, or the DTD for XML representing content management document queries. Each such document will be of type SavedSearch. Note the following points:

  • Each document ID will uniquely identify each search within the system.
  • The document Author field will associate the search with its creator.
  • The document Title field can be used to store the user-provided name for the search.
save search criteria as a named search per user
This simply requires us to create a document and save it to the CM system.


EbiAddDocumentParams params = cmgr.createAddDocumentParams();



// Get to the portal object

      EbiPortal portal = com.sssw.portal.core.EboPortalFactory.getPortal();

      // Get to the Content Manager

      EbiContentManager cmgr =

            (EbiContentManager)portal.getComponentManager().getPortalManagerInterface(

             EbiPortalConstants.PORTAL_CONTENT_MGR);

      // Get our context

      EbiContext ctx = EboFactory.createEbiContext(request, response);

// Get our SavedSearch doctype

EbiDocType type = cmgr.getDocumentTypeByName(ctx, "SavedSearch");

// Figure out the parameters for the new document

EbiAddDocumentParams params = cmgr.createAddDocumentParams();

      params.setDocTypeID(type.getTypeID());

      params.setAuthor("Bob Smith");

      params.setTitle("MA_Townhouses");

      params.setMimeType("text/xml");

      params.setContent(content);

// Add the document

      EbiDocument document = contentManager.addDocument(ctx, params);



Note that the last step only creates the initial revision (version) of the document. At some point, we'll want to publish the document or always pull out the latest version when it's time to load the search.
list the user's saved searches
This requires a search for all documents that have the type SavedSearch and the Author identified as our user (e.g. Bob Smith).


// Create a Document Query Object

EbiDocQuery docQuery =

   (EbiDocQuery)cmgr.createQuery(EbiDocQuery.DOC_QUERY);



// We're interested in SavedSearch'es

EbiDocType type = cmgr.getDocumentType(ctx, "SavedSearch");

EbiQueryExpression queryExpr = docQuery.whereDocTypeID(

    type.getDocTypeID(), EbiDocQuery.ROP_EQUAL, false);



// The author we're interested in is Smith

EbiQueryExpression authorExpr = docQuery.whereAuthor("Bob Smith",

    EbiDocQuery.ROP_EQUAL, false);



// We want saved searches whose author is "Bob Smith"

queryExpr.andExpression(authorExpr);



// The WHERE clause is ready, set it into the Query

docQuery.setWhere(queryExpr);

// We want the resulting list to be sorted by title (search name) in ascending order

docQuery.orderByTitle(true);



// Execute the query

Collection results = cmgr.findElements(ctx, docQuery);



Note that the Collection returned in the last step is a set of EbiDocument's, and each document represents a saved search.
load a saved search
This is a standard document retrieval procedure.


EbiDocument doc = cmgr.getDocument(ctx, docID);



where docID is the document ID uniquely identifying the search we're interested in. Once the metadata object (EbiDocument) is obtained, we can get the actual XML document associated with the docID (depending on whether we get the published version or the latest version):


// Get the published version

// (use 'true' for 'get the content bytes')

EbiDocContent content = cmgr.getContent(ctx, docID, true);

byte[] data = content.getData();

String xmlSearch = new String(data);

// Now, parse the XML and repopulate the search UI

or

// Get the latest search document version

EbiDocVersion version = cmgr.getLatestDocumentContentVersion(ctx, docID, true);

byte[] data = version.getData();

String xmlSearch = new String(data);

// Now, parse the XML and repopulate the search UI

execute a saved search
Once the search XML is retrieved, as in Step 3, it can be converted into the document query object and run again.


EbiDocQueryConverter conv = cmgr.getQueryConverter();

EbiDocQuery docQuery = conv.queryDefToObject(ctx, xmlSearch);

// Execute the query

Collection results = cmgr.findElements(ctx, docQuery);

reset/modify a search
This is a standard document modification procedure. We'll need to check the document, modify its contents, then check in the changes.


cmgr.checkoutDocument(ctx, docID);

cmgr.checkinDocument(ctx,

                           docID,

                           "text/xml",

                           newData, // byte[]

                           "next version", // comment, if any...

                           false); // whether we want to keep the doc checked out or not

rename a search
To do this, we'll simply need to modify the title of the search document.


cmgr.checkoutDocument(ctx, docID);

EbiDocument doc = cmgr.getDocument(ctx, docID);

doc.setTitle("MA_Townhouses_And_Condos"); // supply the new title

cmgr.updateDocument(ctx, doc, false); // false for 'do not keep doc checked out'

delete a search
This is simple.


cmgr.removeDocument(ctx, docID);

attachment 1
The following is the ePortal's DTD for XML representing content management document queries (content_search.dtd).


<!ELEMENT content-search (selected-props?, sort-order?, where-clause?)>



<!ELEMENT selected-props (prop-name*)>

<!ELEMENT sort-order (order-by*)>

<!-- Ascending order is assumed if the 'ascending' node is missing. -->

<!ELEMENT order-by (prop-name, ascending?)>

<!ELEMENT where-clause (and | or | not | eq | lt | leq | gt | geq | in | between | like |

startsWith | endsWith | equalsIgnoreCase | isNull)>

<!ELEMENT and (and | or | not | eq | lt | leq | gt | geq | in | between | like | startsWith |

endsWith | equalsIgnoreCase)*>

<!ELEMENT or  (and | or | not | eq | lt | leq | gt | geq | in | between | like | startsWith |

endsWith | equalsIgnoreCase)*>

<!ELEMENT not (and | or | not | eq | lt | leq | gt | geq | in | between | like | startsWith |

endsWith | equalsIgnoreCase)>



<!ELEMENT eq (var, val, type?, op, negate?)>

<!ELEMENT lt (var, val, type?, op, negate?)>

<!ELEMENT leq (var, val, type?, op, negate?)>

<!ELEMENT gt (var, val, type?, op, negate?)>

<!ELEMENT geq (var, val, type?, op, negate?)>

<!ELEMENT in (var, values, type?, negate?)>

<!ELEMENT between (var, val, val, type?, negate?)>

<!ELEMENT like (var, val, type?, negate?)>

<!ELEMENT startsWith (var, val, type?, negate?)>

<!ELEMENT endsWith (var, val, type?, negate?)>

<!ELEMENT equalsIgnoreCase (var, val, type?, negate?)>

<!ELEMENT isNull (var, type?, negate?)>



<!--

     The following property names are available:

		AUTHOR

		CREATED

		DOCABSTRACT

		DOCID

		DOCNAME

		DOCTYPEID

		DOCTYPENAME

		EXPIRATIONDATE

		FOLDERID

		LOCKEDBY

		PARENTDOCID

		PUBLISHDATE

		STATUS

		SUBTITLE

		TITLE

     Note: DOCID is always selected, by default.

-->

<!ELEMENT prop-name (#PCDATA)>



<!--

	A variable may be a prop-name or CATEGORYID or FIELDID or FIELDVALUE.

	Variables are divided into string ones and non-string ones.

	All variables are string ones except the following:

		CREATED, PUBLISHDATE, EXPIRATIONDATE

-->

<!ELEMENT var (#PCDATA)>



<!ELEMENT val (#PCDATA)>

<!ELEMENT values (el+)>



<!--

     The following type specifiers are available:

		Boolean

		Character

		Byte

		Short

		Integer

		Long

		Float

		Double

		ByteArray

		String

		Date

		Time

		Timestamp

		BigDecimal

     Use a type specifier for predicates that have to do with custom

	 field values.

-->

<!ELEMENT type (#PCDATA)>



<!-- ascending may have the values of "true" or "false" -->

<!ELEMENT ascending (#PCDATA)>



<!-- negate may have the values of "true" or "false" -->

<!ELEMENT negate (#PCDATA)>

attachment 2
The following is a sample real estate search:

CriteriaValuesValues to use in the XML
CategoriesBostonCambridgeTownhouse"1" (category ID for Boston)"2" (category ID for Cambridge)"15" (category ID for Townhouse)
PriceRangeField ID is "88"150,000 to 320,000
MinBedroomsField ID is "102"1
DoctypeRealEstateProperty"33"


Here it is expressed in XML according to the DTD:




<content-search>

	<selected-props>

		<prop-name>TITLE</prop-name>

		<prop-name>CREATED</prop-name>

	</selected-props>

	<where-clause>

		<and>

			<eq>

				<var>DOCTYPEID</var>

				<val>33</val>

			</eq>

			<in>

				<var>CATEGORYID</var>

				<values>

					<el>1</el>

					<el>2</el>

					<el>15</el>

				</values>

			</in>

			<and>

				<eq>

					<var>FIELDID</var>

					<val>88</val>

				</eq>

				<between>

					<var>FIELDVALUE</var>

					<val>150000</val>

					<val>320000</val>

					<type>Integer</type>

				</between>

			</and>

			<and>

				<eq>

					<var>FIELDID</var>

					<val>102</val>

				</eq>

				<eq>

					<var>FIELDVALUE</var>

					<val>1</val>

					<type>Integer</type>

				</eq>

			</and>

		</and>

	</where-clause>

</content-search>