001/* 002 * Copyright 2017 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.plugins.extraction.component; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Set; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030import java.util.stream.Collectors; 031 032import org.apache.avalon.framework.configuration.Configuration; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.xml.sax.ContentHandler; 037 038import org.ametys.cms.content.ContentHelper; 039import org.ametys.cms.contenttype.ContentConstants; 040import org.ametys.cms.contenttype.ContentType; 041import org.ametys.cms.contenttype.MetadataDefinition; 042import org.ametys.cms.contenttype.MetadataType; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.search.GetQueryFromJSONHelper; 045import org.ametys.cms.search.QueryBuilder; 046import org.ametys.cms.search.content.ContentSearcherFactory; 047import org.ametys.cms.search.content.ContentSearcherFactory.SimpleContentSearcher; 048import org.ametys.cms.search.model.SystemProperty; 049import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 050import org.ametys.cms.search.query.Query.Operator; 051import org.ametys.cms.search.query.QuerySyntaxException; 052import org.ametys.cms.search.query.StringQuery; 053import org.ametys.cms.search.solr.SolrQuerySearchAction; 054import org.ametys.cms.search.ui.model.SearchUIModel; 055import org.ametys.core.util.JSONUtils; 056import org.ametys.core.util.LambdaUtils; 057import org.ametys.core.util.StringUtils; 058import org.ametys.plugins.extraction.execution.ExtractionExecutionContext; 059import org.ametys.plugins.extraction.execution.ExtractionExecutionContextHierarchyElement; 060import org.ametys.plugins.queriesdirectory.Query; 061import org.ametys.plugins.repository.AmetysObjectIterable; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.plugins.repository.EmptyIterable; 064import org.ametys.plugins.thesaurus.ThesaurusDAO; 065 066/** 067 * This class represents an extraction component with a solr query 068 */ 069public abstract class AbstractSolrExtractionComponent extends AbstractExtractionComponent 070{ 071 /** 072 * Regex used to extract variables from a join expression: \$\{(\.\.(?:\/\.\.)*(?:\/[^\/}]+)?)\} 073 * a variable is inside a ${} 074 * variable starts with .. (to get the direct parent), 075 * has several /.. (to get parent of parent of (...)) 076 * and can have a /metadataName (to specify the metadata to join on) 077 */ 078 private static final String EXTRACT_JOIN_VARIABLES_REGEX = "\\$\\{(\\.\\.(?:\\/\\.\\.)*(?:\\/[^\\/}]+)?)\\}"; 079 080 /** Content types concerned by the solr search */ 081 protected Set<String> _contentTypes = new HashSet<>(); 082 083 /** Reference id of a recorded query */ 084 protected String _queryReferenceId; 085 086 /** The list of clauses */ 087 protected List<ExtractionClause> _clauses = new ArrayList<>(); 088 089 /** Helper to resolve referenced query infos */ 090 protected GetQueryFromJSONHelper _getQueryFromJSONHelper; 091 092 /** Util class to manipulate JSON String */ 093 protected JSONUtils _jsonUtils; 094 095 private AmetysObjectResolver _resolver; 096 private SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 097 private ContentHelper _contentHelper; 098 private ContentSearcherFactory _contentSearcherFactory; 099 private QueryBuilder _queryBuilder; 100 101 @Override 102 public void service(ServiceManager serviceManager) throws ServiceException 103 { 104 super.service(serviceManager); 105 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 106 _getQueryFromJSONHelper = (GetQueryFromJSONHelper) serviceManager.lookup(GetQueryFromJSONHelper.ROLE); 107 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 108 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) serviceManager.lookup(SystemPropertyExtensionPoint.ROLE); 109 _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE); 110 _contentSearcherFactory = (ContentSearcherFactory) serviceManager.lookup(ContentSearcherFactory.ROLE); 111 _queryBuilder = (QueryBuilder) serviceManager.lookup(QueryBuilder.ROLE); 112 } 113 114 @Override 115 public void configure(Configuration configuration) throws ConfigurationException 116 { 117 super.configure(configuration); 118 119 Configuration clauses = configuration.getChild("clauses"); 120 for (Configuration clause : clauses.getChildren("clause")) 121 { 122 addClauses(clause.getValue()); 123 } 124 125 _contentTypes = new HashSet<>(); 126 if (Arrays.asList(configuration.getAttributeNames()).contains("ref")) 127 { 128 if (Arrays.asList(configuration.getAttributeNames()).contains("contentTypes")) 129 { 130 throw new IllegalArgumentException(getLogsPrefix() + "a component with a query reference should not specify a content type"); 131 } 132 133 _queryReferenceId = configuration.getAttribute("ref"); 134 } 135 else 136 { 137 String contentTypesString = configuration.getAttribute("contentTypes"); 138 _contentTypes.addAll(StringUtils.stringToCollection(contentTypesString)); 139 } 140 } 141 142 @Override 143 public void prepareComponentExecution(ExtractionExecutionContext context) throws Exception 144 { 145 super.prepareComponentExecution(context); 146 147 if (_queryReferenceId != null && !_queryReferenceId.isEmpty()) 148 { 149 Query referencedQuery = _resolver.resolveById(_queryReferenceId); 150 computeReferencedQueryInfos(referencedQuery.getContent()); 151 } 152 153 _computeClausesInfos(context); 154 } 155 156 /** 157 * Manages the stored query referenced by the component 158 * @param refQueryContent referenced query content 159 * @throws QuerySyntaxException if there is a syntax error in the referenced query 160 */ 161 @SuppressWarnings("unchecked") 162 protected void computeReferencedQueryInfos(String refQueryContent) throws QuerySyntaxException 163 { 164 Map<String, Object> contentMap = _jsonUtils.convertJsonToMap(refQueryContent); 165 Map<String, Object> exportParams = (Map<String, Object>) contentMap.get("exportParams"); 166 String modelId = (String) exportParams.get("model"); 167 168 String q; 169 if (modelId.contains("solr")) 170 { 171 Map<String, Object> values = (Map<String, Object>) exportParams.get("values"); 172 String baseQuery = (String) values.get("query"); 173 174 _contentTypes = new HashSet<>((List<String>) values.get("contentTypes")); 175 176 q = SolrQuerySearchAction.buildQuery(_queryBuilder, baseQuery, _contentTypes, Collections.emptySet()); 177 } 178 else 179 { 180 SearchUIModel model = _getQueryFromJSONHelper.getSearchUIModel(exportParams); 181 List<String> contentTypesToFill = new ArrayList<>(); 182 org.ametys.cms.search.query.Query query = _getQueryFromJSONHelper.getQueryFromModel(model, exportParams, contentTypesToFill); 183 184 q = query.build(); 185 _contentTypes = new HashSet<>(contentTypesToFill); 186 } 187 188 ExtractionClause clause = new ExtractionClause(); 189 clause.setExpression(q); 190 _clauses.add(0, clause); 191 } 192 193 private void _computeClausesInfos(ExtractionExecutionContext context) 194 { 195 for (ExtractionClause clause : _clauses) 196 { 197 String clauseExpression = clause.getExpression(); 198 clause.setExpression(_resolveExpression(clauseExpression, context.getClauseVariables())); 199 200 List<ExtractionClauseGroup> groups = _extractGroupExpressionsFromClause(clauseExpression); 201 if (!groups.isEmpty()) 202 { 203 Collection<String> groupExpressions = groups.stream() 204 .map(ExtractionClauseGroup::getCompleteExpression) 205 .collect(Collectors.toList()); 206 if (_hasVariablesOutsideGroups(clauseExpression, groupExpressions)) 207 { 208 throw new IllegalArgumentException(getLogsPrefix() + "if there's at least one group, every variable should be in a group."); 209 } 210 } 211 else 212 { 213 // The only group is the entire expression 214 // The complete expression is the same as the classic one (there is no characters used to identify the group) 215 ExtractionClauseGroup group = new ExtractionClauseGroup(); 216 group.setCompleteExpression(clauseExpression); 217 group.setExpression(clauseExpression); 218 groups.add(group); 219 } 220 221 for (ExtractionClauseGroup group : groups) 222 { 223 Set<String> variables = new HashSet<>(_extractVariableFromClauseExpression(group.getExpression())); 224 if (!variables.isEmpty()) 225 { 226 if (variables.size() > 1) 227 { 228 throw new IllegalArgumentException(getLogsPrefix() + "only variables with same name are allowed within a single group"); 229 } 230 231 for (String variable : variables) 232 { 233 String[] pathSegments = variable.split(JOIN_HIERARCHY_SEPARATOR); 234 String fieldPath = pathSegments[pathSegments.length - 1]; 235 236 group.setVariable(variable); 237 group.setFieldPath(fieldPath); 238 } 239 } 240 241 clause.addGroup(group); 242 } 243 } 244 } 245 246 private String _resolveExpression(String expression, Map<String, String> queryVariables) 247 { 248 String resolvedExpression = expression; 249 for (Map.Entry<String, String> entry : queryVariables.entrySet()) 250 { 251 String variableName = entry.getKey(); 252 String contentId = entry.getValue(); 253 String escapedContentId = StringQuery.escapeStringValue(contentId, Operator.EQ); 254 resolvedExpression = resolvedExpression.replace("${" + variableName + "}", escapedContentId); 255 } 256 257 return resolvedExpression; 258 } 259 260 private boolean _hasVariablesOutsideGroups(String clauseExpression, Collection<String> groupExpressions) 261 { 262 List<String> variablesInClause = _extractVariableFromClauseExpression(clauseExpression); 263 List<String> variablesInGroups = new ArrayList<>(); 264 for (String groupExpression : groupExpressions) 265 { 266 variablesInGroups.addAll(_extractVariableFromClauseExpression(groupExpression)); 267 } 268 return variablesInClause.size() > variablesInGroups.size(); 269 } 270 271 List<ExtractionClauseGroup> _extractGroupExpressionsFromClause(String expression) 272 { 273 List<ExtractionClauseGroup> groupExpressions = new ArrayList<>(); 274 int indexOfGroup = expression.indexOf("#{"); 275 while (indexOfGroup != -1) 276 { 277 StringBuilder currentGroupSb = new StringBuilder(); 278 int endIndex = indexOfGroup; 279 int braceLevel = 0; 280 for (int i = indexOfGroup + 2; i < expression.length(); i++) 281 { 282 endIndex = i; 283 char currentChar = expression.charAt(i); 284 if ('{' == currentChar) 285 { 286 braceLevel++; 287 } 288 else if ('}' == currentChar) 289 { 290 if (0 == braceLevel) 291 { 292 ExtractionClauseGroup group = new ExtractionClauseGroup(); 293 String currentGroup = currentGroupSb.toString(); 294 group.setCompleteExpression("#{" + currentGroup + "}"); 295 group.setExpression(currentGroup); 296 groupExpressions.add(group); 297 break; 298 } 299 braceLevel--; 300 } 301 currentGroupSb.append(currentChar); 302 } 303 304 indexOfGroup = expression.indexOf("#{", endIndex); 305 } 306 return groupExpressions; 307 } 308 309 List<String> _extractVariableFromClauseExpression(String expression) 310 { 311 List<String> variables = new ArrayList<>(); 312 313 Pattern pattern = Pattern.compile(EXTRACT_JOIN_VARIABLES_REGEX); 314 Matcher matcher = pattern.matcher(expression); 315 316 while (matcher.find()) 317 { 318 variables.add(matcher.group(1)); 319 } 320 321 return variables; 322 } 323 324 @Override 325 public void executeComponent(ContentHandler contentHandler, ExtractionExecutionContext context) throws Exception 326 { 327 Iterable<Content> contents = getContents(context); 328 if (contents.iterator().hasNext()) 329 { 330 processContents(contents, contentHandler, context); 331 } 332 } 333 334 List<String> _getClauseQueries(ExtractionExecutionContext context) 335 { 336 List<String> clauseQueries = new ArrayList<>(); 337 338 for (ExtractionClause clause : _clauses) 339 { 340 String expression = clause.getExpression(); 341 342 for (ExtractionClauseGroup group : clause.getGroups()) 343 { 344 String variable = group.getVariable(); 345 346 if (variable != null && !variable.isEmpty()) 347 { 348 ExtractionExecutionContextHierarchyElement currentContextHierarchyElement = _getCurrentContextElementFromVariable(variable, context.getHierarchyElements()); 349 350 String fieldPath = group.getFieldPath(); 351 352 ExtractionComponent contextComponent = currentContextHierarchyElement.getComponent(); 353 MetadataType metadataType = _getMetadataType(fieldPath, contextComponent.getContentTypes()); 354 Collection<Object> values = _getValuesFromVariable(fieldPath, metadataType, currentContextHierarchyElement, context.getDefaultLocale()); 355 356 if (values.isEmpty()) 357 { 358 getLogger().warn(getLogsPrefix() + "no value found for field '" + fieldPath + "'. The query of this component can't be achieved"); 359 return null; 360 } 361 362 Collection<String> groupExpressions = new ArrayList<>(); 363 for (Object value : values) 364 { 365 String valueAsString = _getValueAsString(value, metadataType, fieldPath); 366 groupExpressions.add(group.getExpression().replace("${" + variable + "}", valueAsString)); 367 } 368 369 String groupReplacement = org.apache.commons.lang3.StringUtils.join(groupExpressions, " OR "); 370 expression = expression.replace(group.getCompleteExpression(), "(" + groupReplacement + ")"); 371 } 372 } 373 374 clauseQueries.add(expression); 375 } 376 377 return clauseQueries; 378 } 379 380 private ExtractionExecutionContextHierarchyElement _getCurrentContextElementFromVariable(String variable, List<ExtractionExecutionContextHierarchyElement> context) 381 { 382 int lastIndexOfSlash = variable.lastIndexOf(JOIN_HIERARCHY_SEPARATOR); 383 int indexOfCurrentContext = -1; 384 if (lastIndexOfSlash == -1) 385 { 386 indexOfCurrentContext = context.size() - 1; 387 } 388 else 389 { 390 int hierarchicalLevel = (lastIndexOfSlash + 1) / 3; 391 indexOfCurrentContext = context.size() - hierarchicalLevel; 392 if (variable.endsWith(JOIN_HIERARCHY_ELEMENT)) 393 { 394 indexOfCurrentContext--; 395 } 396 } 397 if (indexOfCurrentContext < 0 || indexOfCurrentContext >= context.size()) 398 { 399 throw new IllegalArgumentException(getLogsPrefix() + "join on '" + variable + "' does not refer to an existing parent"); 400 } 401 return context.get(indexOfCurrentContext); 402 } 403 404 /** 405 * Retrieves the field path's metadata type from content types 406 * @param fieldPath the field path 407 * @param contentTypes the content types 408 * @return the metadata type 409 */ 410 protected MetadataType _getMetadataType(String fieldPath, Collection<String> contentTypes) 411 { 412 // Manage direct content references 413 if (JOIN_HIERARCHY_ELEMENT.equals(fieldPath)) 414 { 415 return MetadataType.CONTENT; 416 } 417 418 // Manage System Properties 419 String[] pathSegments = fieldPath.split(EXTRACTION_METADATA_PATH_SEPARATOR); 420 String propertyName = pathSegments[pathSegments.length - 1]; 421 if (_systemPropertyExtensionPoint.hasExtension(propertyName)) 422 { 423 SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(propertyName); 424 return systemProperty.getType(); 425 } 426 427 // Get content types common ancestor 428 ContentType contentTypesAncestor = null; 429 String contentTypesAncestorId = _contentTypesHelper.getCommonAncestor(contentTypes); 430 if (contentTypesAncestorId != null && _contentTypeExtensionPoint.hasExtension(contentTypesAncestorId)) 431 { 432 contentTypesAncestor = _contentTypeExtensionPoint.getExtension(contentTypesAncestorId); 433 } 434 435 // Manage metadata 436 if (contentTypesAncestor != null) 437 { 438 String fieldPathWthClassicSeparator = fieldPath.replaceAll(EXTRACTION_METADATA_PATH_SEPARATOR, ContentConstants.METADATA_PATH_SEPARATOR); 439 MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(fieldPathWthClassicSeparator, contentTypesAncestor); 440 if (definition != null) 441 { 442 return definition.getType(); 443 } 444 throw new IllegalArgumentException(getLogsPrefix() + "join on '" + fieldPath + "'. This metadata is not available for '" + contentTypesAncestor.getId() + "' content type"); 445 } 446 447 throw new IllegalArgumentException(getLogsPrefix() + "join on '" + fieldPath + "'. This metadata is not available"); 448 } 449 450 private Collection<Object> _getValuesFromVariable(String fieldPath, MetadataType metadataType, ExtractionExecutionContextHierarchyElement contextHierarchyElement, Locale defaultLocale) 451 { 452 Collection<Object> values = new LinkedHashSet<>(); 453 454 Iterable<Content> contents = contextHierarchyElement.getContents(); 455 for (Content content: contents) 456 { 457 boolean isAutoposting = contextHierarchyElement.isAutoposting(); 458 Collection<Object> contentValues = _getContentValuesFromVariable(content, fieldPath, metadataType, isAutoposting, defaultLocale); 459 values.addAll(contentValues); 460 } 461 462 return values; 463 } 464 465 private Collection<Object> _getContentValuesFromVariable(Content content, String fieldPath, MetadataType metadataType, boolean isAutoposting, Locale defaultLocale) 466 { 467 Collection<Object> values = new LinkedHashSet<>(); 468 469 Object value = _getContentValue(content, fieldPath, defaultLocale); 470 if (value == null) 471 { 472 return Collections.emptyList(); 473 } 474 475 if (value instanceof Collection<?>) 476 { 477 values.addAll((Collection<?>) value); 478 } 479 else 480 { 481 values.add(value); 482 } 483 484 Collection<Object> result = new LinkedHashSet<>(values); 485 486 if (isAutoposting) 487 { 488 switch (metadataType) 489 { 490 case CONTENT: 491 for (Object object : values) 492 { 493 Content parent = (Content) object; 494 ContentType contentType = _contentTypesHelper.getFirstContentType(parent); 495 496 // Manage autoposting only if the current value is a thesaurus term 497 if (Arrays.asList(_contentTypesHelper.getSupertypeIds(contentType.getId()).getLeft()).contains(ThesaurusDAO.MICROTHESAURUS_ABSTRACT_CONTENT_TYPE)) 498 { 499 AmetysObjectIterable<Content> chidren = _thesaurusDAO.getChildTerms(contentType.getId(), parent.getId()); 500 for (Content child : chidren) 501 { 502 Collection<Object> childValues = _getContentValuesFromVariable(child, JOIN_HIERARCHY_ELEMENT, metadataType, isAutoposting, defaultLocale); 503 result.addAll(childValues); 504 } 505 } 506 } 507 508 break; 509 default: 510 break; 511 } 512 } 513 514 return result; 515 } 516 517 private Object _getContentValue(Content content, String fieldPath, Locale defaultLocale) 518 { 519 if (JOIN_HIERARCHY_ELEMENT.equals(fieldPath)) 520 { 521 return content; 522 } 523 else 524 { 525 String fieldPathWthClassicSeparator = fieldPath.replaceAll(EXTRACTION_METADATA_PATH_SEPARATOR, ContentConstants.METADATA_PATH_SEPARATOR); 526 return _contentHelper.getValue(content, fieldPathWthClassicSeparator, defaultLocale, true); 527 } 528 } 529 530 private String _getValueAsString(Object value, MetadataType metadataType, String fieldPath) 531 { 532 String valueAsString; 533 switch (metadataType) 534 { 535 case STRING: 536 case LONG: 537 case DOUBLE: 538 case BOOLEAN: 539 valueAsString = value.toString(); 540 break; 541 case CONTENT: 542 valueAsString = ((Content) value).getId(); 543 break; 544 default: 545 throw new IllegalArgumentException(getLogsPrefix() + "join on '" + fieldPath + "'. Metadata type '" + metadataType + "' is not supported by extraction module"); 546 } 547 548 return StringQuery.escapeStringValue(valueAsString, Operator.EQ); 549 } 550 551 /** 552 * Retrieves the content searcher to use for solr search 553 * @return the content searcher 554 */ 555 protected SimpleContentSearcher getContentSearcher() 556 { 557 return _contentSearcherFactory.create(_contentTypes); 558 } 559 560 /** 561 * Gets the content results from Solr 562 * @param context component execution context 563 * @return the content results from Solr 564 * @throws Exception if an error occurs 565 */ 566 protected Iterable<Content> getContents(ExtractionExecutionContext context) throws Exception 567 { 568 List<String> clauseQueries = _getClauseQueries(context); 569 if (clauseQueries == null) 570 { 571 return new EmptyIterable<>(); 572 } 573 else 574 { 575 List<String> filterQueryStrings = clauseQueries 576 .stream() 577 .map(LambdaUtils.wrap(clauseQuery -> SolrQuerySearchAction.buildQuery(_queryBuilder, clauseQuery, Collections.emptySet(), Collections.emptySet()))) 578 .collect(Collectors.toList()); 579 return getContentSearcher() 580 .withFilterQueryStrings(filterQueryStrings) 581 .setCheckRights(false) 582 .search("*:*"); 583 } 584 } 585 586 /** 587 * Process result contents to format the result document 588 * @param contents search results 589 * @param contentHandler result document 590 * @param context component execution context 591 * @throws Exception if an error occurs 592 */ 593 protected abstract void processContents(Iterable<Content> contents, ContentHandler contentHandler, ExtractionExecutionContext context) throws Exception; 594 595 @Override 596 public Map<String, Object> getComponentDetailsForTree() 597 { 598 Map<String, Object> details = super.getComponentDetailsForTree(); 599 600 @SuppressWarnings("unchecked") 601 Map<String, Object> data = (Map<String, Object>) details.get("data"); 602 603 List<String> clauses = new ArrayList<>(); 604 for (ExtractionClause clause : this.getClauses()) 605 { 606 clauses.add(clause.getExpression()); 607 } 608 data.put("clauses", clauses); 609 610 data.put("useQueryRef", org.apache.commons.lang.StringUtils.isNotEmpty(_queryReferenceId)); 611 data.put("contentTypes", this.getContentTypes()); 612 data.put("queryReferenceId", this.getQueryReferenceId()); 613 614 return details; 615 } 616 617 public Set<String> getContentTypes() 618 { 619 return _contentTypes; 620 } 621 622 /** 623 * Add content types to component 624 * @param contentTypes Array of content types to add 625 */ 626 public void addContentTypes(String... contentTypes) 627 { 628 _contentTypes.addAll(Arrays.asList(contentTypes)); 629 } 630 631 /** 632 * Retrieves the id of the referenced query 633 * @return the id of the referenced query 634 */ 635 public String getQueryReferenceId() 636 { 637 return _queryReferenceId; 638 } 639 640 /** 641 * Sets the id of the referenced query 642 * @param queryReferenceId The id of the referenced query to set 643 */ 644 public void setQueryReferenceId(String queryReferenceId) 645 { 646 _queryReferenceId = queryReferenceId; 647 } 648 649 /** 650 * Retrieves the component clauses 651 * @return the component clauses 652 */ 653 public List<ExtractionClause> getClauses() 654 { 655 return _clauses; 656 } 657 658 /** 659 * Add clauses to the component. Do not manage clauses' groups 660 * @param expressions Array clauses expressions to add 661 */ 662 public void addClauses(String... expressions) 663 { 664 for (String expression : expressions) 665 { 666 ExtractionClause clause = new ExtractionClause(); 667 clause.setExpression(expression); 668 _clauses.add(clause); 669 } 670 } 671}