001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.jsp; 029 030import org.opencms.ade.publish.CmsPublishListHelper; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.file.collectors.I_CmsCollectorPublishListProvider; 036import org.opencms.flex.CmsFlexController; 037import org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo; 038import org.opencms.i18n.CmsLocaleManager; 039import org.opencms.jsp.search.config.CmsSearchConfiguration; 040import org.opencms.jsp.search.config.I_CmsSearchConfiguration; 041import org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser; 042import org.opencms.jsp.search.config.parser.CmsPlainQuerySearchConfigurationParser; 043import org.opencms.jsp.search.config.parser.CmsXMLSearchConfigurationParser; 044import org.opencms.jsp.search.controller.CmsSearchController; 045import org.opencms.jsp.search.controller.I_CmsSearchControllerCommon; 046import org.opencms.jsp.search.controller.I_CmsSearchControllerMain; 047import org.opencms.jsp.search.result.CmsSearchResultWrapper; 048import org.opencms.jsp.search.result.I_CmsSearchResultWrapper; 049import org.opencms.jsp.util.CmsJspElFunctions; 050import org.opencms.main.CmsException; 051import org.opencms.main.CmsIllegalArgumentException; 052import org.opencms.main.CmsLog; 053import org.opencms.main.OpenCms; 054import org.opencms.search.CmsSearchException; 055import org.opencms.search.CmsSearchResource; 056import org.opencms.search.fields.CmsSearchField; 057import org.opencms.search.solr.CmsSolrIndex; 058import org.opencms.search.solr.CmsSolrQuery; 059import org.opencms.search.solr.CmsSolrResultList; 060import org.opencms.util.CmsRequestUtil; 061import org.opencms.util.CmsUUID; 062import org.opencms.xml.content.CmsXmlContent; 063import org.opencms.xml.content.CmsXmlContentFactory; 064 065import java.util.HashSet; 066import java.util.Map; 067import java.util.Set; 068 069import javax.servlet.jsp.JspException; 070 071import org.apache.commons.logging.Log; 072 073/** 074 * This tag is used to easily create a search form for a Solr search within a JSP. 075 */ 076public class CmsJspTagSearch extends CmsJspScopedVarBodyTagSuport implements I_CmsCollectorPublishListProvider { 077 078 /** 079 * Type for the file formats that can be parsed. 080 * The format is given via the tag's attribute "fileFormat". 081 */ 082 private static enum FileFormat { 083 /** 084 * XML file (of type jsp-search-form). 085 */ 086 XML, 087 088 /** 089 * json file in respective format. 090 */ 091 JSON 092 } 093 094 /** The log object for this class. */ 095 private static final Log LOG = CmsLog.getLog(CmsJspTagSearch.class); 096 097 /** Serial version UID required for safe serialization. */ 098 private static final long serialVersionUID = 6048771777971251L; 099 100 /** Default number of items which are checked for change for the "This page" publish dialog. */ 101 public static final int DEFAULT_CONTENTINFO_ROWS = 600; 102 103 /** The CmsObject for the current user. */ 104 protected transient CmsObject m_cms; 105 106 /** The FlexController for the current request. */ 107 protected CmsFlexController m_controller; 108 109 /** Number of entries for which content info should be added to allow correct relations in "This page" publish dialog. */ 110 private Integer m_addContentInfoForEntries; 111 112 /** The "configFile" tag attribute. */ 113 private Object m_configFile; 114 115 /** The "configString" tag attribute. */ 116 private String m_configString; 117 118 /** The "fileFormat" tag attribute converted to type FileFormat. */ 119 private FileFormat m_fileFormat; 120 121 /** Search controller keeping all the config and state from the search. */ 122 private I_CmsSearchControllerMain m_searchController; 123 124 /** The search index that should be used . 125 * It will either be the configured index, or "Solr Offline" / "Solr Online" depending on the project. 126 * */ 127 private CmsSolrIndex m_index; 128 129 /** 130 * Empty constructor, required for JSP tags. 131 * 132 */ 133 public CmsJspTagSearch() { 134 135 super(); 136 m_fileFormat = FileFormat.XML; 137 } 138 139 /** 140 * @see org.opencms.file.collectors.I_CmsCollectorPublishListProvider#getPublishResources(org.opencms.file.CmsObject, org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo) 141 */ 142 @SuppressWarnings("javadoc") 143 public static Set<CmsResource> getPublishResourcesInternal(CmsObject cms, I_CmsContentLoadCollectorInfo info) 144 throws CmsException { 145 146 CmsSolrIndex solrOnline = OpenCms.getSearchManager().getIndexSolr(CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE); 147 CmsSolrIndex solrOffline = OpenCms.getSearchManager().getIndexSolr(CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE); 148 Set<CmsResource> result = new HashSet<CmsResource>(); 149 try { 150 Map<String, String[]> searchParams = CmsRequestUtil.createParameterMap( 151 info.getCollectorParams(), 152 true, 153 null); 154 // use "complicated" constructor to allow more than 50 results -> set ignoreMaxResults to true 155 // adjust the CmsObject to prevent unintended filtering of resources 156 CmsSolrResultList offlineResults = solrOffline.search( 157 CmsPublishListHelper.adjustCmsObject(cms, false), 158 new CmsSolrQuery(null, searchParams), 159 true); 160 Set<String> offlineIds = new HashSet<String>(offlineResults.size()); 161 for (CmsSearchResource offlineResult : offlineResults) { 162 offlineIds.add(offlineResult.getField(CmsSearchField.FIELD_ID)); 163 } 164 for (String id : offlineIds) { 165 CmsResource resource = cms.readResource(new CmsUUID(id)); 166 if (!(resource.getState().isUnchanged())) { 167 result.add(resource); 168 } 169 } 170 CmsSolrResultList onlineResults = solrOnline.search( 171 CmsPublishListHelper.adjustCmsObject(cms, true), 172 new CmsSolrQuery(null, searchParams), 173 true); 174 Set<String> deletedIds = new HashSet<String>(onlineResults.size()); 175 for (CmsSearchResource onlineResult : onlineResults) { 176 String uuid = onlineResult.getField(CmsSearchField.FIELD_ID); 177 if (!offlineIds.contains(uuid)) { 178 deletedIds.add(uuid); 179 } 180 } 181 for (String uuid : deletedIds) { 182 CmsResource resource = cms.readResource(new CmsUUID(uuid), CmsResourceFilter.ALL); 183 if (!(resource.getState().isUnchanged())) { 184 result.add(resource); 185 } 186 } 187 } catch (CmsSearchException e) { 188 LOG.warn(Messages.get().getBundle().key(Messages.LOG_TAG_SEARCH_SEARCH_FAILED_0), e); 189 } 190 return result; 191 } 192 193 /** 194 * @see javax.servlet.jsp.tagext.BodyTagSupport#doEndTag() 195 */ 196 @Override 197 public int doEndTag() throws JspException { 198 199 release(); 200 return super.doEndTag(); 201 } 202 203 /** 204 * @see javax.servlet.jsp.tagext.Tag#doStartTag() 205 */ 206 @Override 207 public int doStartTag() throws JspException, CmsIllegalArgumentException { 208 209 // initialize the content load tag 210 init(); 211 addContentInfo(); 212 return EVAL_BODY_INCLUDE; 213 } 214 215 /** Get the value of the specified configuration file (given via the tag's "configFile" attribute). 216 * @return The config file. 217 */ 218 public Object getConfigFile() { 219 220 return m_configFile; 221 } 222 223 /** Getter for the "configString". 224 * @return The "configString". 225 */ 226 public String getConfigString() { 227 228 return m_configString; 229 } 230 231 /** Get the value of the specified format of the configuration file (given via the tag's "fileFormat" attribute). 232 * @return The file format. 233 */ 234 public String getFileFormat() { 235 236 return m_fileFormat.toString(); 237 } 238 239 /** 240 * @see org.opencms.file.collectors.I_CmsCollectorPublishListProvider#getPublishResources(org.opencms.file.CmsObject, org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo) 241 */ 242 public Set<CmsResource> getPublishResources(CmsObject cms, I_CmsContentLoadCollectorInfo info) throws CmsException { 243 244 return getPublishResourcesInternal(cms, info); 245 } 246 247 /** 248 * @see javax.servlet.jsp.tagext.Tag#release() 249 */ 250 @Override 251 public void release() { 252 253 m_cms = null; 254 m_configFile = null; 255 setConfigString(null); 256 m_searchController = null; 257 m_index = null; 258 m_controller = null; 259 m_addContentInfoForEntries = null; 260 super.release(); 261 } 262 263 /** Setter for "addContentInfo", indicating if content information should be added. 264 * @param doAddInfo The value of the "addContentInfo" attribute of the tag 265 */ 266 public void setAddContentInfo(final Boolean doAddInfo) { 267 268 if ((null != doAddInfo) && doAddInfo.booleanValue() && (null == m_addContentInfoForEntries)) { 269 m_addContentInfoForEntries = Integer.valueOf(DEFAULT_CONTENTINFO_ROWS); 270 } 271 } 272 273 /** Setter for the configuration file. 274 * @param fileName Name of the configuration file to use for the search. 275 */ 276 public void setConfigFile(Object fileName) { 277 278 m_configFile = fileName; 279 } 280 281 /** Setter for the "configString". 282 * @param configString The "configString". 283 */ 284 public void setConfigString(final String configString) { 285 286 m_configString = configString; 287 } 288 289 /** Setter for "contentInfoMaxItems". 290 * @param maxItems number of items to maximally check for alterations. 291 */ 292 public void setContentInfoMaxItems(Integer maxItems) { 293 294 if (null != maxItems) { 295 m_addContentInfoForEntries = maxItems; 296 } 297 } 298 299 /** Setter for the file format. 300 * @param fileFormat File format the configuration file is in. 301 */ 302 public void setFileFormat(String fileFormat) { 303 304 if (fileFormat.toUpperCase().equals(FileFormat.JSON.toString())) { 305 m_fileFormat = FileFormat.JSON; 306 } 307 } 308 309 /** 310 * Initializes this formatter tag. 311 * <p> 312 * 313 * @throws JspException 314 * in case something goes wrong 315 */ 316 protected void init() throws JspException { 317 318 // initialize OpenCms access objects 319 m_controller = CmsFlexController.getController(pageContext.getRequest()); 320 m_cms = m_controller.getCmsObject(); 321 322 try { 323 I_CmsSearchConfiguration config = null; 324 if (m_configFile != null) { 325 CmsFile configFile = m_cms.readFile(CmsJspElFunctions.convertRawResource(m_cms, m_configFile)); 326 if (m_fileFormat == FileFormat.JSON) { 327 // read the JSON config file 328 OpenCms.getLocaleManager(); 329 String configString = new String( 330 configFile.getContents(), 331 CmsLocaleManager.getResourceEncoding(m_cms, configFile)); 332 config = new CmsSearchConfiguration(new CmsJSONSearchConfigurationParser(configString), m_cms); 333 } else { // assume XML 334 CmsXmlContent xmlContent = CmsXmlContentFactory.unmarshal(m_cms, configFile); 335 config = new CmsSearchConfiguration( 336 new CmsXMLSearchConfigurationParser(xmlContent, m_cms.getRequestContext().getLocale()), 337 m_cms); 338 } 339 } 340 if (m_configString != null) { 341 if (m_configString.trim().startsWith("{")) { 342 config = new CmsSearchConfiguration( 343 new CmsJSONSearchConfigurationParser(m_configString, config), 344 m_cms); 345 } else { 346 config = new CmsSearchConfiguration( 347 new CmsPlainQuerySearchConfigurationParser(m_configString, config), 348 m_cms); 349 } 350 } 351 m_searchController = new CmsSearchController(config); 352 353 String indexName = m_searchController.getCommon().getConfig().getSolrIndex(); 354 m_index = OpenCms.getSearchManager().getIndexSolr(indexName); 355 356 storeAttribute(getVar(), getSearchResults()); 357 358 } catch (Exception e) { // CmsException | UnsupportedEncodingException | JSONException 359 LOG.error(e.getLocalizedMessage(), e); 360 m_controller.setThrowable(e, m_cms.getRequestContext().getUri()); 361 throw new JspException(e); 362 } 363 } 364 365 /** 366 * Adds the content info for the collected resources used in the "This page" publish dialog. 367 */ 368 private void addContentInfo() { 369 370 if (!m_cms.getRequestContext().getCurrentProject().isOnlineProject() 371 && CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE.equals(m_searchController.getCommon().getConfig().getSolrIndex()) 372 && (null != m_addContentInfoForEntries)) { 373 CmsSolrQuery query = new CmsSolrQuery(); 374 m_searchController.addQueryParts(query, m_cms); 375 query.setStart(Integer.valueOf(0)); 376 query.setRows(m_addContentInfoForEntries); 377 query.setFields(CmsSearchField.FIELD_ID); 378 query.setFacet(false); 379 CmsContentLoadCollectorInfo info = new CmsContentLoadCollectorInfo(); 380 info.setCollectorClass(this.getClass().getName()); 381 // Somehow the normal toString() does add '+' for spaces, but keeps "real" '+' 382 // so we cannot reconstruct the correct query again. 383 // Using toQueryString() puts '+' for spaces as well, but escapes the "real" '+' 384 // so we can "repair" the query. 385 // The method adds '?' as first character, what we do not need. 386 String queryString = query.toQueryString(); 387 if (queryString.length() > 0) { 388 // Cut the leading '?' and put correct spaces in place 389 queryString = queryString.substring(1).replace('+', ' '); 390 } 391 info.setCollectorParams(queryString); 392 info.setId((new CmsUUID()).getStringValue()); 393 if (CmsJspTagEditable.getDirectEditProvider(pageContext) != null) { 394 try { 395 CmsJspTagEditable.getDirectEditProvider(pageContext).insertDirectEditListMetadata( 396 pageContext, 397 info); 398 } catch (JspException e) { 399 LOG.error("Could not write content info.", e); 400 } 401 } 402 } 403 } 404 405 /** Here the search query is composed and executed. 406 * The result is wrapped in an easily usable form. 407 * It is exposed to the JSP via the tag's "var" attribute. 408 * @return The result object exposed via the tag's attribute "var". 409 */ 410 private I_CmsSearchResultWrapper getSearchResults() { 411 412 // The second parameter is just ignored - so it does not matter 413 m_searchController.updateFromRequestParameters(pageContext.getRequest().getParameterMap(), false); 414 I_CmsSearchControllerCommon common = m_searchController.getCommon(); 415 // Do not search for empty query, if configured 416 if (common.getState().getQuery().isEmpty() 417 && (!common.getConfig().getIgnoreQueryParam() && !common.getConfig().getSearchForEmptyQueryParam())) { 418 return new CmsSearchResultWrapper(m_searchController, null, null, m_cms, null); 419 } 420 Map<String, String[]> queryParams = null; 421 boolean isEditMode = CmsJspTagEditable.isEditableRequest(pageContext.getRequest()); 422 if (isEditMode) { 423 String params = ""; 424 if (common.getConfig().getIgnoreReleaseDate()) { 425 params += "&fq=released:[* TO *]"; 426 } 427 if (common.getConfig().getIgnoreExpirationDate()) { 428 params += "&fq=expired:[* TO *]"; 429 } 430 if (!params.isEmpty()) { 431 queryParams = CmsRequestUtil.createParameterMap(params.substring(1)); 432 } 433 } 434 CmsSolrQuery query = new CmsSolrQuery(null, queryParams); 435 m_searchController.addQueryParts(query, m_cms); 436 try { 437 // use "complicated" constructor to allow more than 50 results -> set ignoreMaxResults to true 438 // also set resource filter to allow for returning unreleased/expired resources if necessary. 439 CmsSolrResultList solrResultList = m_index.search( 440 m_cms, 441 query.clone(), // use a clone of the query, since the search function manipulates the query (removes highlighting parts), but we want to keep the original one. 442 true, 443 null, 444 false, 445 isEditMode ? CmsResourceFilter.IGNORE_EXPIRATION : null, 446 m_searchController.getCommon().getConfig().getMaxReturnedResults()); 447 return new CmsSearchResultWrapper(m_searchController, solrResultList, query, m_cms, null); 448 } catch (CmsSearchException e) { 449 LOG.warn(Messages.get().getBundle().key(Messages.LOG_TAG_SEARCH_SEARCH_FAILED_0), e); 450 return new CmsSearchResultWrapper(m_searchController, null, query, m_cms, e); 451 } 452 } 453}