001/* 002 * Copyright 2016 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.content; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033 034import org.ametys.cms.content.indexing.solr.SolrFieldNames; 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.cms.contenttype.ContentTypesHelper; 037import org.ametys.cms.repository.Content; 038import org.ametys.cms.search.QueryBuilder; 039import org.ametys.cms.search.SearchField; 040import org.ametys.cms.search.SearchResults; 041import org.ametys.cms.search.Sort; 042import org.ametys.cms.search.Sort.Order; 043import org.ametys.cms.search.model.ResultField; 044import org.ametys.cms.search.model.SearchCriterion; 045import org.ametys.cms.search.model.SearchModel; 046import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 047import org.ametys.cms.search.query.ContentTypeQuery; 048import org.ametys.cms.search.query.DocumentTypeQuery; 049import org.ametys.cms.search.query.Query; 050import org.ametys.cms.search.solr.SearcherFactory; 051import org.ametys.cms.search.solr.SearcherFactory.Searcher; 052import org.ametys.cms.search.ui.model.SearchUIModel; 053import org.ametys.plugins.repository.AmetysObject; 054import org.ametys.plugins.repository.AmetysObjectIterable; 055import org.ametys.runtime.plugin.component.AbstractLogEnabled; 056 057/** 058 * Component creating content searchers from {@link SearchModel}s or content type IDs. 059 */ 060public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable 061{ 062 063 /** The component role. */ 064 public static final String ROLE = ContentSearcherFactory.class.getName(); 065 066 /** The searcher factory. */ 067 protected SearcherFactory _searcherFactory; 068 069 /** The query builder. */ 070 protected QueryBuilder _queryBuilder; 071 072 /** The content type extension point. */ 073 protected ContentTypeExtensionPoint _cTypeEP; 074 075 /** The content type helper. */ 076 protected ContentTypesHelper _cTypeHelper; 077 078 /** The system property extension point. */ 079 protected SystemPropertyExtensionPoint _sysPropEP; 080 081 /** The search helper. */ 082 protected ContentSearchHelper _searchHelper; 083 084 @Override 085 public void service(ServiceManager manager) throws ServiceException 086 { 087 _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE); 088 _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE); 089 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 090 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 091 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 092 093 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 094 } 095 096 /** 097 * Create a ContentSearcher from a search model. 098 * @param searchModel The reference search model. 099 * @return a ContentSearcher backed by the given search model. 100 */ 101 public SearchModelContentSearcher create(SearchModel searchModel) 102 { 103 return new SearchModelContentSearcher(searchModel); 104 } 105 106 /** 107 * Create a simple ContentSearcher from a list of content types. 108 * @param contentTypes The content types to search on. 109 * @return a ContentSearcher referencing the given content types. 110 */ 111 public SimpleContentSearcher create(String... contentTypes) 112 { 113 return new SimpleContentSearcher(Arrays.asList(contentTypes)); 114 } 115 116 /** 117 * Create a simple ContentSearcher from a list of content types. 118 * @param contentTypes The content types to search on. 119 * @return a ContentSearcher referencing the given content types. 120 */ 121 public SimpleContentSearcher create(Collection<String> contentTypes) 122 { 123 return new SimpleContentSearcher(contentTypes); 124 } 125 126 /** 127 * A ContentSearcher backed by a {@link SearchModel}. 128 */ 129 public class SearchModelContentSearcher 130 { 131 private SearchUIModel _searchModel; 132 private List<Sort> _sort; 133 private String _searchMode; 134 private int _start; 135 private int _maxResults; 136 private boolean _checkRights; 137 138 /** 139 * Build a ContentSearcher referencing a {@link SearchModel}. 140 * @param searchModel the {@link SearchModel}. 141 */ 142 public SearchModelContentSearcher(SearchModel searchModel) 143 { 144 // TODO Do not cast. 145 this._searchModel = (SearchUIModel) searchModel; 146 this._sort = new ArrayList<>(); 147 this._searchMode = "simple"; 148 this._start = 0; 149 this._maxResults = Integer.MAX_VALUE; 150 this._checkRights = true; 151 } 152 153 /** 154 * Add a sort criterion. 155 * @param fieldRef The field reference (name of a SearchField). 156 * @param order The sort order. 157 * @return The ContentSearcher itself. 158 */ 159 public SearchModelContentSearcher addSort(String fieldRef, Order order) 160 { 161 _sort.add(new Sort(fieldRef, order)); 162 return this; 163 } 164 165 /** 166 * Set the sort criteria. 167 * @param sortCriteria The sort criteria as a List. 168 * @return The ContentSearcher itself. 169 */ 170 public SearchModelContentSearcher withSort(List<Sort> sortCriteria) 171 { 172 _sort = new ArrayList<>(sortCriteria); 173 return this; 174 } 175 176 /** 177 * Set the search mode. 178 * @param searchMode The search mode. 179 * @return The ContentSearcher itself. 180 */ 181 public SearchModelContentSearcher withSearchMode(String searchMode) 182 { 183 _searchMode = searchMode; 184 return this; 185 } 186 187 /** 188 * Set the limits to use. 189 * @param start The start index. 190 * @param maxResults The maximum number of results. 191 * @return The ContentSearcher itself. 192 */ 193 public SearchModelContentSearcher withLimits(int start, int maxResults) 194 { 195 this._start = start; 196 this._maxResults = maxResults; 197 return this; 198 } 199 200 /** 201 * Whether to check rights when searching, false otherwise. 202 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 203 * @return The ContentSearcher itself. 204 */ 205 public SearchModelContentSearcher setCheckRights(boolean checkRights) 206 { 207 _checkRights = checkRights; 208 return this; 209 } 210 211 /** 212 * Search the contents. 213 * @param values The values for search criteria defined in the model. 214 * @param <C> The type Content 215 * @return The search results as {@link AmetysObject}s. 216 * @throws Exception if an error occurs. 217 */ 218 public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception 219 { 220 return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search(); 221 } 222 223 /** 224 * Search the contents. 225 * @param values The values for search criteria defined in the model. 226 * @param <C> The type Content * 227 * @return The search results. 228 * @throws Exception if an error occurs. 229 */ 230 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception 231 { 232 return searchWithFacets(values, Collections.emptyMap()); 233 } 234 235 /** 236 * Search the contents. 237 * @param <C> The type Content 238 * @param values The values for search criteria defined in the model. 239 * @param contextualParameters The search contextual parameters. 240 * @return The search results. 241 * @throws Exception if an error occurs. 242 */ 243 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception 244 { 245 return searchWithFacets(values, Collections.emptyMap(), contextualParameters); 246 } 247 248 /** 249 * Search the contents. 250 * @param <C> The type Content 251 * @param values The values for search criteria defined in the model. 252 * @param facetValues The facet values, indexed 253 * @param contextualParameters The search contextual parameters. 254 * @return The search results. 255 * @throws Exception if an error occurs. 256 */ 257 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception 258 { 259 return _searcher(values, facetValues, contextualParameters).searchWithFacets(); 260 } 261 262 private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) 263 { 264 Query query = _queryBuilder.build(_searchModel, _searchMode, true, values, contextualParameters); 265 266 List<Sort> sort = getSort(contextualParameters); 267 List<SearchField> facets = getFacets(contextualParameters); 268 269 return _searcherFactory.create() 270 .withQuery(query) 271 .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 272 .withSort(sort) 273 .withFacets(facets) 274 .withFacetValues(facetValues) 275 .withLimits(_start, _maxResults) 276 .setCheckRights(_checkRights); 277 } 278 279 /** 280 * Get the sort criteria. 281 * @param contextualParameters The search contextual parameters. 282 * @return The sort criteria. 283 */ 284 @SuppressWarnings("synthetic-access") 285 protected List<Sort> getSort(Map<String, Object> contextualParameters) 286 { 287 List<Sort> sort = new ArrayList<>(); 288 289 if (!_sort.isEmpty()) 290 { 291 // Index criterion and results by search field name. 292 Map<String, SearchCriterion> criteriaByName = new HashMap<>(); 293 for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values()) 294 { 295 if (criterion.getSearchField() != null) 296 { 297 criteriaByName.put(criterion.getSearchField().getName(), criterion); 298 } 299 } 300 Map<String, ResultField> resultsByName = new HashMap<>(); 301 for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values()) 302 { 303 if (resultField.getSearchField() != null) 304 { 305 resultsByName.put(resultField.getSearchField().getName(), resultField); 306 } 307 } 308 309 for (Sort sortCriterion : _sort) 310 { 311 String id = sortCriterion.getField(); 312 313 SearchField searchField = null; 314 if (criteriaByName.containsKey(id)) 315 { 316 searchField = criteriaByName.get(id).getSearchField(); 317 } 318 else if (resultsByName.containsKey(id)) 319 { 320 searchField = resultsByName.get(id).getSearchField(); 321 } 322 323 if (searchField == null) 324 { 325 throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model."); 326 } 327 else if (searchField.getSortField() == null) 328 { 329 getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id); 330 } 331 else 332 { 333 sort.add(new Sort(searchField, sortCriterion.getOrder())); 334 } 335 } 336 } 337 else 338 { 339 // Get the default sort from the search model. 340 } 341 342 return sort; 343 } 344 345 /** 346 * Get the facet fields. 347 * @param contextualParameters The search contextual parameters. 348 * @return The facet fields as a List. 349 */ 350 protected List<SearchField> getFacets(Map<String, Object> contextualParameters) 351 { 352 List<SearchField> facets = new ArrayList<>(); 353 354 for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values()) 355 { 356 if (criterion.getSearchField() != null) 357 { 358 facets.add(criterion.getSearchField()); 359 } 360 } 361 362 return facets; 363 } 364 365 } 366 367 /** 368 * A ContentSearcher on a list of content types. 369 */ 370 public class SimpleContentSearcher 371 { 372 373 private Set<String> _contentTypes; 374 private List<Sort> _sort; 375 private List<String> _facets; 376 private int _start; 377 private int _maxResults; 378 private boolean _checkRights; 379 private List<String> _filterQueryStrings; 380 private List<Query> _filterQueries; 381 382 /** 383 * Build a content searcher on a list of content types. 384 * @param contentTypes A collection of content types to search on. 385 */ 386 public SimpleContentSearcher(Collection<String> contentTypes) 387 { 388 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 389 this._sort = new ArrayList<>(); 390 this._facets = new ArrayList<>(); 391 this._start = 0; 392 this._maxResults = Integer.MAX_VALUE; 393 this._checkRights = true; 394 } 395 396 /** 397 * Set the filter queries. 398 * @param filterQueries the filter queries. 399 * @return The ContentSearcher itself. 400 */ 401 public SimpleContentSearcher withFilterQueries(List<Query> filterQueries) 402 { 403 _filterQueries = filterQueries; 404 return this; 405 } 406 407 /** 408 * Set the filter queries. 409 * @param filterQueryStrings the filter queries. 410 * @return The ContentSearcher itself. 411 */ 412 public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings) 413 { 414 _filterQueryStrings = filterQueryStrings; 415 return this; 416 } 417 418 /** 419 * Set the sort criteria. 420 * @param sortCriteria The sort criteria as a List. 421 * @return The ContentSearcher itself. 422 */ 423 public SimpleContentSearcher withSort(List<Sort> sortCriteria) 424 { 425 _sort = new ArrayList<>(sortCriteria); 426 return this; 427 } 428 429 /** 430 * Add a sort criterion. 431 * @param fieldRef The field reference (name of a SearchField). 432 * @param order The sort order. 433 * @return The ContentSearcher itself. 434 */ 435 public SimpleContentSearcher addSort(String fieldRef, Order order) 436 { 437 _sort.add(new Sort(fieldRef, order)); 438 return this; 439 } 440 441 /** 442 * Set the facets. 443 * @param facets The facets list. 444 * @return The ContentSearcher itself. 445 */ 446 public SimpleContentSearcher withFacets(Collection<String> facets) 447 { 448 _facets = new ArrayList<>(facets); 449 return this; 450 } 451 452 /** 453 * Set the facets. 454 * @param facets The facets list. 455 * @return The ContentSearcher itself. 456 */ 457 public SimpleContentSearcher withFacets(String... facets) 458 { 459 _facets = Arrays.asList(facets); 460 return this; 461 } 462 463 /** 464 * Set the limits to use. 465 * @param start The start index. 466 * @param maxResults The maximum number of results. 467 * @return The ContentSearcher itself. 468 */ 469 public SimpleContentSearcher withLimits(int start, int maxResults) 470 { 471 this._start = start; 472 this._maxResults = maxResults; 473 return this; 474 } 475 476 /** 477 * Whether to check rights when searching, false otherwise. 478 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 479 * @return The ContentSearcher itself. 480 */ 481 public SimpleContentSearcher setCheckRights(boolean checkRights) 482 { 483 _checkRights = checkRights; 484 return this; 485 } 486 487 /** 488 * Search the contents. 489 * @param <C> The type Content 490 * @param query The query object to execute. 491 * @return The search results as {@link AmetysObject}s. 492 * @throws Exception if an error occurs. 493 */ 494 public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception 495 { 496 return _searcher(query, Collections.emptyMap()).search(); 497 } 498 499 /** 500 * Search the contents. 501 * @param <C> The type Content 502 * @param query The query string to execute. 503 * @return The search results as {@link AmetysObject}s. 504 * @throws Exception if an error occurs. 505 */ 506 public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception 507 { 508 return _searcher(query, Collections.emptyMap()).search(); 509 } 510 511 /** 512 * Search the contents. 513 * @param <C> The type Content 514 * @param query The query objet to execute. 515 * @return The search results. 516 * @throws Exception if an error occurs. 517 */ 518 public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception 519 { 520 return searchWithFacets(query, Collections.emptyMap()); 521 } 522 523 /** 524 * Search the contents. 525 * @param <C> The type Content 526 * @param query The query string to execute. 527 * @return The search results. 528 * @throws Exception if an error occurs. 529 */ 530 public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception 531 { 532 return searchWithFacets(query, Collections.emptyMap()); 533 } 534 535 /** 536 * Search the contents. 537 * @param <C> The type Content 538 * @param query The query object to execute. 539 * @param facetValues The facet values. 540 * @return The search results. 541 * @throws Exception if an error occurs. 542 */ 543 public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception 544 { 545 return _searcher(query, facetValues).searchWithFacets(); 546 } 547 548 /** 549 * Search the contents. 550 * @param <C> The type Content 551 * @param query The query string to execute. 552 * @param facetValues The facet values. 553 * @return The search results. 554 * @throws Exception if an error occurs. 555 */ 556 public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception 557 { 558 return _searcher(query, facetValues).searchWithFacets(); 559 } 560 561 private Searcher _searcher(String query, Map<String, List<String>> facetValues) 562 { 563 return _searcher(facetValues).withQueryString(query); 564 } 565 566 private Searcher _searcher(Query query, Map<String, List<String>> facetValues) 567 { 568 return _searcher(facetValues).withQuery(query); 569 } 570 571 private Searcher _searcher(Map<String, List<String>> facetValues) 572 { 573 List<Sort> sort = getSort(); 574 List<SearchField> facets = getFacets(); 575 576 List<Query> filterQueries = new ArrayList<>(); 577 filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 578 579 if (!_contentTypes.isEmpty()) 580 { 581 filterQueries.add(new ContentTypeQuery(_contentTypes)); 582 } 583 584 if (_filterQueries != null) 585 { 586 filterQueries.addAll(_filterQueries); 587 } 588 589 List<String> filterQueryStrings = new ArrayList<>(); 590 591 if (_filterQueryStrings != null) 592 { 593 filterQueryStrings.addAll(_filterQueryStrings); 594 } 595 596 return _searcherFactory.create() 597 .withFilterQueries(filterQueries) 598 .withFilterQueryStrings(filterQueryStrings) 599 .withSort(sort) 600 .withFacets(facets) 601 .withFacetValues(facetValues) 602 .withLimits(_start, _maxResults) 603 .setCheckRights(_checkRights); 604 } 605 606 /** 607 * Get the sort criteria from the specified field names. 608 * @return The sort criteria. 609 */ 610 protected List<Sort> getSort() 611 { 612 List<Sort> sortCriteria = new ArrayList<>(); 613 614 for (Sort sort : _sort) 615 { 616 String fieldName = sort.getField(); 617 Order order = sort.getOrder(); 618 619 Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 620// SearchField searchField = getSearchField(_contentTypes, fieldName); 621 if (searchField.isPresent()) 622 { 623 sortCriteria.add(new Sort(searchField.get(), order)); 624 } 625 else 626 { 627 throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName)); 628 } 629 } 630 631 return sortCriteria; 632 } 633 634 /** 635 * Get the facet criteria as a list of SearchField from the specified field names. 636 * @return The facets as a List of SearchField. 637 */ 638 protected List<SearchField> getFacets() 639 { 640 List<SearchField> facets = new ArrayList<>(); 641 642 for (String fieldName : _facets) 643 { 644 Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 645// SearchField searchField = getSearchField(_contentTypes, fieldName); 646 if (searchField.isPresent()) 647 { 648 facets.add(searchField.get()); 649 } 650 else 651 { 652 throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName)); 653 } 654 } 655 656 return facets; 657 } 658 659 private String _exceptionMessageForEmptySearchField(String fieldName) 660 { 661 return "The field '" + fieldName + "' can't be found in the selected content types."; 662 } 663 664// /** 665// * Get a {@link SearchField} from a field name. 666// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 667// * @return The {@link SearchField} corresponding to the 668// */ 669// public SearchField getSearchField(Collection<String> contentTypes, String fieldName) 670// { 671// SearchField searchField = null; 672// 673// if (_sysPropEP.hasExtension(fieldName)) 674// { 675// SystemProperty property = _sysPropEP.getExtension(fieldName); 676// searchField = property.getSearchField(); 677// } 678// else 679// { 680// String metadataPath = fieldName.replace('.', '/'); 681// _searchHelper.getMetadataSearchField(contentTypes, metadataPath); 682// } 683// 684// return searchField; 685// } 686 687// /** 688// * Get a {@link SearchField} from a field name. 689// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 690// * @return The {@link SearchField} corresponding to the 691// */ 692// protected SearchField getSearchField(String fieldName) 693// { 694// SearchField searchField = null; 695// 696// if (_sysPropEP.hasExtension(fieldName)) 697// { 698// SystemProperty property = _sysPropEP.getExtension(fieldName); 699// searchField = property.getSearchField(); 700// } 701// else 702// { 703// String metaPath = fieldName.replace('.', '/'); 704// if (_contentTypeId != null) 705// { 706// ContentType cType = _cTypeEP.getExtension(_contentTypeId); 707// List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionsByPath(cType, metaPath); 708// 709// boolean joinedMetadata = isJoinedMetadata(metaDefs); 710// 711// if (!joinedMetadata) 712// { 713// MetadataType type = metaDefs.get(metaDefs.size() - 1).getType(); 714// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, type); 715// } 716// else 717// { 718// throw new IllegalArgumentException("The metadata '" + fieldName + "' can't be used as it is joined."); 719// } 720// } 721// else if (fieldName.equals("title")) 722// { 723// // No specific content type: allow only title. 724// // TODO Provide a standard "title" metadata definition to avoid getting it on a random content type. 725// ContentType cType = _cTypeEP.getExtension(_contentTypes.iterator().next()); 726// MetadataDefinition metaDef = cType.getMetadataDefinition("title"); 727// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, metaDef.getType()); 728// } 729// } 730// 731// if (searchField == null) 732// { 733// throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the selected content types."); 734// } 735// 736// return searchField; 737// } 738// 739// /** 740// * Test if a metadata, represented by a list of successive definitions, is joined. 741// * @param metaDefs The list of successive definitions. 742// * @return <code>true</code> if the metadata is joined, <code>false</code> otherwise. 743// */ 744// protected boolean isJoinedMetadata(List<MetadataDefinition> metaDefs) 745// { 746// boolean joinedMetadata = false; 747// Iterator<MetadataDefinition> metaDefIt = metaDefs.iterator(); 748// while (metaDefIt.hasNext()) 749// { 750// MetadataType type = metaDefIt.next().getType(); 751// // The column represents a "joined" value if it has a content metadata (except if it's the last one). 752// if ((type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) && metaDefIt.hasNext()) 753// { 754// joinedMetadata = true; 755// } 756// } 757// return joinedMetadata; 758// } 759 760 } 761 762}