001/* 002 * (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Anahide Tchertchian 018 */ 019package org.nuxeo.ecm.platform.smart.query.jsf; 020 021import java.io.Serializable; 022import java.util.List; 023 024import javax.faces.application.FacesMessage; 025import javax.faces.component.EditableValueHolder; 026import javax.faces.component.UIComponent; 027import javax.faces.context.FacesContext; 028import javax.faces.event.ActionEvent; 029import javax.faces.event.AjaxBehaviorEvent; 030import javax.faces.validator.ValidatorException; 031 032import org.apache.commons.lang.StringUtils; 033import org.jboss.seam.ScopeType; 034import org.jboss.seam.annotations.Name; 035import org.jboss.seam.annotations.Observer; 036import org.jboss.seam.annotations.Scope; 037import org.jboss.seam.annotations.intercept.BypassInterceptors; 038import org.jboss.seam.annotations.web.RequestParameter; 039import org.nuxeo.ecm.core.api.NuxeoException; 040import org.nuxeo.ecm.core.api.SortInfo; 041import org.nuxeo.ecm.platform.smart.query.HistoryList; 042import org.nuxeo.ecm.platform.smart.query.SmartQuery; 043import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 044import org.nuxeo.runtime.logging.DeprecationLogger; 045import org.nuxeo.search.ui.seam.SearchUIActions; 046 047/** 048 * Seam component handling a {@link IncrementalSmartNXQLQuery} instance created for a given existing query string. 049 * <p> 050 * Also handles undo/redo actions and all ajax interactions for an incremental query build page. 051 * 052 * @since 5.4 053 * @author Anahide Tchertchian 054 */ 055@Name("smartNXQLQueryActions") 056@Scope(ScopeType.PAGE) 057public class SmartNXQLQueryActions implements Serializable { 058 059 private static final long serialVersionUID = 1L; 060 061 public static final int HISTORY_CAPACITY = 20; 062 063 /** 064 * @deprecated since 8.1: query part held/set directly on the content view search document model 065 */ 066 protected String queryPart; 067 068 protected HistoryList<String> queryPartHistory; 069 070 protected HistoryList<String> redoQueryPartHistory; 071 072 protected IncrementalSmartNXQLQuery currentSmartQuery; 073 074 /** 075 * @deprecated since 8.1: selected columns directly held/set on the content view search document model 076 */ 077 @Deprecated 078 protected List<String> selectedLayoutColumns; 079 080 /** 081 * @deprecated since 8.1: search sort infos directly held/set on the content view search document model 082 */ 083 @Deprecated 084 protected List<SortInfo> searchSortInfos; 085 086 /** 087 * Request parameter passed from the widget that makes it possible to decide whether the backing object should be 088 * updated when performing ajax calls. 089 * <p> 090 * For instance, on the smart search form, any ajax action should update the backing property {@link #queryPart}. 091 * When the query part is held by a document property, it should not be updated on ajax actions: only the global 092 * submit of the form should impact it. 093 * 094 * @deprecated since 8.1: query part is now held by the content view search document model, consider it does not 095 * need to be updated until user clicks on "filter". 096 */ 097 @RequestParameter 098 @Deprecated 099 protected Boolean updateQueryPart; 100 101 /** 102 * Request parameter making it possible to find the component holding the query part to update it. 103 */ 104 @RequestParameter 105 protected String queryPartComponentId; 106 107 /** 108 * @deprecated since 8.1: query part held/set directly on the content view search document model 109 */ 110 @Deprecated 111 public String getQueryPart() { 112 DeprecationLogger.log("Query part held/set directly on the content view search document model", "8.1"); 113 return queryPart; 114 } 115 116 /** 117 * @deprecated since 8.1: query part held/set directly on the content view search document model 118 */ 119 public void setQueryPart(String queryPart) { 120 DeprecationLogger.log("Query part held/set directly on the content view search document model", "8.1"); 121 this.queryPart = queryPart; 122 addToQueryPartHistory(queryPart); 123 } 124 125 /** 126 * @deprecated since 8.1: selected columns directly held/set on the content view search document model 127 */ 128 @Deprecated 129 public List<String> getSelectedLayoutColumns() { 130 DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1"); 131 return selectedLayoutColumns; 132 } 133 134 /** 135 * @deprecated since 8.1: selected columns directly held/set on the content view search document model 136 */ 137 @Deprecated 138 public void setSelectedLayoutColumns(List<String> selectedLayoutColumns) { 139 DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1"); 140 this.selectedLayoutColumns = selectedLayoutColumns; 141 } 142 143 /** 144 * @deprecated since 8.1: selected columns directly held/set on the content view search document model 145 */ 146 @Deprecated 147 public void resetSelectedLayoutColumns() { 148 DeprecationLogger.log("Selected columns held/set directly on the content view search document model", "8.1"); 149 setSelectedLayoutColumns(null); 150 } 151 152 /** 153 * @deprecated since 8.1: search sort infos directly held/set on the content view search document model 154 */ 155 @Deprecated 156 public List<SortInfo> getSearchSortInfos() { 157 DeprecationLogger.log("Search sort infos held/set directly on the content view search document model", "8.1"); 158 return searchSortInfos; 159 } 160 161 /** 162 * @deprecated since 8.1: search sort infos directly held/set on the content view search document model 163 */ 164 @Deprecated 165 public void setSearchSortInfos(List<SortInfo> searchSortInfos) { 166 DeprecationLogger.log("Search sort infos held/set directly on the content view search document model", "8.1"); 167 this.searchSortInfos = searchSortInfos; 168 } 169 170 public void initCurrentSmartQuery(String existingQueryPart, boolean resetHistory) { 171 currentSmartQuery = new IncrementalSmartNXQLQuery(existingQueryPart); 172 if (resetHistory) { 173 queryPartHistory = null; 174 addToQueryPartHistory(existingQueryPart); 175 } 176 } 177 178 /** 179 * Creates a new {@link #currentSmartQuery} instance. 180 * <p> 181 * This method is supposed to be called once when loading a new page: it will initialize the smart query object 182 * according to the current existing qury part. 183 * <p> 184 * It should not be called when there are validation errors happening on the page, otherwise the new query part may 185 * be discarded. 186 */ 187 public void initCurrentSmartQuery(String existingQueryPart) { 188 initCurrentSmartQuery(existingQueryPart, true); 189 } 190 191 public SmartQuery getCurrentSmartQuery() { 192 if (currentSmartQuery == null) { 193 initCurrentSmartQuery("", true); 194 } 195 return currentSmartQuery; 196 } 197 198 /** 199 * Updates the current {@link #currentSmartQuery} instance according to changes on the existing query part. 200 */ 201 public void queryPartChanged(AjaxBehaviorEvent event) { 202 UIComponent comp = event.getComponent(); 203 if (comp instanceof EditableValueHolder) { 204 EditableValueHolder queryComp = (EditableValueHolder) comp; 205 String newQuery = (String) queryComp.getSubmittedValue(); 206 // set local value in case of validation error in ajax region 207 // when adding a new item to the query 208 queryComp.setValue(newQuery); 209 // update query 210 currentSmartQuery.setExistingQueryPart(newQuery); 211 addToQueryPartHistory(newQuery); 212 } else { 213 throw new NuxeoException("Component not found"); 214 } 215 } 216 217 /** 218 * Updates the JSF component holding the query part. 219 * 220 * @param event the JSF event that will give an anchor on the JSF tree to find the target component. 221 * @param newQuery the new query to set. 222 * @param rebuildSmartQuery if true, will rebuild the smart query completely, otherwise will just set the query part 223 * on it. 224 * @throws NuxeoException if target JSF component is not found in the JSF tree. 225 */ 226 protected void setQueryPart(ActionEvent event, String newQuery, boolean rebuildSmartQuery) { 227 if (currentSmartQuery != null) { 228 UIComponent component = event.getComponent(); 229 if (component == null) { 230 return; 231 } 232 // find component to update in the hierarchy of JSF components: 233 // this is specific to rendering structure... 234 EditableValueHolder queryPartComp = ComponentUtils.getComponent(component, queryPartComponentId, 235 EditableValueHolder.class); 236 if (queryPartComp != null) { 237 // set submitted value to ensure validation 238 queryPartComp.setSubmittedValue(newQuery); 239 // set local value in case of validation error in ajax region 240 // when adding a new item to the query 241 queryPartComp.setValue(newQuery); 242 if (rebuildSmartQuery) { 243 // rebuild smart query 244 initCurrentSmartQuery(newQuery, false); 245 } else { 246 currentSmartQuery.setExistingQueryPart(newQuery); 247 } 248 addToQueryPartHistory(newQuery); 249 } else { 250 throw new NuxeoException("Component not found"); 251 } 252 } 253 } 254 255 /** 256 * Updates the query part, asking the {@link #currentSmartQuery} to build the new resulting query. 257 * 258 * @see #setQueryPart(ActionEvent, String) 259 */ 260 public void buildQueryPart(ActionEvent event) { 261 if (currentSmartQuery != null) { 262 String newQuery = currentSmartQuery.buildQuery(); 263 setQueryPart(event, newQuery, true); 264 } 265 } 266 267 /** 268 * Sets the query part to an empty value. 269 * 270 * @see #setQueryPart(ActionEvent, String) 271 */ 272 public void clearQueryPart(ActionEvent event) { 273 setQueryPart(event, "", false); 274 } 275 276 protected String getCurrentQueryPart() { 277 if (currentSmartQuery != null) { 278 return currentSmartQuery.getExistingQueryPart(); 279 } 280 return null; 281 } 282 283 protected boolean hasQueryPartHistory(HistoryList<String> history) { 284 if (history == null || history.isEmpty()) { 285 return false; 286 } 287 String lastQueryPart = history.getLast(); 288 // lastQueryPart cannot be null 289 if (history.size() == 1 && lastQueryPart.equals(getCurrentQueryPart())) { 290 return false; 291 } 292 return true; 293 } 294 295 public boolean getCanUndoQueryPartChanges() { 296 return hasQueryPartHistory(queryPartHistory); 297 } 298 299 public boolean getCanRedoQueryPartChanges() { 300 return hasQueryPartHistory(redoQueryPartHistory); 301 } 302 303 public void undoHistoryChanges(ActionEvent event, HistoryList<String> history, HistoryList<String> redoHistory) { 304 if (!hasQueryPartHistory(history)) { 305 return; 306 } 307 String lastQueryPart = history.getLast(); 308 history.removeLast(); 309 String currentQueryPart = getCurrentQueryPart(); 310 // lastQueryPart cannot be null 311 if (!lastQueryPart.equals(currentQueryPart)) { 312 setQueryPart(event, lastQueryPart, false); 313 } else if (history.size() > 0) { 314 lastQueryPart = history.getLast(); 315 setQueryPart(event, lastQueryPart, false); 316 history.removeLast(); 317 } 318 if (redoHistory != null) { 319 addToHistory(currentQueryPart, redoHistory); 320 } 321 } 322 323 public void undoQueryPartChanges(ActionEvent event) { 324 if (redoQueryPartHistory == null) { 325 redoQueryPartHistory = new HistoryList<String>(HISTORY_CAPACITY); 326 } 327 undoHistoryChanges(event, queryPartHistory, redoQueryPartHistory); 328 } 329 330 public void redoQueryPartChanges(ActionEvent event) { 331 undoHistoryChanges(event, redoQueryPartHistory, null); 332 } 333 334 protected void addToHistory(String queryPart, HistoryList<String> queryPartHistory) { 335 if (queryPartHistory == null) { 336 return; 337 } 338 if (queryPart == null) { 339 queryPart = ""; 340 } 341 if (queryPartHistory.size() == 0) { 342 queryPartHistory.addLast(queryPart); 343 } else { 344 String lastQueryPart = queryPartHistory.getLast(); 345 if (!queryPart.equals(lastQueryPart)) { 346 queryPartHistory.addLast(queryPart); 347 } 348 } 349 } 350 351 protected void addToQueryPartHistory(String queryPart) { 352 if (queryPartHistory == null) { 353 queryPartHistory = new HistoryList<String>(HISTORY_CAPACITY); 354 } 355 addToHistory(queryPart, queryPartHistory); 356 } 357 358 /** 359 * Validates the query part: throws a {@link ValidatorException} if query is not a String, or if 360 * {@link IncrementalSmartNXQLQuery#isValid(String)} returns false. 361 * 362 * @see IncrementalSmartNXQLQuery#isValid(String) 363 */ 364 public void validateQueryPart(FacesContext context, UIComponent component, Object value) { 365 if (value == null) { 366 return; 367 } 368 if (!(value instanceof String)) { 369 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, 370 ComponentUtils.translate(context, "error.smart.query.invalidQuery"), null); 371 // also add global message 372 context.addMessage(null, message); 373 throw new ValidatorException(message); 374 } 375 String query = (String) value; 376 if (StringUtils.isBlank(query)) { 377 return; 378 } 379 if (!IncrementalSmartNXQLQuery.isValid(query)) { 380 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, 381 ComponentUtils.translate(context, "error.smart.query.invalidQuery"), null); 382 // also add global message 383 context.addMessage(null, message); 384 throw new ValidatorException(message); 385 } 386 } 387 388 /** 389 * Returns true if current request is an ajax request. 390 * <p> 391 * Useful when some component should be required only when the global form is submitted, and not when ajax calls are 392 * performed. 393 */ 394 public boolean isAjaxRequest() { 395 DeprecationLogger.log( 396 "smartNXQLQueryActions#isAjaxRequest is not needed anymore, proper ajax calls make it possible to validate or not a field depending on use cases.", 397 "8.1"); 398 FacesContext context = FacesContext.getCurrentInstance(); 399 if (context != null) { 400 return context.getPartialViewContext().isAjaxRequest(); 401 } 402 return false; 403 } 404 405 /** 406 * Returns a valid where clause from a query part. 407 * <p> 408 * Useful to avoid generating an invalid query if query part is empty (especially if content view is not marked as 409 * waiting for first execution). 410 * 411 * @since 8.1 412 */ 413 public String getWhereClause(String queryPart, boolean followedByClause) { 414 if (StringUtils.isBlank(queryPart)) { 415 if (followedByClause) { 416 return "WHERE "; 417 } 418 return ""; 419 } 420 return "WHERE (" + queryPart + ")" + (followedByClause ? " AND " : ""); 421 } 422 423 /** 424 * @since 8.1 425 */ 426 public boolean isInitialized() { 427 return currentSmartQuery != null; 428 } 429 430 /** 431 * @since 8.1 432 */ 433 @Observer(value = { SearchUIActions.SEARCH_SELECTED_EVENT }, create = false) 434 @BypassInterceptors 435 public void resetCurrentSmartQuery() { 436 currentSmartQuery = null; 437 } 438 439}