001/* 002 * Copyright 2015 Anyware Services 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 */ 016package org.ametys.cms.search.systemprop; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Date; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024 025import org.apache.avalon.framework.configuration.Configurable; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.cocoon.xml.AttributesImpl; 032import org.apache.cocoon.xml.XMLUtils; 033import org.apache.solr.common.SolrInputDocument; 034import org.xml.sax.ContentHandler; 035import org.xml.sax.SAXException; 036 037import org.ametys.cms.content.indexing.solr.SolrFieldHelper; 038import org.ametys.cms.content.indexing.solr.SolrIndexer; 039import org.ametys.cms.contenttype.MetadataType; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.search.SearchField; 042import org.ametys.cms.search.model.SystemProperty; 043import org.ametys.cms.search.solr.schema.CopyFieldDefinition; 044import org.ametys.cms.search.solr.schema.FieldDefinition; 045import org.ametys.cms.search.solr.schema.SchemaDefinition; 046import org.ametys.cms.search.solr.schema.SchemaHelper; 047import org.ametys.core.user.User; 048import org.ametys.core.user.UserIdentity; 049import org.ametys.core.user.UserManager; 050import org.ametys.core.util.DateUtils; 051import org.ametys.core.util.I18nUtils; 052import org.ametys.core.util.date.AdaptableDate; 053import org.ametys.core.util.date.AdaptableDateParser; 054import org.ametys.plugins.core.user.UserHelper; 055import org.ametys.plugins.repository.AmetysObjectResolver; 056import org.ametys.plugins.repository.UnknownAmetysObjectException; 057import org.ametys.runtime.i18n.I18nizableText; 058import org.ametys.runtime.parameter.ParameterHelper; 059import org.ametys.runtime.plugin.component.AbstractLogEnabled; 060import org.ametys.runtime.plugin.component.PluginAware; 061 062/** 063 * Abstract class providing standard behavior and helper methods for a 064 * {@link SystemProperty}. 065 */ 066public abstract class AbstractSystemProperty extends AbstractLogEnabled implements SystemProperty, Serviceable, Configurable, PluginAware 067{ 068 /** The i18n utils. */ 069 protected I18nUtils _i18nUtils; 070 071 /** The property ID. */ 072 protected String _id; 073 074 /** The property label. */ 075 protected I18nizableText _label; 076 077 /** The property description. */ 078 protected I18nizableText _description; 079 080 /** The property plugin name. */ 081 protected String _pluginName; 082 083 /** The user helper */ 084 protected UserHelper _userHelper; 085 /** The ametys object resolver */ 086 protected AmetysObjectResolver _resolver; 087 /** The user manager */ 088 protected UserManager _userManager; 089 090 @Override 091 public void setPluginInfo(String pluginName, String featureName, String id) 092 { 093 _pluginName = pluginName; 094 _id = id; 095 } 096 097 public void service(ServiceManager manager) throws ServiceException 098 { 099 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 100 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 101 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 102 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 103 } 104 105 @Override 106 public void configure(Configuration configuration) throws ConfigurationException 107 { 108 _label = _parseI18nizableText(configuration, _pluginName, "label"); 109 _description = _parseI18nizableText(configuration, _pluginName, "description"); 110 } 111 112 @Override 113 public String getId() 114 { 115 return _id; 116 } 117 118 @Override 119 public I18nizableText getLabel() 120 { 121 return _label; 122 } 123 124 @Override 125 public I18nizableText getDescription() 126 { 127 return _description; 128 } 129 130 @SuppressWarnings("unchecked") 131 @Override 132 public void index(Content content, SolrInputDocument document) 133 { 134 Object value = getValue(content); 135 136 SearchField searchField = getSearchField(); 137 138 if (value == null || searchField == null || searchField.getName() == null) 139 { 140 // Nothing to index 141 return; 142 } 143 144 MetadataType type = getType(); 145 switch (type) 146 { 147 case STRING: 148 case CONTENT: 149 if (isMultiple()) 150 { 151 _indexStringValues(searchField, (String[]) value, document); 152 } 153 else 154 { 155 _indexStringValue(searchField, (String) value, document); 156 } 157 break; 158 case DOUBLE: 159 if (isMultiple()) 160 { 161 _indexDoubleValues(searchField, (Double[]) value, document); 162 } 163 else 164 { 165 _indexDoubleValue(searchField, (Double) value, document); 166 } 167 break; 168 case LONG: 169 if (isMultiple()) 170 { 171 _indexLongValues(searchField, (Long[]) value, document); 172 } 173 else 174 { 175 _indexLongValue(searchField, (Long) value, document); 176 } 177 break; 178 case GEOCODE: 179 _indexGeocodeValue(searchField, (Map<String, Double>) value, document); 180 break; 181 case BOOLEAN: 182 if (isMultiple()) 183 { 184 _indexBooleanValues(searchField, (Boolean[]) value, document); 185 } 186 else 187 { 188 _indexBooleanValue(searchField, (Boolean) value, document); 189 } 190 break; 191 case DATE: 192 case DATETIME: 193 if (isMultiple()) 194 { 195 _indexDateValues(searchField, (Date[]) value, document); 196 } 197 else 198 { 199 _indexDateValue(searchField, (Date) value, document); 200 } 201 break; 202 case USER: 203 if (isMultiple()) 204 { 205 _indexUserValues(searchField, (UserIdentity[]) value, document); 206 } 207 else 208 { 209 _indexUserValue(searchField, (UserIdentity) value, document); 210 } 211 break; 212 case FILE: 213 case BINARY: 214 case COMPOSITE: 215 case RICH_TEXT: 216 case REFERENCE: 217 case SUB_CONTENT: 218 case MULTILINGUAL_STRING: 219 getLogger().warn("Type '" + type + "' is not supported for indexation on a system indexing field"); 220 break; 221 default: 222 break; 223 } 224 225 // Index sort field 226 Object sortValue = getSortValue(content); 227 if (isSortable() && sortValue != null && searchField.getSortField() != null && !searchField.getName().equals(searchField.getSortField())) 228 { 229 document.setField(searchField.getSortField(), sortValue); 230 } 231 } 232 233 @Override 234 public Object getSortValue(Content content) 235 { 236 // Default to getValue(), override to provide a specific sort value. 237 Object value = getValue(content); 238 239 if (value == null) 240 { 241 return null; 242 } 243 244 if (isMultiple() && value instanceof Object[] && ((Object[]) value).length > 0) 245 { 246 return ((Object[]) value)[0]; // return the first value 247 } 248 else 249 { 250 return value; 251 } 252 } 253 254 @SuppressWarnings("unchecked") 255 public void saxValue(ContentHandler handler, Content content) throws SAXException 256 { 257 Object value = getValue(content); 258 259 if (value == null) 260 { 261 return; 262 } 263 264 MetadataType type = getType(); 265 switch (type) 266 { 267 case USER: 268 if (value instanceof UserIdentity[]) 269 { 270 for (UserIdentity identity : (UserIdentity[]) value) 271 { 272 _saxUserIdentityValue(handler, identity); 273 } 274 } 275 else if (value instanceof UserIdentity) 276 { 277 _saxUserIdentityValue(handler, (UserIdentity) value); 278 } 279 break; 280 case CONTENT: 281 Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : null; 282 if (isMultiple()) 283 { 284 for (String id : (String[]) value) 285 { 286 _saxContentValue(handler, id, locale); 287 } 288 } 289 else 290 { 291 _saxContentValue(handler, (String) value, locale); 292 } 293 break; 294 case GEOCODE: 295 _saxGeocodeValue(handler, (Map<String, Double>) value); 296 break; 297 case BOOLEAN: 298 case DOUBLE: 299 case LONG: 300 case DATE: 301 case DATETIME: 302 case STRING: 303 if (isMultiple()) 304 { 305 for (Object v : (Object[]) value) 306 { 307 _saxSingleValue(handler, v); 308 } 309 } 310 else 311 { 312 _saxSingleValue(handler, value); 313 } 314 break; 315 case BINARY: 316 case COMPOSITE: 317 case REFERENCE: 318 case RICH_TEXT: 319 case SUB_CONTENT: 320 case MULTILINGUAL_STRING: 321 default: 322 getLogger().warn("Type '" + type + "' is not supported for saxing on a system indexing field"); 323 break; 324 325 } 326 } 327 328 private void _saxContentValue(ContentHandler handler, String value, Locale locale) throws SAXException 329 { 330 try 331 { 332 Content content = _resolver.resolveById(value); 333 334 AttributesImpl attrs = new AttributesImpl(); 335 attrs.addCDATAAttribute("id", content.getId()); 336 attrs.addCDATAAttribute("name", content.getName()); 337 attrs.addCDATAAttribute("title", content.getTitle(locale)); 338 if (content.getLanguage() != null) 339 { 340 attrs.addCDATAAttribute("language", content.getLanguage()); 341 } 342 attrs.addCDATAAttribute("createdAt", DateUtils.dateToString(content.getCreationDate())); 343 attrs.addCDATAAttribute("creator", content.getCreator().getLogin()); 344 attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified())); 345 XMLUtils.createElement(handler, getId(), attrs); 346 } 347 catch (UnknownAmetysObjectException e) 348 { 349 if (getLogger().isWarnEnabled()) 350 { 351 getLogger().warn("The system property'" + getId() + "' references a non-existing content '" + value + "'. It will be ignored.", e); 352 } 353 } 354 } 355 356 private void _saxUserIdentityValue(ContentHandler handler, UserIdentity value) throws SAXException 357 { 358 User user = _userManager.getUser(value); 359 if (user != null) 360 { 361 AttributesImpl attrs = new AttributesImpl(); 362 attrs.addCDATAAttribute("login", value.getLogin()); 363 attrs.addCDATAAttribute("populationId", value.getPopulationId()); 364 attrs.addCDATAAttribute("email", user.getEmail()); 365 XMLUtils.createElement(handler, getId(), attrs, user.getFullName()); 366 } 367 } 368 369 private void _saxGeocodeValue(ContentHandler handler, Map<String, Double> value) throws SAXException 370 { 371 AttributesImpl attrs = new AttributesImpl(); 372 attrs.addCDATAAttribute("longitude", String.valueOf(value.get("longitude"))); 373 attrs.addCDATAAttribute("latitude", String.valueOf(value.get("latitude"))); 374 375 XMLUtils.createElement(handler, getId(), attrs); 376 } 377 378 private void _saxSingleValue(ContentHandler handler, Object value) throws SAXException 379 { 380 XMLUtils.createElement(handler, getId(), ParameterHelper.valueToString(value)); 381 } 382 383 @Override 384 public Collection<SchemaDefinition> getSchemaDefinitions() 385 { 386 List<SchemaDefinition> definitions = new ArrayList<>(); 387 388 SearchField searchField = getSearchField(); 389 if (searchField != null) 390 { 391 String name = searchField.getName(); 392 String sortFieldName = searchField.getSortField(); 393 String facetFieldName = searchField.getFacetField(); 394 boolean multiple = isMultiple(); 395 String type = SchemaHelper.getSchemaType(getType()); 396 397 if (type != null) 398 { 399 definitions.add(new FieldDefinition(name, type, multiple, false)); 400 401 if (sortFieldName != null && !sortFieldName.equals(name)) 402 { 403 definitions.add(new FieldDefinition(sortFieldName, type, false, false)); 404 } 405 406 if (facetFieldName != null && !facetFieldName.equals(name)) 407 { 408 // By default the index value in field name will be automatically copy in facet field 409 // So we do not need the index facet field manually 410 definitions.add(new FieldDefinition(facetFieldName, type, multiple, true)); 411 definitions.add(new CopyFieldDefinition(name, facetFieldName)); 412 } 413 } 414 } 415 416 return definitions; 417 } 418 419 /** 420 * Parse an i18n text. 421 * 422 * @param config the configuration to use. 423 * @param pluginName the current plugin name. 424 * @param name the child name. 425 * @return the i18n text. 426 * @throws ConfigurationException if the configuration is not valid. 427 */ 428 protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException 429 { 430 return I18nizableText.parseI18nizableText(config.getChild(name), "plugin." + pluginName); 431 } 432 433 /** 434 * Get the value as a long. 435 * 436 * @param value The value as an object. 437 * @return The value as a long. 438 */ 439 protected String parseString(Object value) 440 { 441 if (value instanceof String) 442 { 443 return (String) value; 444 } 445 else 446 { 447 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid String value."); 448 } 449 } 450 451 /** 452 * Get the value as a long. 453 * 454 * @param value The value as an object. 455 * @return The value as a long. 456 */ 457 protected long parseLong(Object value) 458 { 459 if (value instanceof Number) 460 { 461 return ((Number) value).longValue(); 462 } 463 else if (value instanceof String) 464 { 465 return Long.parseLong((String) value); 466 } 467 else 468 { 469 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid long value."); 470 } 471 } 472 473 /** 474 * Get the value as a double. 475 * 476 * @param value The value as an object. 477 * @return The value as a double. 478 */ 479 protected double parseDouble(Object value) 480 { 481 if (value instanceof Number) 482 { 483 return ((Number) value).doubleValue(); 484 } 485 else if (value instanceof String) 486 { 487 return Double.parseDouble((String) value); 488 } 489 else 490 { 491 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid double value."); 492 } 493 } 494 495 /** 496 * Get the value as a boolean. 497 * 498 * @param value The value as an object, can be a Boolean or a String. 499 * @return The value as a boolean. 500 */ 501 protected boolean parseBoolean(Object value) 502 { 503 if (value instanceof Boolean) 504 { 505 return (Boolean) value; 506 } 507 else if (value instanceof String) 508 { 509 return Boolean.parseBoolean((String) value); 510 } 511 else 512 { 513 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid boolean value."); 514 } 515 } 516 517 /** 518 * Get the value as a date. 519 * 520 * @param value The value as an object, can be a Date or a String. 521 * @return The value as a Date object. 522 */ 523 protected AdaptableDate parseDate(Object value) 524 { 525 if (value instanceof AdaptableDate) 526 { 527 return (AdaptableDate) value; 528 } 529 else if (value instanceof String) 530 { 531 return AdaptableDateParser.parse((String) value); 532 } 533 else 534 { 535 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid date value."); 536 } 537 } 538 539 /** 540 * Get the value as array of {@link UserIdentity} 541 * @param value The value to parse 542 * @return A array of {@link UserIdentity} 543 */ 544 @SuppressWarnings("unchecked") 545 protected UserIdentity[] parseUserArray (Object value) 546 { 547 if (value instanceof UserIdentity) 548 { 549 return new UserIdentity[] {(UserIdentity) value}; 550 } 551 else if (value instanceof UserIdentity[]) 552 { 553 return (UserIdentity[]) value; 554 } 555 else if (value instanceof List<?>) 556 { 557 return (UserIdentity[]) ((List<?>) value).stream().map(v -> _userHelper.json2userIdentity((Map<String, String>) v)).toArray(); 558 } 559 else if (value instanceof Map) 560 { 561 UserIdentity userIdentity = _userHelper.json2userIdentity((Map<String, String>) value); 562 if (userIdentity != null) 563 { 564 return new UserIdentity[] {userIdentity}; 565 } 566 return new UserIdentity[0]; 567 } 568 else 569 { 570 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid UserIdentity values."); 571 } 572 } 573 574 /** 575 * Get the value as a array of String 576 * @param value The value as an object to parse 577 * @return The values as a String array 578 */ 579 protected String[] parseStringArray(Object value) 580 { 581 if (value instanceof String) 582 { 583 return new String[] {(String) value}; 584 } 585 else if (value instanceof String[]) 586 { 587 return (String[]) value; 588 } 589 else if (value instanceof List<?>) 590 { 591 return ((List<?>) value).stream() 592 .map(v -> String.valueOf(v)) 593 .toArray(String[]::new); 594 } 595 else 596 { 597 throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid String[] values."); 598 } 599 } 600 601 // ------------------------------------------------------ 602 // INDEX 603 // ------------------------------------------------------ 604 /** 605 * Index multiple String values 606 * @param field The search field 607 * @param values The values 608 * @param document The Solr document to index into 609 */ 610 protected void _indexStringValues (SearchField field, String[] values, SolrInputDocument document) 611 { 612 document.setField(field.getName(), values); 613 } 614 615 /** 616 * Index String value 617 * @param field The search field 618 * @param value The value 619 * @param document The Solr document to index into 620 */ 621 protected void _indexStringValue (SearchField field, String value, SolrInputDocument document) 622 { 623 document.setField(field.getName(), value); 624 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 625 } 626 627 /** 628 * Index multiple Date values 629 * @param field The search field 630 * @param values The values 631 * @param document The Solr document to index into 632 */ 633 protected void _indexDateValues (SearchField field, Date[] values, SolrInputDocument document) 634 { 635 document.setField(field.getName(), values); 636 } 637 638 /** 639 * Index Date value 640 * @param field The search field 641 * @param value The value 642 * @param document The Solr document to index into 643 */ 644 protected void _indexDateValue (SearchField field, Date value, SolrInputDocument document) 645 { 646 document.setField(field.getName(), SolrIndexer.dateFormat().format(value)); 647 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 648 } 649 650 /** 651 * Index multiple Long values 652 * @param field The search field 653 * @param values The values 654 * @param document The Solr document to index into 655 */ 656 protected void _indexLongValues(SearchField field, Long[] values, SolrInputDocument document) 657 { 658 document.setField(field.getName(), values); 659 } 660 661 /** 662 * Index Long value 663 * @param field The search field 664 * @param value The value 665 * @param document The Solr document to index into 666 */ 667 protected void _indexLongValue(SearchField field, Long value, SolrInputDocument document) 668 { 669 document.setField(field.getName(), value); 670 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 671 } 672 673 /** 674 * Index multiple Double values 675 * @param field The search field 676 * @param values The values 677 * @param document The Solr document to index into 678 */ 679 protected void _indexDoubleValues(SearchField field, Double[] values, SolrInputDocument document) 680 { 681 document.setField(field.getName(), values); 682 } 683 684 /** 685 * Index Double value 686 * @param field The search field 687 * @param value The value 688 * @param document The Solr document to index into 689 */ 690 protected void _indexDoubleValue(SearchField field, Double value, SolrInputDocument document) 691 { 692 document.setField(field.getName(), value); 693 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 694 } 695 696 /** 697 * Index multiple Boolean values 698 * @param field The search field 699 * @param values The values 700 * @param document The Solr document to index into 701 */ 702 protected void _indexBooleanValues(SearchField field, Boolean[] values, SolrInputDocument document) 703 { 704 document.setField(field.getName(), values); 705 } 706 707 /** 708 * Index Boolean value 709 * @param field The search field 710 * @param value The value 711 * @param document The Solr document to index into 712 */ 713 protected void _indexBooleanValue(SearchField field, Boolean value, SolrInputDocument document) 714 { 715 document.setField(field.getName(), value); 716 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 717 } 718 719 /** 720 * Index multiple UserIdentity values 721 * @param field The search field 722 * @param values The values 723 * @param document The Solr document to index into 724 */ 725 protected void _indexUserValues(SearchField field, UserIdentity[] values, SolrInputDocument document) 726 { 727 document.setField(field.getName(), values); 728 } 729 730 /** 731 * Index UserIdentity value 732 * @param field The search field 733 * @param value The value 734 * @param document The Solr document to index into 735 */ 736 protected void _indexUserValue(SearchField field, UserIdentity value, SolrInputDocument document) 737 { 738 document.setField(field.getName(), UserIdentity.userIdentityToString(value)); 739 // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions) 740 } 741 742 /** 743 * Index geocode value 744 * @param field The search field 745 * @param value The value 746 * @param document The Solr document to index into 747 */ 748 protected void _indexGeocodeValue(SearchField field, Map<String, Double> value, SolrInputDocument document) 749 { 750 double longitude = value.get("longitude"); 751 double latitude = value.get("latitude"); 752 753 document.setField(field.getName() + "$longitude_d", longitude); 754 document.setField(field.getName() + "$latitude_d", latitude); 755 756 String geoFieldName = SolrFieldHelper.getIndexingFieldName(MetadataType.GEOCODE, field.getName()); 757 document.setField(geoFieldName, longitude + " " + latitude); 758 } 759 760 761}