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.I18nUtils; 050import org.ametys.plugins.core.user.UserHelper; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.UnknownAmetysObjectException; 053import org.ametys.runtime.i18n.I18nizableText; 054import org.ametys.runtime.parameter.ParameterHelper; 055import org.ametys.runtime.parameter.ParameterHelper.ParameterType; 056import org.ametys.runtime.plugin.component.AbstractLogEnabled; 057import org.ametys.runtime.plugin.component.PluginAware; 058 059/** 060 * Abstract class providing standard behavior and helper methods for a 061 * {@link SystemProperty}. 062 */ 063public abstract class AbstractSystemProperty extends AbstractLogEnabled implements SystemProperty, Serviceable, Configurable, PluginAware 064{ 065 /** The i18n utils. */ 066 protected I18nUtils _i18nUtils; 067 068 /** The property ID. */ 069 protected String _id; 070 071 /** The property label. */ 072 protected I18nizableText _label; 073 074 /** The property description. */ 075 protected I18nizableText _description; 076 077 /** The property plugin name. */ 078 protected String _pluginName; 079 080 /** The user helper */ 081 protected UserHelper _userHelper; 082 /** The ametys object resolver */ 083 protected AmetysObjectResolver _resolver; 084 085 @Override 086 public void setPluginInfo(String pluginName, String featureName, String id) 087 { 088 _pluginName = pluginName; 089 _id = id; 090 } 091 092 public void service(ServiceManager manager) throws ServiceException 093 { 094 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 095 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 096 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 097 } 098 099 @Override 100 public void configure(Configuration configuration) throws ConfigurationException 101 { 102 _label = _parseI18nizableText(configuration, _pluginName, "label"); 103 _description = _parseI18nizableText(configuration, _pluginName, "description"); 104 } 105 106 @Override 107 public String getId() 108 { 109 return _id; 110 } 111 112 @Override 113 public I18nizableText getLabel() 114 { 115 return _label; 116 } 117 118 @Override 119 public I18nizableText getDescription() 120 { 121 return _description; 122 } 123 124 @SuppressWarnings("unchecked") 125 @Override 126 public void index(Content content, SolrInputDocument document) 127 { 128 Object value = getValue(content); 129 130 SearchField searchField = getSearchField(); 131 132 if (value == null || searchField == null || searchField.getName() == null) 133 { 134 // Nothing to index 135 return; 136 } 137 138 MetadataType type = getType(); 139 switch (type) 140 { 141 case STRING: 142 case CONTENT: 143 if (isMultiple()) 144 { 145 _indexStringValues(searchField, (String[]) value, document); 146 } 147 else 148 { 149 _indexStringValue(searchField, (String) value, document); 150 } 151 break; 152 case DOUBLE: 153 if (isMultiple()) 154 { 155 _indexDoubleValues(searchField, (Double[]) value, document); 156 } 157 else 158 { 159 _indexDoubleValue(searchField, (Double) value, document); 160 } 161 break; 162 case LONG: 163 if (isMultiple()) 164 { 165 _indexLongValues(searchField, (Long[]) value, document); 166 } 167 else 168 { 169 _indexLongValue(searchField, (Long) value, document); 170 } 171 break; 172 case GEOCODE: 173 _indexGeocodeValue(searchField, (Map<String, Double>) value, document); 174 break; 175 case BOOLEAN: 176 if (isMultiple()) 177 { 178 _indexBooleanValues(searchField, (Boolean[]) value, document); 179 } 180 else 181 { 182 _indexBooleanValue(searchField, (Boolean) value, document); 183 } 184 break; 185 case DATE: 186 case DATETIME: 187 if (isMultiple()) 188 { 189 _indexDateValues(searchField, (Date[]) value, document); 190 } 191 else 192 { 193 _indexDateValue(searchField, (Date) value, document); 194 } 195 break; 196 case USER: 197 if (isMultiple()) 198 { 199 _indexUserValues(searchField, (UserIdentity[]) value, document); 200 } 201 else 202 { 203 _indexUserValue(searchField, (UserIdentity) value, document); 204 } 205 break; 206 case FILE: 207 case BINARY: 208 case COMPOSITE: 209 case RICH_TEXT: 210 case REFERENCE: 211 case SUB_CONTENT: 212 case MULTILINGUAL_STRING: 213 getLogger().warn("Type '" + type + "' is not supported for indexation on a system indexing field"); 214 break; 215 default: 216 break; 217 } 218 219 // Index sort field 220 Object sortValue = getSortValue(content); 221 if (isSortable() && sortValue != null && searchField.getSortField() != null && !searchField.getName().equals(searchField.getSortField())) 222 { 223 document.setField(searchField.getSortField(), sortValue); 224 } 225 } 226 227 @Override 228 public Object getSortValue(Content content) 229 { 230 // Default to getValue(), override to provide a specific sort value. 231 Object value = getValue(content); 232 233 if (value == null) 234 { 235 return null; 236 } 237 238 if (isMultiple() && value instanceof Object[] && ((Object[]) value).length > 0) 239 { 240 return ((Object[]) value)[0]; // return the first value 241 } 242 else 243 { 244 return value; 245 } 246 } 247 248 @SuppressWarnings("unchecked") 249 public void saxValue(ContentHandler handler, Content content) throws SAXException 250 { 251 Object value = getValue(content); 252 253 if (value == null) 254 { 255 return; 256 } 257 258 MetadataType type = getType(); 259 switch (type) 260 { 261 case USER: 262 if (value instanceof UserIdentity[]) 263 { 264 for (UserIdentity identity : (UserIdentity[]) value) 265 { 266 _saxUserIdentityValue(handler, identity); 267 } 268 } 269 else if (value instanceof UserIdentity) 270 { 271 _saxUserIdentityValue(handler, (UserIdentity) value); 272 } 273 break; 274 case CONTENT: 275 Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : null; 276 if (isMultiple()) 277 { 278 for (String id : (String[]) value) 279 { 280 _saxContentValue(handler, id, locale); 281 } 282 } 283 else 284 { 285 _saxContentValue(handler, (String) value, locale); 286 } 287 break; 288 case GEOCODE: 289 _saxGeocodeValue(handler, (Map<String, Double>) value); 290 break; 291 case BOOLEAN: 292 case DOUBLE: 293 case LONG: 294 case DATE: 295 case DATETIME: 296 case STRING: 297 if (isMultiple()) 298 { 299 for (Object v : (Object[]) value) 300 { 301 _saxSingleValue(handler, v); 302 } 303 } 304 else 305 { 306 _saxSingleValue(handler, value); 307 } 308 break; 309 case BINARY: 310 case COMPOSITE: 311 case REFERENCE: 312 case RICH_TEXT: 313 case SUB_CONTENT: 314 case MULTILINGUAL_STRING: 315 default: 316 getLogger().warn("Type '" + type + "' is not supported for saxing on a system indexing field"); 317 break; 318 319 } 320 } 321 322 private void _saxContentValue(ContentHandler handler, String value, Locale locale) throws SAXException 323 { 324 try 325 { 326 Content content = _resolver.resolveById(value); 327 328 AttributesImpl attrs = new AttributesImpl(); 329 attrs.addCDATAAttribute("id", content.getId()); 330 attrs.addCDATAAttribute("name", content.getName()); 331 attrs.addCDATAAttribute("title", content.getTitle(locale)); 332 if (content.getLanguage() != null) 333 { 334 attrs.addCDATAAttribute("language", content.getLanguage()); 335 } 336 attrs.addCDATAAttribute("createdAt", ParameterHelper.valueToString(content.getCreationDate())); 337 attrs.addCDATAAttribute("creator", content.getCreator().getLogin()); 338 attrs.addCDATAAttribute("lastModifiedAt", ParameterHelper.valueToString(content.getLastModified())); 339 XMLUtils.createElement(handler, getId(), attrs); 340 } 341 catch (UnknownAmetysObjectException e) 342 { 343 if (getLogger().isWarnEnabled()) 344 { 345 getLogger().warn("The system property'" + getId() + "' references a non-existing content '" + value + "'. It will be ignored.", e); 346 } 347 } 348 } 349 350 private void _saxUserIdentityValue(ContentHandler handler, UserIdentity value) throws SAXException 351 { 352 User user = _userHelper.getUser(value); 353 if (user != null) 354 { 355 AttributesImpl attrs = new AttributesImpl(); 356 attrs.addCDATAAttribute("login", value.getLogin()); 357 attrs.addCDATAAttribute("populationId", value.getPopulationId()); 358 attrs.addCDATAAttribute("email", user.getEmail()); 359 XMLUtils.createElement(handler, getId(), attrs, user.getFullName()); 360 } 361 } 362 363 private void _saxGeocodeValue(ContentHandler handler, Map<String, Double> value) throws SAXException 364 { 365 AttributesImpl attrs = new AttributesImpl(); 366 attrs.addCDATAAttribute("longitude", String.valueOf(value.get("longitude"))); 367 attrs.addCDATAAttribute("latitude", String.valueOf(value.get("latitude"))); 368 369 XMLUtils.createElement(handler, getId(), attrs); 370 } 371 372 private void _saxSingleValue(ContentHandler handler, Object value) throws SAXException 373 { 374 XMLUtils.createElement(handler, getId(), ParameterHelper.valueToString(value)); 375 } 376 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 Date parseDate(Object value) 520 { 521 if (value instanceof Date) 522 { 523 return (Date) value; 524 } 525 else if (value instanceof String) 526 { 527 return (Date) ParameterHelper.castValue((String) value, ParameterType.DATE); 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}