|
|

[an error occurred while processing this directive]
|
 |
 |
 |
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 name | Field data type |
| Number of bedrooms | Integer |
| Hardwood floors | Boolean |
| Swimming pool | Boolean |
| 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:
| Criteria | Values | Values to use in the XML |
| Categories | BostonCambridgeTownhouse | "1" (category ID for Boston)"2" (category ID for Cambridge)"15" (category ID for Townhouse) |
| PriceRange | Field ID is "88" | 150,000 to 320,000 |
| MinBedrooms | Field ID is "102" | 1 |
| Doctype | RealEstateProperty | "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>
|
 |
|
 |
 |
 |