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 = 200; 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 // use "complicated" constructor to allow more than 50 results -> set ignoreMaxResults to true 151 // adjust the CmsObject to prevent unintended filtering of resources 152 CmsSolrResultList offlineResults = solrOffline.search( 153 CmsPublishListHelper.adjustCmsObject(cms, false), 154 new CmsSolrQuery(null, CmsRequestUtil.createParameterMap(info.getCollectorParams())), 155 true); 156 Set<String> offlineIds = new HashSet<String>(offlineResults.size()); 157 for (CmsSearchResource offlineResult : offlineResults) { 158 offlineIds.add(offlineResult.getField(CmsSearchField.FIELD_ID)); 159 } 160 for (String id : offlineIds) { 161 CmsResource resource = cms.readResource(new CmsUUID(id)); 162 if (!(resource.getState().isUnchanged())) { 163 result.add(resource); 164 } 165 } 166 CmsSolrResultList onlineResults = solrOnline.search( 167 CmsPublishListHelper.adjustCmsObject(cms, true), 168 new CmsSolrQuery(null, CmsRequestUtil.createParameterMap(info.getCollectorParams())), 169 true); 170 Set<String> deletedIds = new HashSet<String>(onlineResults.size()); 171 for (CmsSearchResource onlineResult : onlineResults) { 172 String uuid = onlineResult.getField(CmsSearchField.FIELD_ID); 173 if (!offlineIds.contains(uuid)) { 174 deletedIds.add(uuid); 175 } 176 } 177 for (String uuid : deletedIds) { 178 CmsResource resource = cms.readResource(new CmsUUID(uuid)); 179 if (!(resource.getState().isUnchanged())) { 180 result.add(resource); 181 } 182 } 183 } catch (CmsSearchException e) { 184 LOG.warn(Messages.get().getBundle().key(Messages.LOG_TAG_SEARCH_SEARCH_FAILED_0), e); 185 } 186 return result; 187 } 188 189 /** 190 * @see javax.servlet.jsp.tagext.BodyTagSupport#doEndTag() 191 */ 192 @Override 193 public int doEndTag() throws JspException { 194 195 release(); 196 return super.doEndTag(); 197 } 198 199 /** 200 * @see javax.servlet.jsp.tagext.Tag#doStartTag() 201 */ 202 @Override 203 public int doStartTag() throws JspException, CmsIllegalArgumentException { 204 205 // initialize the content load tag 206 init(); 207 addContentInfo(); 208 return EVAL_BODY_INCLUDE; 209 } 210 211 /** Get the value of the specified configuration file (given via the tag's "configFile" attribute). 212 * @return The config file. 213 */ 214 public Object getConfigFile() { 215 216 return m_configFile; 217 } 218 219 /** Getter for the "configString". 220 * @return The "configString". 221 */ 222 public String getConfigString() { 223 224 return m_configString; 225 } 226 227 /** Get the value of the specified format of the configuration file (given via the tag's "fileFormat" attribute). 228 * @return The file format. 229 */ 230 public String getFileFormat() { 231 232 return m_fileFormat.toString(); 233 } 234 235 /** 236 * @see org.opencms.file.collectors.I_CmsCollectorPublishListProvider#getPublishResources(org.opencms.file.CmsObject, org.opencms.gwt.shared.I_CmsContentLoadCollectorInfo) 237 */ 238 public Set<CmsResource> getPublishResources(CmsObject cms, I_CmsContentLoadCollectorInfo info) throws CmsException { 239 240 return getPublishResourcesInternal(cms, info); 241 } 242 243 /** 244 * @see javax.servlet.jsp.tagext.Tag#release() 245 */ 246 @Override 247 public void release() { 248 249 m_cms = null; 250 m_configFile = null; 251 setConfigString(null); 252 m_searchController = null; 253 m_index = null; 254 m_controller = null; 255 super.release(); 256 } 257 258 /** Setter for "addContentInfo", indicating if content information should be added. 259 * @param doAddInfo The value of the "addContentInfo" attribute of the tag 260 */ 261 public void setAddContentInfo(final Boolean doAddInfo) { 262 263 if ((null != doAddInfo) && doAddInfo.booleanValue() && (null != m_addContentInfoForEntries)) { 264 m_addContentInfoForEntries = Integer.valueOf(DEFAULT_CONTENTINFO_ROWS); 265 } 266 } 267 268 /** Setter for the configuration file. 269 * @param fileName Name of the configuration file to use for the search. 270 */ 271 public void setConfigFile(Object fileName) { 272 273 m_configFile = fileName; 274 } 275 276 /** Setter for the "configString". 277 * @param configString The "configString". 278 */ 279 public void setConfigString(final String configString) { 280 281 m_configString = configString; 282 } 283 284 /** Setter for "contentInfoMaxItems". 285 * @param maxItems number of items to maximally check for alterations. 286 */ 287 public void setContentInfoMaxItems(Integer maxItems) { 288 289 if (null != maxItems) { 290 m_addContentInfoForEntries = maxItems; 291 } 292 } 293 294 /** Setter for the file format. 295 * @param fileFormat File format the configuration file is in. 296 */ 297 public void setFileFormat(String fileFormat) { 298 299 if (fileFormat.toUpperCase().equals(FileFormat.JSON.toString())) { 300 m_fileFormat = FileFormat.JSON; 301 } 302 } 303 304 /** 305 * Initializes this formatter tag. 306 * <p> 307 * 308 * @throws JspException 309 * in case something goes wrong 310 */ 311 protected void init() throws JspException { 312 313 // initialize OpenCms access objects 314 m_controller = CmsFlexController.getController(pageContext.getRequest()); 315 m_cms = m_controller.getCmsObject(); 316 317 try { 318 I_CmsSearchConfiguration config = null; 319 if (m_configFile != null) { 320 CmsFile configFile = m_cms.readFile(CmsJspElFunctions.convertRawResource(m_cms, m_configFile)); 321 if (m_fileFormat == FileFormat.JSON) { 322 // read the JSON config file 323 OpenCms.getLocaleManager(); 324 String configString = new String( 325 configFile.getContents(), 326 CmsLocaleManager.getResourceEncoding(m_cms, configFile)); 327 config = new CmsSearchConfiguration(new CmsJSONSearchConfigurationParser(configString)); 328 } else { // assume XML 329 CmsXmlContent xmlContent = CmsXmlContentFactory.unmarshal(m_cms, configFile); 330 config = new CmsSearchConfiguration( 331 new CmsXMLSearchConfigurationParser(xmlContent, m_cms.getRequestContext().getLocale())); 332 } 333 } 334 if (m_configString != null) { 335 if (m_configString.trim().startsWith("{")) { 336 config = new CmsSearchConfiguration(new CmsJSONSearchConfigurationParser(m_configString, config)); 337 } else { 338 config = new CmsSearchConfiguration( 339 new CmsPlainQuerySearchConfigurationParser(m_configString, config)); 340 } 341 } 342 m_searchController = new CmsSearchController(config); 343 344 String indexName = m_searchController.getCommon().getConfig().getSolrIndex(); 345 // try to use configured index 346 if ((indexName != null) && !indexName.trim().isEmpty()) { 347 m_index = OpenCms.getSearchManager().getIndexSolr(indexName); 348 } 349 // if not successful, use the following default 350 if (m_index == null) { 351 m_index = OpenCms.getSearchManager().getIndexSolr( 352 m_cms.getRequestContext().getCurrentProject().isOnlineProject() 353 ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE 354 : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE); 355 } 356 357 storeAttribute(getVar(), getSearchResults()); 358 359 } catch (Exception e) { // CmsException | UnsupportedEncodingException | JSONException 360 LOG.error(e.getLocalizedMessage(), e); 361 m_controller.setThrowable(e, m_cms.getRequestContext().getUri()); 362 throw new JspException(e); 363 } 364 } 365 366 /** 367 * Adds the content info for the collected resources used in the "This page" publish dialog. 368 */ 369 private void addContentInfo() { 370 371 if (!m_cms.getRequestContext().getCurrentProject().isOnlineProject() 372 && (null == m_searchController.getCommon().getConfig().getSolrIndex()) 373 && (null != m_addContentInfoForEntries)) { 374 CmsSolrQuery query = new CmsSolrQuery(); 375 m_searchController.addQueryParts(query, m_cms); 376 query.setStart(Integer.valueOf(0)); 377 query.setRows(m_addContentInfoForEntries); 378 CmsContentLoadCollectorInfo info = new CmsContentLoadCollectorInfo(); 379 info.setCollectorClass(this.getClass().getName()); 380 info.setCollectorParams(query.getQuery()); 381 info.setId((new CmsUUID()).getStringValue()); 382 if (CmsJspTagEditable.getDirectEditProvider(pageContext) != null) { 383 try { 384 CmsJspTagEditable.getDirectEditProvider(pageContext).insertDirectEditListMetadata( 385 pageContext, 386 info); 387 } catch (JspException e) { 388 LOG.error("Could not write content info.", e); 389 } 390 } 391 } 392 } 393 394 /** Here the search query is composed and executed. 395 * The result is wrapped in an easily usable form. 396 * It is exposed to the JSP via the tag's "var" attribute. 397 * @return The result object exposed via the tag's attribute "var". 398 */ 399 private I_CmsSearchResultWrapper getSearchResults() { 400 401 // The second parameter is just ignored - so it does not matter 402 m_searchController.updateFromRequestParameters(pageContext.getRequest().getParameterMap(), false); 403 I_CmsSearchControllerCommon common = m_searchController.getCommon(); 404 // Do not search for empty query, if configured 405 if (common.getState().getQuery().isEmpty() 406 && (!common.getConfig().getIgnoreQueryParam() && !common.getConfig().getSearchForEmptyQueryParam())) { 407 return new CmsSearchResultWrapper(m_searchController, null, null, m_cms, null); 408 } 409 Map<String, String[]> queryParams = null; 410 boolean isEditMode = CmsJspTagEditable.isEditableRequest(pageContext.getRequest()); 411 if (isEditMode) { 412 String params = ""; 413 if (common.getConfig().getIgnoreReleaseDate()) { 414 params += "&fq=released:[* TO *]"; 415 } 416 if (common.getConfig().getIgnoreExpirationDate()) { 417 params += "&fq=expired:[* TO *]"; 418 } 419 if (!params.isEmpty()) { 420 queryParams = CmsRequestUtil.createParameterMap(params.substring(1)); 421 } 422 } 423 CmsSolrQuery query = new CmsSolrQuery(null, queryParams); 424 m_searchController.addQueryParts(query, m_cms); 425 try { 426 // use "complicated" constructor to allow more than 50 results -> set ignoreMaxResults to true 427 // also set resource filter to allow for returning unreleased/expired resources if necessary. 428 CmsSolrResultList solrResultList = m_index.search( 429 m_cms, 430 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. 431 true, 432 isEditMode ? CmsResourceFilter.IGNORE_EXPIRATION : null); 433 return new CmsSearchResultWrapper(m_searchController, solrResultList, query, m_cms, null); 434 } catch (CmsSearchException e) { 435 LOG.warn(Messages.get().getBundle().key(Messages.LOG_TAG_SEARCH_SEARCH_FAILED_0), e); 436 return new CmsSearchResultWrapper(m_searchController, null, query, m_cms, e); 437 } 438 } 439}