package org.apache.archiva.indexer.search; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.archiva.admin.model.RepositoryAdminException; import org.apache.archiva.admin.model.beans.ManagedRepository; import org.apache.archiva.admin.model.beans.ProxyConnector; import org.apache.archiva.admin.model.managed.ManagedRepositoryAdmin; import org.apache.archiva.admin.model.proxyconnector.ProxyConnectorAdmin; import org.apache.archiva.common.plexusbridge.MavenIndexerUtils; import org.apache.archiva.common.plexusbridge.PlexusSisuBridge; import org.apache.archiva.common.plexusbridge.PlexusSisuBridgeException; import org.apache.archiva.indexer.util.SearchUtil; import org.apache.commons.lang.StringUtils; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.apache.maven.index.ArtifactInfo; import org.apache.maven.index.FlatSearchRequest; import org.apache.maven.index.FlatSearchResponse; import org.apache.maven.index.MAVEN; import org.apache.maven.index.NexusIndexer; import org.apache.maven.index.OSGI; import org.apache.maven.index.QueryCreator; import org.apache.maven.index.SearchType; import org.apache.maven.index.context.IndexCreator; import org.apache.maven.index.context.IndexingContext; import org.apache.maven.index.expr.SearchExpression; import org.apache.maven.index.expr.SearchTyped; import org.apache.maven.index.expr.SourcedSearchExpression; import org.apache.maven.index.expr.UserInputSearchExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * RepositorySearch implementation which uses the Maven Indexer for searching. */ @Service( "repositorySearch#maven" ) public class MavenRepositorySearch implements RepositorySearch { private Logger log = LoggerFactory.getLogger( getClass() ); private NexusIndexer indexer; private QueryCreator queryCreator; private ManagedRepositoryAdmin managedRepositoryAdmin; private ProxyConnectorAdmin proxyConnectorAdmin; private MavenIndexerUtils mavenIndexerUtils; protected MavenRepositorySearch() { // for test purpose } @Inject public MavenRepositorySearch( PlexusSisuBridge plexusSisuBridge, ManagedRepositoryAdmin managedRepositoryAdmin, MavenIndexerUtils mavenIndexerUtils, ProxyConnectorAdmin proxyConnectorAdmin ) throws PlexusSisuBridgeException { this.indexer = plexusSisuBridge.lookup( NexusIndexer.class ); this.queryCreator = plexusSisuBridge.lookup( QueryCreator.class ); this.managedRepositoryAdmin = managedRepositoryAdmin; this.mavenIndexerUtils = mavenIndexerUtils; this.proxyConnectorAdmin = proxyConnectorAdmin; } /** * @see RepositorySearch#search(String, List, String, SearchResultLimits, List) */ @Override public SearchResults search( String principal, List<String> selectedRepos, String term, SearchResultLimits limits, List<String> previousSearchTerms ) throws RepositorySearchException { List<String> indexingContextIds = addIndexingContexts( selectedRepos ); // since upgrade to nexus 2.0.0, query has changed from g:[QUERIED TERM]* to g:*[QUERIED TERM]* // resulting to more wildcard searches so we need to increase max clause count BooleanQuery.setMaxClauseCount( Integer.MAX_VALUE ); BooleanQuery q = new BooleanQuery(); if ( previousSearchTerms == null || previousSearchTerms.isEmpty() ) { constructQuery( term, q ); } else { for ( String previousTerm : previousSearchTerms ) { BooleanQuery iQuery = new BooleanQuery(); constructQuery( previousTerm, iQuery ); q.add( iQuery, Occur.MUST ); } BooleanQuery iQuery = new BooleanQuery(); constructQuery( term, iQuery ); q.add( iQuery, Occur.MUST ); } // we retun only artifacts without classifier in quick search, olamy cannot find a way to say with this field empty // FIXME cannot find a way currently to setup this in constructQuery !!! return search( limits, q, indexingContextIds, NoClassifierArtifactInfoFilter.LIST, selectedRepos, true ); } /** * @see RepositorySearch#search(String, SearchFields, SearchResultLimits) */ @Override public SearchResults search( String principal, SearchFields searchFields, SearchResultLimits limits ) throws RepositorySearchException { if ( searchFields.getRepositories() == null ) { throw new RepositorySearchException( "Repositories cannot be null." ); } List<String> indexingContextIds = addIndexingContexts( searchFields.getRepositories() ); // if no index found in the specified ones return an empty search result instead of doing a search on all index // olamy: IMHO doesn't make sense if ( !searchFields.getRepositories().isEmpty() && ( indexingContextIds == null || indexingContextIds.isEmpty() ) ) { return new SearchResults(); } BooleanQuery q = new BooleanQuery(); if ( StringUtils.isNotBlank( searchFields.getGroupId() ) ) { q.add( indexer.constructQuery( MAVEN.GROUP_ID, searchFields.isExactSearch() ? new SourcedSearchExpression( searchFields.getGroupId() ) : new UserInputSearchExpression( searchFields.getGroupId() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getArtifactId() ) ) { q.add( indexer.constructQuery( MAVEN.ARTIFACT_ID, searchFields.isExactSearch() ? new SourcedSearchExpression( searchFields.getArtifactId() ) : new UserInputSearchExpression( searchFields.getArtifactId() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getVersion() ) ) { q.add( indexer.constructQuery( MAVEN.VERSION, searchFields.isExactSearch() ? new SourcedSearchExpression( searchFields.getVersion() ) : new SourcedSearchExpression( searchFields.getVersion() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getPackaging() ) ) { q.add( indexer.constructQuery( MAVEN.PACKAGING, searchFields.isExactSearch() ? new SourcedSearchExpression( searchFields.getPackaging() ) : new UserInputSearchExpression( searchFields.getPackaging() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getClassName() ) ) { q.add( indexer.constructQuery( MAVEN.CLASSNAMES, new UserInputSearchExpression( searchFields.getClassName() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleSymbolicName() ) ) { q.add( indexer.constructQuery( OSGI.SYMBOLIC_NAME, new UserInputSearchExpression( searchFields.getBundleSymbolicName() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleVersion() ) ) { q.add( indexer.constructQuery( OSGI.VERSION, new UserInputSearchExpression( searchFields.getBundleVersion() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleExportPackage() ) ) { q.add( indexer.constructQuery( OSGI.EXPORT_PACKAGE, new UserInputSearchExpression( searchFields.getBundleExportPackage() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleExportService() ) ) { q.add( indexer.constructQuery( OSGI.EXPORT_SERVICE, new UserInputSearchExpression( searchFields.getBundleExportService() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleImportPackage() ) ) { q.add( indexer.constructQuery( OSGI.IMPORT_PACKAGE, new UserInputSearchExpression( searchFields.getBundleImportPackage() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleName() ) ) { q.add( indexer.constructQuery( OSGI.NAME, new UserInputSearchExpression( searchFields.getBundleName() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleImportPackage() ) ) { q.add( indexer.constructQuery( OSGI.IMPORT_PACKAGE, new UserInputSearchExpression( searchFields.getBundleImportPackage() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getBundleRequireBundle() ) ) { q.add( indexer.constructQuery( OSGI.REQUIRE_BUNDLE, new UserInputSearchExpression( searchFields.getBundleRequireBundle() ) ), Occur.MUST ); } if ( StringUtils.isNotBlank( searchFields.getClassifier() ) ) { q.add( indexer.constructQuery( MAVEN.CLASSIFIER, searchFields.isExactSearch() ? new SourcedSearchExpression( searchFields.getClassifier() ) : new UserInputSearchExpression( searchFields.getClassifier() ) ), Occur.MUST ); } else if ( searchFields.isExactSearch() ) { //TODO improvement in case of exact search and no classifier we must query for classifier with null value // currently it's done in DefaultSearchService with some filtering } if ( q.getClauses() == null || q.getClauses().length <= 0 ) { throw new RepositorySearchException( "No search fields set." ); } return search( limits, q, indexingContextIds, Collections.<ArtifactInfoFilter>emptyList(), searchFields.getRepositories(), searchFields.isIncludePomArtifacts() ); } private static class NullSearch implements SearchTyped, SearchExpression { private static final NullSearch INSTANCE = new NullSearch(); @Override public String getStringValue() { return "[[NULL_VALUE]]"; } @Override public SearchType getSearchType() { return SearchType.EXACT; } } private SearchResults search( SearchResultLimits limits, BooleanQuery q, List<String> indexingContextIds, List<? extends ArtifactInfoFilter> filters, List<String> selectedRepos, boolean includePoms ) throws RepositorySearchException { try { FlatSearchRequest request = new FlatSearchRequest( q ); request.setContexts( getIndexingContexts( indexingContextIds ) ); if ( limits != null ) { // we apply limits only when first page asked if ( limits.getSelectedPage() == 0 ) { request.setCount( limits.getPageSize() * ( Math.max( 1, limits.getSelectedPage() ) ) ); } } FlatSearchResponse response = indexer.searchFlat( request ); if ( response == null || response.getTotalHits() == 0 ) { SearchResults results = new SearchResults(); results.setLimits( limits ); return results; } return convertToSearchResults( response, limits, filters, selectedRepos, includePoms ); } catch ( IOException e ) { throw new RepositorySearchException( e.getMessage(), e ); } catch ( RepositoryAdminException e ) { throw new RepositorySearchException( e.getMessage(), e ); } } private List<IndexingContext> getIndexingContexts( List<String> ids ) { List<IndexingContext> contexts = new ArrayList<>( ids.size() ); for ( String id : ids ) { IndexingContext context = indexer.getIndexingContexts().get( id ); if ( context != null ) { contexts.add( context ); } else { log.warn( "context with id {} not exists", id ); } } return contexts; } private void constructQuery( String term, BooleanQuery q ) { q.add( indexer.constructQuery( MAVEN.GROUP_ID, new UserInputSearchExpression( term ) ), Occur.SHOULD ); q.add( indexer.constructQuery( MAVEN.ARTIFACT_ID, new UserInputSearchExpression( term ) ), Occur.SHOULD ); q.add( indexer.constructQuery( MAVEN.VERSION, new UserInputSearchExpression( term ) ), Occur.SHOULD ); q.add( indexer.constructQuery( MAVEN.PACKAGING, new UserInputSearchExpression( term ) ), Occur.SHOULD ); q.add( indexer.constructQuery( MAVEN.CLASSNAMES, new UserInputSearchExpression( term ) ), Occur.SHOULD ); //Query query = // new WildcardQuery( new Term( MAVEN.CLASSNAMES.getFieldName(), "*" ) ); //q.add( query, Occur.MUST_NOT ); // olamy IMHO we could set this option as at least one must match //q.setMinimumNumberShouldMatch( 1 ); } /** * @param selectedRepos * @return indexing contextId used */ private List<String> addIndexingContexts( List<String> selectedRepos ) { Set<String> indexingContextIds = new HashSet<>(); for ( String repo : selectedRepos ) { try { ManagedRepository repoConfig = managedRepositoryAdmin.getManagedRepository( repo ); if ( repoConfig != null ) { IndexingContext context = managedRepositoryAdmin.createIndexContext( repoConfig ); if ( context.isSearchable() ) { indexingContextIds.addAll( getRemoteIndexingContextIds( repo ) ); indexingContextIds.add( context.getId() ); } else { log.warn( "indexingContext with id {} not searchable", repoConfig.getId() ); } } else { log.warn( "Repository '{}' not found in configuration.", repo ); } } catch ( RepositoryAdminException e ) { log.warn( "RepositoryAdminException occured while accessing index of repository '{}' : {}", repo, e.getMessage() ); continue; } } return new ArrayList<>( indexingContextIds ); } @Override public Set<String> getRemoteIndexingContextIds( String managedRepoId ) throws RepositoryAdminException { Set<String> ids = new HashSet<>(); List<ProxyConnector> proxyConnectors = proxyConnectorAdmin.getProxyConnectorAsMap().get( managedRepoId ); if ( proxyConnectors == null || proxyConnectors.isEmpty() ) { return ids; } for ( ProxyConnector proxyConnector : proxyConnectors ) { String remoteId = "remote-" + proxyConnector.getTargetRepoId(); IndexingContext context = indexer.getIndexingContexts().get( remoteId ); if ( context != null && context.isSearchable() ) { ids.add( remoteId ); } } return ids; } @Override public Collection<String> getAllGroupIds( String principal, List<String> selectedRepos ) throws RepositorySearchException { List<IndexingContext> indexContexts = getIndexingContexts( selectedRepos ); if ( indexContexts == null || indexContexts.isEmpty() ) { return Collections.emptyList(); } try { Set<String> allGroupIds = new HashSet<>(); for ( IndexingContext indexingContext : indexContexts ) { allGroupIds.addAll( indexingContext.getAllGroups() ); } return allGroupIds; } catch ( IOException e ) { throw new RepositorySearchException( e.getMessage(), e ); } } protected List<? extends IndexCreator> getAllIndexCreators() { return mavenIndexerUtils.getAllIndexCreators(); } private SearchResults convertToSearchResults( FlatSearchResponse response, SearchResultLimits limits, List<? extends ArtifactInfoFilter> artifactInfoFilters, List<String> selectedRepos, boolean includePoms ) throws RepositoryAdminException { SearchResults results = new SearchResults(); Set<ArtifactInfo> artifactInfos = response.getResults(); for ( ArtifactInfo artifactInfo : artifactInfos ) { if ( StringUtils.equalsIgnoreCase( "pom", artifactInfo.fextension ) && !includePoms ) { continue; } String id = SearchUtil.getHitId( artifactInfo.groupId, artifactInfo.artifactId, artifactInfo.classifier, artifactInfo.packaging ); Map<String, SearchResultHit> hitsMap = results.getHitsMap(); if ( !applyArtifactInfoFilters( artifactInfo, artifactInfoFilters, hitsMap ) ) { continue; } SearchResultHit hit = hitsMap.get( id ); if ( hit != null ) { if ( !hit.getVersions().contains( artifactInfo.version ) ) { hit.addVersion( artifactInfo.version ); } } else { hit = new SearchResultHit(); hit.setArtifactId( artifactInfo.artifactId ); hit.setGroupId( artifactInfo.groupId ); hit.setRepositoryId( artifactInfo.repository ); hit.addVersion( artifactInfo.version ); hit.setBundleExportPackage( artifactInfo.bundleExportPackage ); hit.setBundleExportService( artifactInfo.bundleExportService ); hit.setBundleSymbolicName( artifactInfo.bundleSymbolicName ); hit.setBundleVersion( artifactInfo.bundleVersion ); hit.setBundleDescription( artifactInfo.bundleDescription ); hit.setBundleDocUrl( artifactInfo.bundleDocUrl ); hit.setBundleRequireBundle( artifactInfo.bundleRequireBundle ); hit.setBundleImportPackage( artifactInfo.bundleImportPackage ); hit.setBundleLicense( artifactInfo.bundleLicense ); hit.setBundleName( artifactInfo.bundleName ); hit.setContext( artifactInfo.context ); hit.setGoals( artifactInfo.goals ); hit.setPrefix( artifactInfo.prefix ); hit.setPackaging( artifactInfo.packaging ); hit.setClassifier( artifactInfo.classifier ); hit.setFileExtension( artifactInfo.fextension ); hit.setUrl( getBaseUrl( artifactInfo, selectedRepos ) ); } results.addHit( id, hit ); } results.setTotalHits( response.getTotalHitsCount() ); results.setTotalHitsMapSize( results.getHitsMap().values().size() ); results.setReturnedHitsCount( response.getReturnedHitsCount() ); results.setLimits( limits ); if ( limits == null || limits.getSelectedPage() == SearchResultLimits.ALL_PAGES ) { return results; } else { return paginate( results ); } } /** * calculate baseUrl without the context and base Archiva Url * * @param artifactInfo * @return */ protected String getBaseUrl( ArtifactInfo artifactInfo, List<String> selectedRepos ) throws RepositoryAdminException { StringBuilder sb = new StringBuilder(); if ( StringUtils.startsWith( artifactInfo.context, "remote-" ) ) { // it's a remote index result we search a managed which proxying this remote and on which // current user has read karma String managedRepoId = getManagedRepoId( StringUtils.substringAfter( artifactInfo.context, "remote-" ), selectedRepos ); if ( managedRepoId != null ) { sb.append( '/' ).append( managedRepoId ); artifactInfo.context = managedRepoId; } } else { sb.append( '/' ).append( artifactInfo.context ); } sb.append( '/' ).append( StringUtils.replaceChars( artifactInfo.groupId, '.', '/' ) ); sb.append( '/' ).append( artifactInfo.artifactId ); sb.append( '/' ).append( artifactInfo.version ); sb.append( '/' ).append( artifactInfo.artifactId ); sb.append( '-' ).append( artifactInfo.version ); if ( StringUtils.isNotBlank( artifactInfo.classifier ) ) { sb.append( '-' ).append( artifactInfo.classifier ); } // maven-plugin packaging is a jar if ( StringUtils.equals( "maven-plugin", artifactInfo.packaging ) ) { sb.append( "jar" ); } else { sb.append( '.' ).append( artifactInfo.packaging ); } return sb.toString(); } /** * return a managed repo for a remote result * * @param remoteRepo * @param selectedRepos * @return * @throws RepositoryAdminException */ private String getManagedRepoId( String remoteRepo, List<String> selectedRepos ) throws RepositoryAdminException { Map<String, List<ProxyConnector>> proxyConnectorMap = proxyConnectorAdmin.getProxyConnectorAsMap(); if ( proxyConnectorMap == null || proxyConnectorMap.isEmpty() ) { return null; } if ( selectedRepos != null && !selectedRepos.isEmpty() ) { for ( Map.Entry<String, List<ProxyConnector>> entry : proxyConnectorMap.entrySet() ) { if ( selectedRepos.contains( entry.getKey() ) ) { for ( ProxyConnector proxyConnector : entry.getValue() ) { if ( StringUtils.equals( remoteRepo, proxyConnector.getTargetRepoId() ) ) { return proxyConnector.getSourceRepoId(); } } } } } // we don't find in search selected repos so return the first one for ( Map.Entry<String, List<ProxyConnector>> entry : proxyConnectorMap.entrySet() ) { for ( ProxyConnector proxyConnector : entry.getValue() ) { if ( StringUtils.equals( remoteRepo, proxyConnector.getTargetRepoId() ) ) { return proxyConnector.getSourceRepoId(); } } } return null; } private boolean applyArtifactInfoFilters( ArtifactInfo artifactInfo, List<? extends ArtifactInfoFilter> artifactInfoFilters, Map<String, SearchResultHit> currentResult ) { if ( artifactInfoFilters == null || artifactInfoFilters.isEmpty() ) { return true; } for ( ArtifactInfoFilter filter : artifactInfoFilters ) { if ( !filter.addArtifactInResult( artifactInfo, currentResult ) ) { return false; } } return true; } protected SearchResults paginate( SearchResults results ) { SearchResultLimits limits = results.getLimits(); SearchResults paginated = new SearchResults(); // ( limits.getPageSize() * ( Math.max( 1, limits.getSelectedPage() ) ) ); int fetchCount = limits.getPageSize(); int offset = ( limits.getSelectedPage() * limits.getPageSize() ); if ( fetchCount > results.getTotalHits() ) { fetchCount = results.getTotalHits(); } // Goto offset. if ( offset < results.getTotalHits() ) { // only process if the offset is within the hit count. for ( int i = 0; i < fetchCount; i++ ) { // Stop fetching if we are past the total # of available hits. if ( offset + i >= results.getHits().size() ) { break; } SearchResultHit hit = results.getHits().get( ( offset + i ) ); if ( hit != null ) { String id = SearchUtil.getHitId( hit.getGroupId(), hit.getArtifactId(), hit.getClassifier(), hit.getPackaging() ); paginated.addHit( id, hit ); } else { break; } } } paginated.setTotalHits( results.getTotalHits() ); paginated.setReturnedHitsCount( paginated.getHits().size() ); paginated.setTotalHitsMapSize( results.getTotalHitsMapSize() ); paginated.setLimits( limits ); return paginated; } }