Extension Recherche Simple Webtop
Sur l'interface Webtop, une zone de saisie permet de réaliser une recherche simple, dite rapide. Par défaut celle ci effectue une recherche plein texte avec la valeur saisie par l'utilisateur. Dans le cas ou le moteur de recherche plein texte n'est pas activé, cette recherche est effectuée, avec l'opérateur OU, sur les attributs:
- object_name
- title
- subject
L'objectif de cette extension est de rajouter un filtre pour ne retourner que les objets dont l'attribut description
contient la valeur Recover
.
Les extraits de code respectent les bonnes pratiques présentées sur la page Bonnes_Pratiques_Webtop.
Sommaire
Votre avis
Current user rating: 93/100 (1 votes)
|
|
Version 5.3 SP2
L'exécution de la recherche simple a été identifiée dans le composant search
définit par le fichier XML webtop/config/searchex_component.xml
. Seule la classe d'instance doit être modifiée pour implémenter cette modification.
Fichier XML
L'extension est donc définie à l'emplacement custom/config/webtop/searchex_component.xml
afin de spécifier une nouvelle classe d'instance:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<config version='1.0'>
<scope>
<component id="search" extends="search:webtop/config/searchex_component.xml">
<class>fr.amexio.webtop.webcomponent.search.SearchEx</class>
</component>
</scope>
</config>
Classe d'instance
Après analyse du code standard, la méthode identifiée pour la construction de la requête est buildQueryFromKeywords(String keywords)
. La modification apportée permet de:
- Exécuter le code standard pour construire la définition de la recherche.
- Récupérer la définition de la recherche et ajouter un filtre.
package fr.amexio.webtop.webcomponent.search;
import com.documentum.fc.client.search.IDfExpressionSet;
import com.documentum.fc.client.search.IDfQueryBuilder;
import com.documentum.fc.client.search.IDfQueryDefinition;
import com.documentum.fc.client.search.IDfSearchOperation;
import com.documentum.fc.client.search.IDfSmartListDefinition;
import com.documentum.fc.common.DfAttr;
import com.documentum.fc.common.DfDocbaseConstants;
import com.documentum.webcomponent.library.search.SearchInfo;
/**
* @author Etienne Jouvin
*
*/
public class SearchEx extends com.documentum.webtop.webcomponent.search.SearchEx {
private static final String DESC_RECOVER = "Recover";
/**
* Serial version UID
*/
private static final long serialVersionUID = 8483322094034597915L;
/** {@inheritDoc} */
@Override
protected void buildQueryFromKeywords(String keywords) {
super.buildQueryFromKeywords(keywords);
/* Get the SearchInfo instance where the query definition is stored. */
SearchInfo searchInfo = getSearchInfo();
/* Get the smart list definition. */
IDfSmartListDefinition idfSmartListDefinition = searchInfo.getSmartListDefinition();
IDfQueryDefinition idfQueryDefinition = idfSmartListDefinition.getQueryDefinition();
if (idfQueryDefinition instanceof IDfQueryBuilder) {
/* The query definition must be a IDfQueryBuilder, because built like this is in parent method. Do the check in case of... */
IDfQueryBuilder idfQueryBuilder = (IDfQueryBuilder) idfQueryDefinition;
/* Get the root expression, where filter can be added. */
IDfExpressionSet rootExpressionSet = idfQueryBuilder.getRootExpressionSet();
rootExpressionSet.addSimpleAttrExpression(DfDocbaseConstants.DESCRIPTION, DfAttr.DM_STRING, IDfSearchOperation.SEARCH_OP_EQUAL, true, false, DESC_RECOVER);
}
}
}
Analyse
Ce code, aussi simple soit-il, nécessite une certaine analyse afin de trouver le point d'entrée permettant d'atteindre l'objectif. Ce paragraphe résume les différentes étapes afin d'identifier le point d'entrée.
Composant titlebar
La recherche simple est affichée au niveau de la barre de titre de Webtop. Il faut donc regarder ce composant afin de comprendre comment cette recherche est exécutée.
La définition XML de titlebar
se trouve à l'emplacement webtop/config/titlebar_component.xml
, permettant de trouver la page JSP.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<config version='1.0'>
<scope>
<component id="titlebar">
<!-- Description (not NLS'd) -->
<desc>
Webtop Titlebar component: Contains single box search and static buttons displayed
in the titlebar of both WebTop views.
</desc>
<params>
</params>
<pages>
<start>/webtop/titlebar/titlebar.jsp</start>
</pages>
<class>com.documentum.webtop.webcomponent.titlebar.TitleBar</class>
<nlsbundle>com.documentum.webtop.webcomponent.titlebar.TitleBarNlsProp</nlsbundle>
<!-- If this component supports failover/replication, the failoverenabled needs to be set to true-->
<failoverenabled>true</failoverenabled>
</component>
</scope>
</config>
Au sein de cette page, la fonction Javascript onClickSearch
est appelé en cliquant sur le bouton "GO". Celle ci appelle le composant search
avec le contenu de la zone de saisie identifiée par txtSearch
pour l'argument query
.
<%
//
%>
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page errorPage="/wdk/errorhandler.jsp" %>
<%@ taglib uri="/WEB-INF/tlds/dmform_1_0.tld" prefix="dmf" %>
<%@ taglib uri="/WEB-INF/tlds/dmformext_1_0.tld" prefix="dmfx" %>
<html>
<head>
<dmf:webform/>
<script>
var isMac = (navigator.platform.toLowerCase().indexOf("mac")!=-1)
keyValueToLaunchEditor = "ctrlKeyPressed";
if (g_clientInfo.isPlatform(ClientInfo.MACOS))
{
keyValueToLaunchEditor = "shiftKeyPressed";
}
else if (g_clientInfo.isBrowser(ClientInfo.NETSCAPE))
{
keyValueToLaunchEditor = "altKeyPressed";
}
registerClientEventHandler(null, "onClickLogoEvent", onClickLogoEvent);
function onClickLink()
{
postComponentPageEvent(null, "titlebar", "titlebar");
}
function onClickAdvancedSearch()
{
var contentPage = eval(getAbsoluteFramePath("content"));
if (contentPage != null)
{
var textField = document.getElementById("txtSearch");
var strValue = textField.value;
if (strValue != "")
{
textField.value = "";
postComponentNestEvent(null, "advsearchcontainer","content","component","advsearch", "type", "dm_sysobject", "usepreviousinput", "false", "query", strValue );
}
else
{
postComponentNestEvent(null, "advsearchcontainer","content","component","advsearch", "type", "dm_sysobject", "usepreviousinput", "true");
}
}
}
function onClickSearch()
{
var contentPage = eval(getAbsoluteFramePath("content"));
if (contentPage != null)
{
var text = document.getElementById("txtSearch");
var strValue = text.value;
if (strValue != "")
{
postComponentJumpEvent(null, "search", "content", "query", strValue);
}
}
}
function onClickPreferences()
{
postComponentNestEvent(null, "preferences", "content", "component", "general_preferences");
}
function onClickHelp()
{
fireClientEvent("InvokeHelp");
}
function onClickLogo()
{
fireClientEvent("onClickLogoEvent");
}
function onClickLogoEvent(keys)
{
if (keys == keyValueToLaunchEditor)
{
postComponentNestEvent(null, "about", "content", "enableTools", "true");
}
else
{
postComponentNestEvent(null, "about", "content");
}
}
</script>
</head>
<body class='webtopTitlebarBackground' marginheight='0' marginwidth='0' leftmargin='0' rightmargin='0' topmargin='0' bottommargin='0'>
<dmf:form>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td align="left" width="50%">
<table cellspacing='0' cellpadding='4' border='0'>
<tr>
<td valign='middle'>
<dmf:panel id='workarealinkpanel' name='workarealinkpanel'>
<dmf:link id='workarealink' focus='true' name='workarealink' cssclass='titleLink' onclick='setFocusOnFrame,"content"' runatclient='true' nlsid='MSG_GO_TO_WORK_AREA' tooltipnlsid='MSG_GO_TO_WORK_AREA_TIP'/>
</dmf:panel>
</td>
<td valign='middle'>
<dmf:label cssclass="searchLink" nlsid="MSG_SEARCH"/>
</td>
<td valign='middle'>
<dmf:text name="txtSearch" id="txtSearch" size="25" defaultonenter="true" tooltipnlsid='MSG_SEARCH'/>
</td>
<td valign='middle'>
<dmf:button cssclass="buttonLink" nlsid="MSG_GO"
onclick="onClickSearch" runatclient="true" default="true"
imagefolder="images/themebutton" tooltipnlsid='MSG_GO_TIP'/>
</td>
<td valign='middle'>
<dmf:button cssclass="buttonLink" nlsid="MSG_ADVANCED_SEARCH"
onclick="onClickAdvancedSearch" runatclient="true"
imagefolder="images/themebutton" tooltipnlsid='MSG_ADVANCED_SEARCH_TIP'/>
</td>
</tr>
</table>
</td>
<td align="right" width="50%">
<table cellspacing='0' cellpadding='4' border='0'>
<td align='right' valign='middle'>
<dmf:button cssclass="buttonLink" nlsid="MSG_PREFERENCES"
onclick="onClickPreferences" runatclient="true"
imagefolder="images/themebutton" tooltipnlsid='MSG_PREFERENCES_TIP'/>
</td>
<td valign='middle'>
<dmfx:actionbutton name='logout' action='logout' nlsid='MSG_LOGOUT'
cssclass="buttonLink" imagefolder="images/themebutton" tooltipnlsid='MSG_LOGOUT_TIP'>
</dmfx:actionbutton>
</td>
<td valign='middle'>
<dmf:button cssclass="buttonLink" nlsid="MSG_HELP"
onclick="onClickHelp" runatclient="true"
imagefolder="images/themebutton" tooltipnlsid='MSG_HELP_TIP'/>
</td>
<td valign='middle'>
</td>
</tr>
</table>
</td>
</tr>
</table>
</dmf:form>
</body>
</html>
Composant search
La définition de ce composant se situe dans le fichier webtop\config\searchex_component.xml
, où seule la classe d'instance est spécifiée. C'est également une extension du composant défini dans la couche webcomponent
à l'emplacement webcomponent/config/library/search/searchex/search_component.xml
.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Confidential Property of Documentum, Inc. -->
<!-- (c) Copyright Documentum, Inc. 2001. -->
<!-- All Rights reserved. -->
<!-- May not be used without prior written agreement -->
<!-- signed by a Documentum corporate officer. -->
<!-- -->
<!-- Component: search -->
<!-- Scope: None -->
<!-- Revision $revision$ -->
<!-- Modified on $date$ -->
<config version='1.0'>
<!-- this component doesn't handle display of search results of a dm_query object -->
<scope>
<!-- the simple search component definition -->
<component id="search" extends="search:webcomponent/config/library/search/searchex/search_component.xml">
<!-- Component Behavior -->
<class>com.documentum.webtop.webcomponent.search.SearchEx</class>
</component>
</scope>
</config>
Le contenu de la classe com.documentum.webtop.webcomponent.search.SearchEx
est assez minimaliste. Il permet juste de définir la page jsp à afficher en fonction du contexte de l'utilisateur.
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
package com.documentum.webtop.webcomponent.search;
import com.documentum.web.common.ArgumentList;
import com.documentum.webtop.app.AppSessionContext;
import com.documentum.webtop.app.ApplicationNavigation;
public class SearchEx extends com.documentum.webcomponent.library.search.SearchEx
{
public SearchEx()
{
}
public void onInit(ArgumentList args)
{
if(args.get("drilldown") == null)
{
String strView = AppSessionContext.getView();
if(strView != null && strView.equals("classic"))
args.add("drilldown", "false");
else
args.add("drilldown", "true");
}
super.onInit(args);
}
protected void navigateToFolderPath(String strFolderPath)
{
ApplicationNavigation.navigateToFolderPath(this, strFolderPath);
}
}
Il faut donc regarder dans la classe parente. La première analyse conduit à la méthode onInit
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
public void onInit(ArgumentList args)
{
m_initializing = true;
super.onInit(args);
if(!delegateDqlSearch(args))
initSearch(args);
IConfigElement configElement = lookupElement("dragdrop");
m_dragDropSupportBuilder.initialize(configElement);
NavigationObservable.getObservable(this).addObserver(this);
m_initializing = false;
}
Il semblerait que la fonction initSearch(ArgumentList args)
soit intéressante, mais elle n'est appelée que si la fonction delegateDqlSearch(ArgumentList args)
retourne false
.
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
private boolean delegateDqlSearch(ArgumentList args)
{
boolean fDelegated = false;
String strQueryType = args.get("queryType");
String strDelegateComponentName = "dqlsearchdelegate";
if(strQueryType != null && !strQueryType.equals("string") && !strQueryType.equals("objectId") && !strQueryType.equals("querydef") && !strQueryType.equals("queryId"))
{
args.add("useColumnsFromQuery", "true");
setComponentJump(strDelegateComponentName, args, getContext());
fDelegated = true;
}
return fDelegated;
}
Celle ci retourne vrai que dans le cas où:
- Argument queryType est non null.
- Et argument queryType n'est pas
string
- Et argument queryType n'est pas
objectId
- Et argument queryType n'est pas
queryDef
- Et argument queryType n'est pas
queryId
Or celui ci n'est pas spécifié lors de l'appel depuis la barre de titre. Donc sa valeur est nulle, entraînant le retour de la valeur false
, entraînant l'exécution de la méthode initSearch(ArgumentList args)
.
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
protected void initSearch(ArgumentList args)
{
initSearchInfo(args);
String strDrillDown = args.get("drilldown");
if(strDrillDown != null && strDrillDown.equalsIgnoreCase("false"))
setDrillDownView(false);
String showWaitPage = args.get("showwait");
if(showWaitPage != null && showWaitPage.equalsIgnoreCase("false"))
m_fShowWaitPage = false;
Hidden hidden = (Hidden)getControl("waitRequestId", com.documentum.web.form.control.Hidden.class);
hidden.setValue(String.valueOf(getNewWaitRequestId()));
getControl("processing", com.documentum.web.form.control.Link.class);
getControl("revise", com.documentum.web.form.control.Link.class).setVisible(true);
m_datagrid = (Datagrid)getControl("doclistgrid", com.documentum.web.form.control.databound.Datagrid.class);
DataPaging dataPaging = (DataPaging)getControl("docpager", com.documentum.web.form.control.databound.DataPaging.class);
dataPaging.setEventHandler("onnextpage", "onNextPageClicked", this);
dataPaging.setEventHandler("onlastpage", "onLastPageClicked", this);
addControlListener(this);
buildQuery(args);
if(!processContextSensitiveJump(args) && authenticateSources() && checkQueryTypeExists())
executeQuery();
}
Lors de l'exécution de cette méthode, il y a un appel sur la méthode buildQuery(ArgumentList args)
qui va construire la définition de cette requête en fonction des arguments d'appel au composant:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
protected void buildQuery(ArgumentList args)
{
IDfSession session = null;
try
{
String strQueryType = args.get("queryType");
String strQuery = args.get("query");
if(strQueryType != null && strQueryType.equals("objectId"))
{
setSearchInfo(new SearchInfo());
IDfSmartListDefinition smartlist = SearchUtil.loadSmartListDefinition(strQuery, getSearchInfo().getSearchService());
getSearchInfo().setSmartListDefinition(smartlist);
getSearchInfo().setObjectId(strQuery);
IDfId idSavedSearchObject = new DfId(strQuery);
String strDocbase = DocbaseUtils.getDocbaseNameFromId(idSavedSearchObject);
session = SessionManagerHttpBinding.getSessionManager().getSession(strDocbase);
IDfSysObject obj = (IDfSysObject)session.getObject(idSavedSearchObject);
getSearchInfo().setQueryDescription(obj.getString("title"));
} else
if(strQueryType != null && strQueryType.equals("querydef"))
{
IDfSmartListDefinition smartlist = SearchUtil.getSmartListDefinition(strQuery);
getSearchInfo().setSmartListDefinition(smartlist);
if(getSearchInfo().getQueryDescription() == null || getSearchInfo().getObjectId() == null)
getSearchInfo().setQueryDescription(getQueryDescription());
} else
if(strQueryType == null || !strQueryType.equals("queryId"))
buildQueryFromKeywords(strQuery);
else
if(getSearchInfo().getQueryDescription() == null || getSearchInfo().getObjectId() == null)
getSearchInfo().setQueryDescription(getQueryDescription());
}
catch(DfException e)
{
throw new WrapperRuntimeException(e);
}
catch(Exception e)
{
throw new WrapperRuntimeException(e);
}
finally
{
if(session != null)
SessionManagerHttpBinding.getSessionManager().release(session);
}
}
Pour rappel, le seul argument envoyé au composant est query
avec la valeur saisie par l'utilisateur. Donc la variable strQueryType
sera nulle, entraînant l'appel à la méthode buildQueryFromKeywords(String strKeywords)
.
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SearchEx.java
protected void buildQueryFromKeywords(String strKeywords)
{
try
{
String strObjectType = lookupString("type");
if(strObjectType == null || strObjectType.length() == 0)
strObjectType = "dm_sysobject";
IDfSearchService searchService = getSearchInfo().getSearchService();
IDfQueryManager IDfQueryManager = searchService.newQueryMgr();
IDfQueryBuilder iDfQueryBuilder = IDfQueryManager.newQueryBuilder(strObjectType);
IDfSearchMetadataManager IDfSearchMetadataManager = iDfQueryBuilder.getMetadataMgr();
List lsLocations = getKeywordsSearchLocations();
Iterator itLocations;
String location[];
for(itLocations = lsLocations.iterator(); itLocations.hasNext(); IDfSearchMetadataManager.addSelectedSource(location[0]))
location = (String[])itLocations.next();
itLocations = lsLocations.iterator();
do
{
if(!itLocations.hasNext())
break;
String location[] = (String[])itLocations.next();
iDfQueryBuilder.addSelectedSource(location[0]);
IDfSearchSourceMap sourceMap = IDfSearchMetadataManager.getSourceMap();
IDfSearchSource source = sourceMap.getSource(location[0]);
if(source != null)
{
int sourceType = source.getType();
if(location.length > 1 && location[1] != null && location[1].length() > 0 && sourceType == 1)
iDfQueryBuilder.addLocationScope(location[0], location[1], true);
}
} while(true);
IDfExpressionSet idfExpressionSet = iDfQueryBuilder.getRootExpressionSet();
idfExpressionSet.addFullTextExpression(strKeywords);
IDfSmartListDefinition idfSmartListDefinition = IDfQueryManager.newSmartListDefinition();
idfSmartListDefinition.setQueryDefinition(iDfQueryBuilder);
getSearchInfo().setSmartListDefinition(idfSmartListDefinition);
getSearchInfo().setQueryDescription(getQueryDescription());
}
catch(DfException e)
{
throw new WrapperRuntimeException(e);
}
}
Une instance de IDfQueryBuilder
est initialisé en y ajoutant un filtre de type "recherche plein texte". C'est cette instance qui porte la définition de la recherche à exécuter. La méthode étant "protected", il est possible de la surcharger pour y apporter un nouveau comportement. Ce qui permet de réaliser l'objectif de l'article, à savoir rajouter un filtre dans la définition de la recherche.