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