This is an automated email from the ASF dual-hosted git repository. cstamas pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-indexer.git
The following commit(s) were added to refs/heads/master by this push: new 1e532fd [MINDEXER-143] Introduce Search API and backends (#197) 1e532fd is described below commit 1e532fdc8624cb5798798e9f52641015a9894aa5 Author: Tamas Cservenak <ta...@cservenak.net> AuthorDate: Fri Apr 22 10:35:38 2022 +0200 [MINDEXER-143] Introduce Search API and backends (#197) Introduce Search API and provide back-ends. Introduced 3 new modules: search-api and two back-ends: indexer based one and SMO based one. --- pom.xml | 21 ++ search-api/README.md | 24 ++ search-api/header.txt | 17 ++ search-api/pom.xml | 51 ++++ .../main/java/org/apache/maven/search/MAVEN.java | 103 ++++++++ .../main/java/org/apache/maven/search/Record.java | 139 ++++++++++ .../org/apache/maven/search/SearchBackend.java | 38 +++ .../java/org/apache/maven/search/SearchEngine.java | 33 +++ .../org/apache/maven/search/SearchRequest.java | 82 ++++++ .../org/apache/maven/search/SearchResponse.java | 49 ++++ .../apache/maven/search/request/BooleanQuery.java | 87 +++++++ .../org/apache/maven/search/request/Field.java | 132 ++++++++++ .../apache/maven/search/request/FieldQuery.java | 62 +++++ .../org/apache/maven/search/request/Paging.java | 85 +++++++ .../org/apache/maven/search/request/Query.java | 57 +++++ .../maven/search/support/SearchBackendSupport.java | 58 +++++ .../search/support/SearchResponseSupport.java | 78 ++++++ search-backend-indexer/README.md | 14 + search-backend-indexer/header.txt | 17 ++ search-backend-indexer/pom.xml | 80 ++++++ .../backend/indexer/IndexerCoreSearchBackend.java | 34 +++ .../backend/indexer/IndexerCoreSearchResponse.java | 42 +++ .../internal/IndexerCoreSearchBackendImpl.java | 226 ++++++++++++++++ .../internal/IndexerCoreSearchResponseImpl.java | 61 +++++ .../internal/IndexerCoreSearchBackendImplTest.java | 283 +++++++++++++++++++++ search-backend-smo/README.md | 13 + search-backend-smo/header.txt | 17 ++ search-backend-smo/pom.xml | 66 +++++ .../main/filtered-resources/smo-version.properties | 18 ++ .../maven/search/backend/smo/SmoSearchBackend.java | 33 +++ .../search/backend/smo/SmoSearchResponse.java | 38 +++ .../backend/smo/internal/SmoSearchBackendImpl.java | 235 +++++++++++++++++ .../smo/internal/SmoSearchResponseImpl.java | 56 ++++ .../smo/internal/SmoSearchTransportSupport.java | 81 ++++++ .../internal/UrlConnectionSmoSearchTransport.java | 59 +++++ .../smo/internal/SmoSearchBackendImplTest.java | 172 +++++++++++++ 36 files changed, 2661 insertions(+) diff --git a/pom.xml b/pom.xml index 6f3a46e..bb34134 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,12 @@ under the License. <dependencyManagement> <dependencies> <!-- Reactor --> + <dependency> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>search-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> <groupId>org.apache.maven.indexer</groupId> <artifactId>indexer-core</artifactId> @@ -285,10 +291,25 @@ under the License. <module>indexer-cli</module> <module>indexer-reader</module> <module>indexer-examples</module> + <module>search-api</module> + <module>search-backend-indexer</module> + <module>search-backend-smo</module> </modules> <build> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.0.0</version> + <dependencies> + <dependency> + <groupId>org.codehaus.mojo</groupId> + <artifactId>extra-enforcer-rules</artifactId> + <version>1.4</version> + </dependency> + </dependencies> + </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> diff --git a/search-api/README.md b/search-api/README.md new file mode 100644 index 0000000..6205a7a --- /dev/null +++ b/search-api/README.md @@ -0,0 +1,24 @@ +Indexer Search API +================== + +Defines a simple Search API usable to most common Maven related searches. + +Example of GA search: + +```java + // obtain some backend instance + SearchBackend backend = ... + + SearchRequest searchRequest = new SearchRequest( and( + fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ), + fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ) ) + ); + SearchResponse searchResponse = backend.search( searchRequest ); + process( searchResponse.getPage() ); // here consume the page data + while ( searchResponse.getCurrentHits() > 0 ) // if ALL needed, page through it + { + searchResponse = backend.search( searchResponse.getSearchRequest().nextPage() ); + process( searchResponse.getPage() ); + } +``` + diff --git a/search-api/header.txt b/search-api/header.txt new file mode 100644 index 0000000..1a2ef73 --- /dev/null +++ b/search-api/header.txt @@ -0,0 +1,17 @@ +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. + diff --git a/search-api/pom.xml b/search-api/pom.xml new file mode 100644 index 0000000..a143cac --- /dev/null +++ b/search-api/pom.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>maven-indexer</artifactId> + <version>6.1.2-SNAPSHOT</version> + </parent> + + <artifactId>search-api</artifactId> + + <name>Maven :: Search API</name> + <description> + Indexer Search API. + </description> + + <dependencies> + <!-- Test --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/search-api/src/main/java/org/apache/maven/search/MAVEN.java b/search-api/src/main/java/org/apache/maven/search/MAVEN.java new file mode 100644 index 0000000..99f3cb4 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/MAVEN.java @@ -0,0 +1,103 @@ +package org.apache.maven.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.maven.search.request.Field.BooleanField; +import org.apache.maven.search.request.Field.NumberField; +import org.apache.maven.search.request.Field.StringField; + +/** + * The ontology of Apache Maven related fields. + */ +public final class MAVEN +{ + private MAVEN() + { + // no instances + } + + /** + * String field for artifact groupId. Searchable. + */ + public static final StringField GROUP_ID = new StringField( "groupId", true ); + + /** + * String field for artifact artifactId. Searchable. + */ + public static final StringField ARTIFACT_ID = new StringField( "artifactId", true ); + + /** + * String field for artifact version. Searchable. + */ + public static final StringField VERSION = new StringField( "version", true ); + + /** + * String field for artifact classifier. Searchable. + */ + public static final StringField CLASSIFIER = new StringField( "classifier", true ); + + /** + * String field for artifact packaging. Searchable. + */ + public static final StringField PACKAGING = new StringField( "packaging", true ); + + /** + * String field for artifact contained Java class name. Searchable, but not present in resulting records. + */ + public static final StringField CLASS_NAME = new StringField( "cn", true ); + + /** + * String field for artifact contained FQ Java class name. Searchable, but not present in resulting records. + */ + public static final StringField FQ_CLASS_NAME = new StringField( "fqcn", true ); + + /** + * String field for artifact SHA1 checksum. Searchable, but not present in resulting records. + */ + public static final StringField SHA1 = new StringField( "sha1", true ); + + /** + * String field for artifact file extension. Non-searchable. Indexer backend specific. + */ + public static final StringField FILE_EXTENSION = new StringField( "fileExtension", false ); + + /** + * Number field carrying {@link Integer}, representing the count of versions for given GA. Non-searchable. + */ + public static final NumberField VERSION_COUNT = new NumberField( "versionCount", false ); + + /** + * Boolean field representing the known presence/absence of artifact sources (is {@code -sources.jar} present). + * Non-searchable. + */ + public static final BooleanField HAS_SOURCE = new BooleanField( "source", false ); + + /** + * Boolean field representing the known presence/absence of artifact Javadoc (is {@code -javadoc.jar} present). + * Non-searchable. + */ + public static final BooleanField HAS_JAVADOC = new BooleanField( "javadoc", false ); + + /** + * Boolean field representing the known presence/absence of artifact GPG signature. Non-searchable. Indexer + * backend specific. + */ + public static final BooleanField HAS_GPG_SIGNATURE = new BooleanField( "gpg", false ); +} diff --git a/search-api/src/main/java/org/apache/maven/search/Record.java b/search-api/src/main/java/org/apache/maven/search/Record.java new file mode 100644 index 0000000..0f99b0e --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/Record.java @@ -0,0 +1,139 @@ +package org.apache.maven.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 java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.apache.maven.search.request.Field; + +import static java.util.Objects.requireNonNull; + +/** + * A search response record. + */ +public final class Record +{ + private final String backendId; + + private final String repositoryId; + + private final String uid; + + private final Long lastUpdated; + + private final Map<Field, Object> fields; + + public Record( String backendId, + String repositoryId, + String uid, + Long lastUpdated, + Map<Field, Object> fields ) + { + this.backendId = requireNonNull( backendId ); + this.repositoryId = requireNonNull( repositoryId ); + this.uid = uid; + this.lastUpdated = lastUpdated; + this.fields = Collections.unmodifiableMap( fields ); + } + + /** + * Returns {@link SearchBackend#getBackendId()} of originating search backend. Never {@code null}. + */ + public String getBackendId() + { + return backendId; + } + + /** + * Returns {@link SearchBackend#getRepositoryId()}) of originating search backend. Never {@code null}. + */ + public String getRepositoryId() + { + return repositoryId; + } + + /** + * Returns UID (unique if combined with {@link #getBackendId()}) of search result record, if provided by backend. + * May be {@code null} if not provided. + */ + public String getUid() + { + return uid; + } + + /** + * Returns {@link Long}, representing "last updated" timestamp as epoch millis if provided by backend. May be + * {@code null} if not provided. + */ + public Long getLastUpdated() + { + return lastUpdated; + } + + /** + * Returns unmodifiable map of all values keyed by {@link Field} backing this record. + */ + public Map<Field, Object> getFields() + { + return fields; + } + + /** + * Returns unmodifiable set of present fields in this record, never {@code null}. + */ + public Set<Field> fieldSet() + { + return fields.keySet(); + } + + /** + * Returns {@code true} if given field is present in this record. + */ + public boolean hasField( Field field ) + { + return fields.containsKey( field ); + } + + /** + * Returns the value belonging to given field in this record, or {@code null} if field not present. + */ + public String getValue( Field.StringField field ) + { + return field.getFieldValue( fields ); + } + + /** + * Returns the value belonging to given field in this record, or {@code null} if field not present. + */ + public Number getValue( Field.NumberField field ) + { + return field.getFieldValue( fields ); + } + + /** + * Returns the value belonging to given field in this record, or {@code null} if field not present. + */ + public Boolean getValue( Field.BooleanField field ) + { + return field.getFieldValue( fields ); + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/SearchBackend.java b/search-api/src/main/java/org/apache/maven/search/SearchBackend.java new file mode 100644 index 0000000..68fecd3 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/SearchBackend.java @@ -0,0 +1,38 @@ +package org.apache.maven.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 java.io.Closeable; + +/** + * An engine to perform search trough single repository index (backend). + */ +public interface SearchBackend extends SearchEngine, Closeable +{ + /** + * Returns the ID of this backend, never {@code null}. + */ + String getBackendId(); + + /** + * Returns the repository ID that this backend searches for, never {@code null}. + */ + String getRepositoryId(); +} diff --git a/search-api/src/main/java/org/apache/maven/search/SearchEngine.java b/search-api/src/main/java/org/apache/maven/search/SearchEngine.java new file mode 100644 index 0000000..cd82039 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/SearchEngine.java @@ -0,0 +1,33 @@ +package org.apache.maven.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 java.io.IOException; + +/** + * A search engine to perform searches trough configured repository indexes. + */ +public interface SearchEngine +{ + /** + * Performs a search with given {@link SearchRequest} and returns {@link SearchResponse}, never {@code null}. + */ + SearchResponse search( SearchRequest searchRequest ) throws IOException; +} diff --git a/search-api/src/main/java/org/apache/maven/search/SearchRequest.java b/search-api/src/main/java/org/apache/maven/search/SearchRequest.java new file mode 100644 index 0000000..c71fde8 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/SearchRequest.java @@ -0,0 +1,82 @@ +package org.apache.maven.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.maven.search.request.Paging; +import org.apache.maven.search.request.Query; + +import static java.util.Objects.requireNonNull; + +/** + * A search request to perform search: defines paging and query. + */ +public final class SearchRequest +{ + private final Paging paging; + + private final Query query; + + /** + * Creates a request with given {@link Query} instance and default page size of 50. + */ + public SearchRequest( Query query ) + { + this( new Paging( 50 ), query ); + } + + /** + * Creates a request with given {@link Query} and {@link Paging}. + */ + public SearchRequest( Paging paging, Query query ) + { + this.paging = requireNonNull( paging ); + this.query = requireNonNull( query ); + } + + /** + * The {@link Paging} of this request: defines page size and page offset, never {@code null}. + */ + public Paging getPaging() + { + return paging; + } + + /** + * The {@link Query} of this request, never {@code null}. + */ + public Query getQuery() + { + return query; + } + + /** + * Returns a new {@link SearchRequest} instance for "next page" relative to this instance, never {@code null}. + */ + public SearchRequest nextPage() + { + return new SearchRequest( paging.nextPage(), query ); + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "{" + "paging=" + paging + ", query=" + query + '}'; + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/SearchResponse.java b/search-api/src/main/java/org/apache/maven/search/SearchResponse.java new file mode 100644 index 0000000..969b10a --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/SearchResponse.java @@ -0,0 +1,49 @@ +package org.apache.maven.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 java.util.List; + +/** + * A search engine response. + */ +public interface SearchResponse +{ + /** + * Returns the {@link SearchRequest} used for this response, never {@code null}. + */ + SearchRequest getSearchRequest(); + + /** + * Returns the total count of hits produced by {@link #getSearchRequest()}. + */ + int getTotalHits(); + + /** + * Returns the count of current hits in current "page". It may be less or equal to page size of {@link + * SearchRequest#getPaging()}. + */ + int getCurrentHits(); + + /** + * Returns current "page" of results as list of records, never {@code null}. + */ + List<Record> getPage(); +} diff --git a/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java b/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java new file mode 100644 index 0000000..0a4619d --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/request/BooleanQuery.java @@ -0,0 +1,87 @@ +package org.apache.maven.search.request; + +/* + * 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 static java.util.Objects.requireNonNull; + +/** + * Boolean query. + */ +public abstract class BooleanQuery extends Query +{ + private final Query left; + + private final Query right; + + protected BooleanQuery( Query left, String op, Query right ) + { + super( op ); + this.left = requireNonNull( left ); + this.right = requireNonNull( right ); + } + + /** + * Returns left term of this boolean query, never {@code null}. + */ + public Query getLeft() + { + return left; + } + + /** + * Returns right term of this boolean query, never {@code null}. + */ + public Query getRight() + { + return right; + } + + @Override + public String toString() + { + return getLeft() + " " + getValue() + " " + getRight(); + } + + public static final class And extends BooleanQuery + { + private And( Query left, Query right ) + { + super( left, "AND", right ); + } + } + + /** + * Creates Logical AND query (requires presence of all queries) out of passed in queries (at least 2 or more + * should be given). + */ + public static BooleanQuery and( Query left, Query... rights ) + { + if ( rights.length == 0 ) + { + throw new IllegalArgumentException( "one or more on right needed" ); + } + BooleanQuery result = null; + for ( Query right : rights ) + { + result = new And( result == null ? left : result, right ); + } + return result; + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/request/Field.java b/search-api/src/main/java/org/apache/maven/search/request/Field.java new file mode 100644 index 0000000..558a676 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/request/Field.java @@ -0,0 +1,132 @@ +package org.apache.maven.search.request; + +/* + * 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 java.util.Map; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * Field, that is used as key in record. + */ +public abstract class Field +{ + private final String fieldName; + + private final boolean searchable; + + private Field( String fieldName, boolean searchable ) + { + this.fieldName = requireNonNull( fieldName ); + this.searchable = searchable; + } + + /** + * Returns the field name. + */ + public String getFieldName() + { + return fieldName; + } + + /** + * Returns {@code true} if field may be used for {@link FieldQuery} (is searchable). + */ + public boolean isSearchable() + { + return searchable; + } + + /** + * Returns the value of the field from given record instance, or {@code null} if field not present in record. + * See subclasses for proper return types. + */ + public abstract Object getFieldValue( Map<Field, Object> record ); + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + Field fieldName1 = (Field) o; + return Objects.equals( getFieldName(), fieldName1.getFieldName() ); + } + + @Override + public int hashCode() + { + return Objects.hash( getFieldName() ); + } + + @Override + public String toString() + { + return fieldName; + } + + public static class StringField extends Field + { + public StringField( String fieldName, boolean searchable ) + { + super( fieldName, searchable ); + } + + @Override + public String getFieldValue( Map<Field, Object> record ) + { + return (String) record.get( this ); + } + } + + public static class NumberField extends Field + { + public NumberField( String fieldName, boolean searchable ) + { + super( fieldName, searchable ); + } + + @Override + public Number getFieldValue( Map<Field, Object> record ) + { + return (Number) record.get( this ); + } + } + + public static class BooleanField extends Field + { + public BooleanField( String fieldName, boolean searchable ) + { + super( fieldName, searchable ); + } + + @Override + public Boolean getFieldValue( Map<Field, Object> record ) + { + return (Boolean) record.get( this ); + } + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java b/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java new file mode 100644 index 0000000..63f8497 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/request/FieldQuery.java @@ -0,0 +1,62 @@ +package org.apache.maven.search.request; + +/* + * 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 static java.util.Objects.requireNonNull; + +/** + * Field query. + */ +public class FieldQuery extends Query +{ + private final Field field; + + protected FieldQuery( Field field, String queryString ) + { + super( queryString ); + if ( !field.isSearchable() ) + { + throw new IllegalArgumentException( "Field is not searchable: " + field ); + } + this.field = requireNonNull( field ); + } + + /** + * Returns the field, never {@code null}. + */ + public Field getField() + { + return field; + } + + @Override + public String toString() + { + return getField().getFieldName() + ":" + getValue(); + } + + /** + * Creates a field query using given {@link Field} and query string. + */ + public static FieldQuery fieldQuery( Field fieldName, String query ) + { + return new FieldQuery( fieldName, query ); + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/request/Paging.java b/search-api/src/main/java/org/apache/maven/search/request/Paging.java new file mode 100644 index 0000000..290a3b2 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/request/Paging.java @@ -0,0 +1,85 @@ +package org.apache.maven.search.request; + +/* + * 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. + */ + +/** + * Paging. + */ +public final class Paging +{ + private final int pageSize; + + private final int pageOffset; + + /** + * Creates paging instance with given page size (must be greater than 0) and page offset (must be non-negative). + */ + public Paging( int pageSize, int pageOffset ) + { + if ( pageSize < 1 ) + { + throw new IllegalArgumentException( "pageSize" ); + } + if ( pageOffset < 0 ) + { + throw new IllegalArgumentException( "pageOffset" ); + } + this.pageSize = pageSize; + this.pageOffset = pageOffset; + } + + /** + * Creates paging instance with given page size (must be grater than 0) and 0 page offset. + */ + public Paging( int pageSize ) + { + this( pageSize, 0 ); + } + + /** + * Returns the page size: positive integer, never zero or less. + */ + public int getPageSize() + { + return pageSize; + } + + /** + * Returns the page offset: a zero or a positive integer. + */ + public int getPageOffset() + { + return pageOffset; + } + + /** + * Creates "next page" instance relative to this instance. + */ + public Paging nextPage() + { + return new Paging( pageSize, pageOffset + 1 ); + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "{pageSize=" + pageSize + ", pageOffset=" + pageOffset + "}"; + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/request/Query.java b/search-api/src/main/java/org/apache/maven/search/request/Query.java new file mode 100644 index 0000000..35ddc59 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/request/Query.java @@ -0,0 +1,57 @@ +package org.apache.maven.search.request; + +/* + * 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 static java.util.Objects.requireNonNull; + +/** + * Query. + */ +public class Query +{ + private final String queryString; + + protected Query( String queryString ) + { + this.queryString = requireNonNull( queryString ); + } + + /** + * Returns the query string value, never {@code null}. + */ + public String getValue() + { + return queryString; + } + + @Override + public String toString() + { + return getValue(); + } + + /** + * Creates a plain query. + */ + public static Query query( String queryString ) + { + return new Query( queryString ); + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java b/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java new file mode 100644 index 0000000..7697942 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/support/SearchBackendSupport.java @@ -0,0 +1,58 @@ +package org.apache.maven.search.support; + +/* + * 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.maven.search.SearchBackend; + +import static java.util.Objects.requireNonNull; + +/** + * A search backend support class. + */ +public abstract class SearchBackendSupport implements SearchBackend +{ + private final String backendId; + + private final String repositoryId; + + protected SearchBackendSupport( String backendId, String repositoryId ) + { + this.backendId = requireNonNull( backendId ); + this.repositoryId = requireNonNull( repositoryId ); + } + + @Override + public String getBackendId() + { + return backendId; + } + + @Override + public String getRepositoryId() + { + return repositoryId; + } + + @Override + public void close() + { + // override if needed + } +} diff --git a/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java b/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java new file mode 100644 index 0000000..ff8fa35 --- /dev/null +++ b/search-api/src/main/java/org/apache/maven/search/support/SearchResponseSupport.java @@ -0,0 +1,78 @@ +package org.apache.maven.search.support; + +/* + * 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 java.util.List; + +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.SearchResponse; + +import static java.util.Objects.requireNonNull; + +/** + * A search response support class. + */ +public abstract class SearchResponseSupport implements SearchResponse +{ + private final SearchRequest searchRequest; + + private final int totalHits; + + private final List<Record> page; + + protected SearchResponseSupport( SearchRequest searchRequest, int totalHits, List<Record> page ) + { + this.searchRequest = requireNonNull( searchRequest ); + this.totalHits = totalHits; + this.page = requireNonNull( page ); + } + + @Override + public SearchRequest getSearchRequest() + { + return searchRequest; + } + + @Override + public int getTotalHits() + { + return totalHits; + } + + @Override + public int getCurrentHits() + { + return page.size(); + } + + @Override + public List<Record> getPage() + { + return page; + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "{" + "searchRequest=" + searchRequest + ", totalHits=" + totalHits + + ", page=" + page + '}'; + } +} diff --git a/search-backend-indexer/README.md b/search-backend-indexer/README.md new file mode 100644 index 0000000..e4c3116 --- /dev/null +++ b/search-backend-indexer/README.md @@ -0,0 +1,14 @@ +Indexer Search Indexer Backend +============================== + +Search API Indexer Core Backend implementation. + +Examples: + +```java + // obtain Indexer instance + Indexer indexer = ... + IndexingContext indexingContext = ... + + SearchBackend backend = new IndexerCoreSearchBackendImpl( indexer, indexingContext ); +``` \ No newline at end of file diff --git a/search-backend-indexer/header.txt b/search-backend-indexer/header.txt new file mode 100644 index 0000000..1a2ef73 --- /dev/null +++ b/search-backend-indexer/header.txt @@ -0,0 +1,17 @@ +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. + diff --git a/search-backend-indexer/pom.xml b/search-backend-indexer/pom.xml new file mode 100644 index 0000000..cf0643d --- /dev/null +++ b/search-backend-indexer/pom.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>maven-indexer</artifactId> + <version>6.1.2-SNAPSHOT</version> + </parent> + + <artifactId>search-backend-indexer</artifactId> + + <name>Maven :: Search API Indexer Backend</name> + <description> + Indexer Search Backend implemented by Indexer Core. + </description> + + <dependencies> + <dependency> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>search-api</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>indexer-core</artifactId> + </dependency> + + <!-- Test --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.eclipse.sisu</groupId> + <artifactId>org.eclipse.sisu.plexus</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.maven.wagon</groupId> + <artifactId>wagon-http-lightweight</artifactId> + <version>${wagon.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + +</project> diff --git a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java new file mode 100644 index 0000000..9b9843a --- /dev/null +++ b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchBackend.java @@ -0,0 +1,34 @@ +package org.apache.maven.search.backend.indexer; + +/* + * 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.maven.index.context.IndexingContext; +import org.apache.maven.search.SearchBackend; + +/** + * The Indexer Core search backend. + */ +public interface IndexerCoreSearchBackend extends SearchBackend +{ + /** + * Returns the {@link IndexingContext} used by this search backend, never {@code null}. + */ + IndexingContext getIndexingContext(); +} diff --git a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java new file mode 100644 index 0000000..727b526 --- /dev/null +++ b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/IndexerCoreSearchResponse.java @@ -0,0 +1,42 @@ +package org.apache.maven.search.backend.indexer; + +/* + * 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 java.util.List; + +import org.apache.lucene.search.Query; +import org.apache.maven.index.ArtifactInfo; +import org.apache.maven.search.SearchResponse; + +/** + * The Indexer Core search response. + */ +public interface IndexerCoreSearchResponse extends SearchResponse +{ + /** + * Returns the Lucene query used to create this response, never {@code null}. + */ + Query getQuery(); + + /** + * Returns the "raw" list of {@link ArtifactInfo}s used to create this response, never {@code null}. + */ + List<ArtifactInfo> getArtifactInfos(); +} diff --git a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java new file mode 100644 index 0000000..62ec217 --- /dev/null +++ b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImpl.java @@ -0,0 +1,226 @@ +package org.apache.maven.search.backend.indexer.internal; + +/* + * 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 java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.maven.index.ArtifactAvailability; +import org.apache.maven.index.ArtifactInfo; +import org.apache.maven.index.GroupedSearchRequest; +import org.apache.maven.index.GroupedSearchResponse; +import org.apache.maven.index.Indexer; +import org.apache.maven.index.IteratorSearchRequest; +import org.apache.maven.index.IteratorSearchResponse; +import org.apache.maven.index.SearchType; +import org.apache.maven.index.context.IndexingContext; +import org.apache.maven.index.expr.SourcedSearchExpression; +import org.apache.maven.index.search.grouping.GAGrouping; +import org.apache.maven.search.MAVEN; +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.backend.indexer.IndexerCoreSearchBackend; +import org.apache.maven.search.backend.indexer.IndexerCoreSearchResponse; +import org.apache.maven.search.request.Field; +import org.apache.maven.search.request.Paging; +import org.apache.maven.search.support.SearchBackendSupport; +import org.apache.maven.search.request.FieldQuery; + +import static java.util.Objects.requireNonNull; + +/** + * An engine to perform search trough single repository index (endpoint). + */ +public class IndexerCoreSearchBackendImpl extends SearchBackendSupport implements IndexerCoreSearchBackend +{ + private static final Map<Field, org.apache.maven.index.Field> FIELD_TRANSLATION; + + static + { + HashMap<Field, org.apache.maven.index.Field> map = new HashMap<>(); + map.put( MAVEN.GROUP_ID, org.apache.maven.index.MAVEN.GROUP_ID ); + map.put( MAVEN.ARTIFACT_ID, org.apache.maven.index.MAVEN.ARTIFACT_ID ); + map.put( MAVEN.VERSION, org.apache.maven.index.MAVEN.VERSION ); + map.put( MAVEN.CLASSIFIER, org.apache.maven.index.MAVEN.CLASSIFIER ); + map.put( MAVEN.PACKAGING, org.apache.maven.index.MAVEN.PACKAGING ); + map.put( MAVEN.CLASS_NAME, org.apache.maven.index.MAVEN.CLASSNAMES ); + map.put( MAVEN.FQ_CLASS_NAME, org.apache.maven.index.MAVEN.CLASSNAMES ); + map.put( MAVEN.SHA1, org.apache.maven.index.MAVEN.SHA1 ); + FIELD_TRANSLATION = Collections.unmodifiableMap( map ); + } + + private final Indexer indexer; + + private final IndexingContext indexingContext; + + /** + * Creates backend instance using provided indexer and context. + */ + public IndexerCoreSearchBackendImpl( Indexer indexer, IndexingContext indexingContext ) + { + super( indexingContext.getId(), indexingContext.getRepositoryId() ); + this.indexer = requireNonNull( indexer ); + this.indexingContext = indexingContext; + } + + @Override + public IndexingContext getIndexingContext() + { + return indexingContext; + } + + @Override + public IndexerCoreSearchResponse search( SearchRequest searchRequest ) throws IOException + { + Paging paging = searchRequest.getPaging(); + int totalHitsCount; + List<ArtifactInfo> artifactInfos = new ArrayList<>( paging.getPageSize() ); + List<Record> page = new ArrayList<>( paging.getPageSize() ); + + // if GA present in query: doing flat, otherwise grouped search to mimic SMO + HashSet<Field> searchedFields = new HashSet<>(); + Query query = toQuery( searchedFields, searchRequest.getQuery() ); + if ( searchedFields.contains( MAVEN.SHA1 ) || ( searchedFields.contains( MAVEN.GROUP_ID ) + && searchedFields.contains( MAVEN.ARTIFACT_ID ) ) ) + { + if ( !searchedFields.contains( MAVEN.CLASSIFIER ) ) + { + query = new BooleanQuery.Builder().add( new BooleanClause( query, BooleanClause.Occur.MUST ) ) + .add( indexer.constructQuery( org.apache.maven.index.MAVEN.CLASSIFIER, + new SourcedSearchExpression( org.apache.maven.index.Field.NOT_PRESENT ) ), + BooleanClause.Occur.MUST_NOT ).build(); + } + IteratorSearchRequest iteratorSearchRequest = + new IteratorSearchRequest( query, Collections.singletonList( indexingContext ) ); + iteratorSearchRequest.setCount( paging.getPageSize() ); + iteratorSearchRequest.setStart( paging.getPageSize() * paging.getPageOffset() ); + + try ( IteratorSearchResponse iteratorSearchResponse = indexer.searchIterator( iteratorSearchRequest ) ) + { + totalHitsCount = iteratorSearchResponse.getTotalHitsCount(); + StreamSupport.stream( iteratorSearchResponse.iterator().spliterator(), false ) + .sorted( ArtifactInfo.VERSION_COMPARATOR ).forEach( ai -> + { + artifactInfos.add( ai ); + page.add( convert( ai, null ) ); + } ); + } + return new IndexerCoreSearchResponseImpl( searchRequest, totalHitsCount, page, query, artifactInfos ); + } + else + { + GroupedSearchRequest groupedSearchRequest = + new GroupedSearchRequest( query, new GAGrouping(), indexingContext ); + + try ( GroupedSearchResponse groupedSearchResponse = indexer.searchGrouped( groupedSearchRequest ) ) + { + totalHitsCount = groupedSearchResponse.getResults().size(); + groupedSearchResponse.getResults().values().stream() + .skip( (long) paging.getPageSize() * paging.getPageOffset() ).limit( paging.getPageSize() ) + .forEach( aig -> + { + ArtifactInfo ai = aig.getArtifactInfos().iterator().next(); + artifactInfos.add( ai ); + page.add( convert( ai, aig.getArtifactInfos().size() ) ); + } ); + } + return new IndexerCoreSearchResponseImpl( searchRequest, totalHitsCount, page, query, artifactInfos ); + } + } + + private Query toQuery( HashSet<Field> searchedFields, org.apache.maven.search.request.Query query ) + { + if ( query instanceof org.apache.maven.search.request.BooleanQuery.And ) + { + org.apache.maven.search.request.BooleanQuery bq = + (org.apache.maven.search.request.BooleanQuery) query; + return new BooleanQuery.Builder().add( + new BooleanClause( toQuery( searchedFields, bq.getLeft() ), BooleanClause.Occur.MUST ) ) + .add( new BooleanClause( toQuery( searchedFields, bq.getRight() ), BooleanClause.Occur.MUST ) ) + .build(); + } + else if ( query instanceof FieldQuery ) + { + FieldQuery fq = + (FieldQuery) query; + org.apache.maven.index.Field icFieldName = FIELD_TRANSLATION.get( fq.getField() ); + if ( icFieldName != null ) + { + searchedFields.add( fq.getField() ); + if ( fq.getValue().endsWith( "*" ) ) + { + return indexer.constructQuery( icFieldName, fq.getValue(), SearchType.SCORED ); + } + else + { + return indexer.constructQuery( icFieldName, fq.getValue(), SearchType.EXACT ); + } + } + else + { + throw new IllegalArgumentException( "Unsupported Indexer field: " + fq.getField() ); + } + } + return new BooleanQuery.Builder().add( new BooleanClause( + indexer.constructQuery( org.apache.maven.index.MAVEN.GROUP_ID, query.getValue(), SearchType.SCORED ), + BooleanClause.Occur.SHOULD ) ).add( new BooleanClause( + indexer.constructQuery( org.apache.maven.index.MAVEN.ARTIFACT_ID, query.getValue(), SearchType.SCORED ), + BooleanClause.Occur.SHOULD ) ).add( new BooleanClause( + indexer.constructQuery( org.apache.maven.index.MAVEN.NAME, query.getValue(), SearchType.SCORED ), + BooleanClause.Occur.SHOULD ) ).build(); + } + + private Record convert( ArtifactInfo ai, /* nullable */ Integer versionCount ) + { + HashMap<Field, Object> result = new HashMap<>(); + + mayPut( result, MAVEN.GROUP_ID, ai.getGroupId() ); + mayPut( result, MAVEN.ARTIFACT_ID, ai.getArtifactId() ); + mayPut( result, MAVEN.VERSION, ai.getVersion() ); + mayPut( result, MAVEN.PACKAGING, ai.getPackaging() ); + mayPut( result, MAVEN.CLASSIFIER, ai.getClassifier() ); + mayPut( result, MAVEN.FILE_EXTENSION, ai.getFileExtension() ); + + mayPut( result, MAVEN.VERSION_COUNT, versionCount ); + + mayPut( result, MAVEN.HAS_SOURCE, ai.getSourcesExists() == ArtifactAvailability.PRESENT ); + mayPut( result, MAVEN.HAS_JAVADOC, ai.getJavadocExists() == ArtifactAvailability.PRESENT ); + mayPut( result, MAVEN.HAS_GPG_SIGNATURE, ai.getSignatureExists() == ArtifactAvailability.PRESENT ); + + return new Record( getBackendId(), getRepositoryId(), ai.getUinfo(), ai.getLastModified(), result ); + } + + private static void mayPut( Map<Field, Object> result, Field fieldName, /* nullable */ Object value ) + { + if ( value != null ) + { + result.put( fieldName, value ); + } + } +} diff --git a/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java new file mode 100644 index 0000000..d33206c --- /dev/null +++ b/search-backend-indexer/src/main/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchResponseImpl.java @@ -0,0 +1,61 @@ +package org.apache.maven.search.backend.indexer.internal; + +/* + * 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 java.util.List; + +import org.apache.lucene.search.Query; +import org.apache.maven.index.ArtifactInfo; +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.backend.indexer.IndexerCoreSearchResponse; +import org.apache.maven.search.support.SearchResponseSupport; + +import static java.util.Objects.requireNonNull; + +/** + * An engine to perform search trough single repository index (endpoint). + */ +public class IndexerCoreSearchResponseImpl extends SearchResponseSupport implements IndexerCoreSearchResponse +{ + private final Query query; + + private final List<ArtifactInfo> artifactInfos; + + public IndexerCoreSearchResponseImpl( SearchRequest searchRequest, int totalHits, List<Record> page, + Query query, List<ArtifactInfo> artifactInfos ) + { + super( searchRequest, totalHits, page ); + this.query = requireNonNull( query ); + this.artifactInfos = requireNonNull( artifactInfos ); + } + + @Override + public Query getQuery() + { + return query; + } + + @Override + public List<ArtifactInfo> getArtifactInfos() + { + return artifactInfos; + } +} diff --git a/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java b/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java new file mode 100644 index 0000000..2250841 --- /dev/null +++ b/search-backend-indexer/src/test/java/org/apache/maven/search/backend/indexer/internal/IndexerCoreSearchBackendImplTest.java @@ -0,0 +1,283 @@ +package org.apache.maven.search.backend.indexer.internal; + +/* + * 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.maven.index.Indexer; +import org.apache.maven.index.context.IndexCreator; +import org.apache.maven.index.context.IndexingContext; +import org.apache.maven.search.MAVEN; +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.SearchResponse; +import org.apache.maven.index.updater.IndexUpdateRequest; +import org.apache.maven.index.updater.IndexUpdateResult; +import org.apache.maven.index.updater.IndexUpdater; +import org.apache.maven.index.updater.ResourceFetcher; +import org.apache.maven.index.updater.WagonHelper; +import org.apache.maven.search.request.FieldQuery; +import org.apache.maven.wagon.Wagon; +import org.apache.maven.wagon.events.TransferEvent; +import org.apache.maven.wagon.events.TransferListener; +import org.apache.maven.wagon.observers.AbstractTransferListener; +import org.codehaus.plexus.DefaultContainerConfiguration; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusConstants; +import org.codehaus.plexus.PlexusContainer; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.apache.maven.search.request.BooleanQuery.and; +import static org.apache.maven.search.request.Query.query; + +@Ignore("This is more a showcase") +public class IndexerCoreSearchBackendImplTest +{ + private PlexusContainer plexusContainer; + + private Indexer indexer; + + private IndexUpdater indexUpdater; + + private Wagon httpWagon; + + private IndexingContext centralContext; + + private IndexerCoreSearchBackendImpl backend; + + private void dumpSingle( AtomicInteger counter, List<Record> page ) + { + for ( Record record : page ) + { + StringBuilder sb = new StringBuilder(); + sb.append( record.getValue( MAVEN.GROUP_ID ) ).append( ":" ).append( record.getValue( MAVEN.ARTIFACT_ID ) ) + .append( ":" ).append( record.getValue( MAVEN.VERSION ) ); + if ( record.hasField( MAVEN.PACKAGING ) ) + { + if ( record.hasField( MAVEN.CLASSIFIER ) ) + { + sb.append( ":" ).append( record.getValue( MAVEN.CLASSIFIER ) ); + } + sb.append( ":" ).append( record.getValue( MAVEN.PACKAGING ) ); + } + + List<String> remarks = new ArrayList<>(); + if ( record.getLastUpdated() != null ) + { + remarks.add( "lastUpdate=" + Instant.ofEpochMilli( record.getLastUpdated() ) ); + } + if ( record.hasField( MAVEN.VERSION_COUNT ) ) + { + remarks.add( "versionCount=" + record.getValue( MAVEN.VERSION_COUNT ) ); + } + if ( record.hasField( MAVEN.HAS_SOURCE ) ) + { + remarks.add( "hasSource=" + record.getValue( MAVEN.HAS_SOURCE ) ); + } + if ( record.hasField( MAVEN.HAS_JAVADOC ) ) + { + remarks.add( "hasJavadoc=" + record.getValue( MAVEN.HAS_JAVADOC ) ); + } + + System.out.print( counter.incrementAndGet() + ". " + sb ); + if ( !remarks.isEmpty() ) + { + System.out.print( " " + remarks ); + } + System.out.println(); + } + } + + private void dumpPage( SearchResponse searchResponse ) throws IOException + { + AtomicInteger counter = new AtomicInteger( 0 ); + System.out.println("QUERY: " + searchResponse.getSearchRequest().getQuery().toString()); + dumpSingle( counter, searchResponse.getPage() ); + while ( searchResponse.getCurrentHits() > 0 ) + { + searchResponse = backend.search( searchResponse.getSearchRequest().nextPage() ); + dumpSingle( counter, searchResponse.getPage() ); + if ( counter.get() > 50 ) + { + System.out.println( "ABORTED TO NOT SPAM" ); + break; // do not spam the SMO service + } + } + System.out.println(); + } + + @Before + public void prepareAndUpdateBackend() throws Exception + { + final DefaultContainerConfiguration config = new DefaultContainerConfiguration(); + config.setClassPathScanning( PlexusConstants.SCANNING_CACHE ); + this.plexusContainer = new DefaultPlexusContainer( config ); + + // lookup the indexer components from plexus + this.indexer = plexusContainer.lookup( Indexer.class ); + this.indexUpdater = plexusContainer.lookup( IndexUpdater.class ); + // lookup wagon used to remotely fetch index + this.httpWagon = plexusContainer.lookup( Wagon.class, "http" ); + + // Files where local cache is (if any) and Lucene Index should be located + File centralLocalCache = new File( "target/central-cache" ); + File centralIndexDir = new File( "target/central-index" ); + + // Creators we want to use (search for fields it defines) + List<IndexCreator> indexers = new ArrayList<>(); + indexers.add( plexusContainer.lookup( IndexCreator.class, "min" ) ); + indexers.add( plexusContainer.lookup( IndexCreator.class, "jarContent" ) ); + indexers.add( plexusContainer.lookup( IndexCreator.class, "maven-plugin" ) ); + + // Create context for central repository index + centralContext = indexer.createIndexingContext( "central-context", "central", centralLocalCache, + centralIndexDir, "https://repo1.maven.org/maven2", null, true, true, indexers ); + + // Update the index (incremental update will happen if this is not 1st run and files are not deleted) + // This whole block below should not be executed on every app start, but rather controlled by some configuration + // since this block will always emit at least one HTTP GET. Central indexes are updated once a week, but + // other index sources might have different index publishing frequency. + // Preferred frequency is once a week. + System.out.println( "Updating Index..." ); + System.out.println( "This might take a while on first run, so please be patient!" ); + // Create ResourceFetcher implementation to be used with IndexUpdateRequest + // Here, we use Wagon based one as shorthand, but all we need is a ResourceFetcher implementation + TransferListener listener = new AbstractTransferListener() + { + public void transferStarted( TransferEvent transferEvent ) + { + System.out.print( " Downloading " + transferEvent.getResource().getName() ); + } + + public void transferProgress( TransferEvent transferEvent, byte[] buffer, int length ) + { + } + + public void transferCompleted( TransferEvent transferEvent ) + { + System.out.println( " - Done" ); + } + }; + ResourceFetcher resourceFetcher = new WagonHelper.WagonFetcher( httpWagon, listener, null, null ); + + Date centralContextCurrentTimestamp = centralContext.getTimestamp(); + IndexUpdateRequest updateRequest = new IndexUpdateRequest( centralContext, resourceFetcher ); + IndexUpdateResult updateResult = indexUpdater.fetchAndUpdateIndex( updateRequest ); + if ( updateResult.isFullUpdate() ) + { + System.out.println( "Full update happened!" ); + } + else if ( updateResult.getTimestamp().equals( centralContextCurrentTimestamp ) ) + { + System.out.println( "No update needed, index is up to date!" ); + } + else + { + System.out.println( + "Incremental update happened, change covered " + centralContextCurrentTimestamp + " - " + updateResult.getTimestamp() + " period." ); + } + System.out.println(); + + this.backend = new IndexerCoreSearchBackendImpl( indexer, centralContext ); + } + + @After + public void cleanup() throws IOException + { + indexer.closeIndexingContext( centralContext, false ); + plexusContainer.dispose(); + } + + @Test + public void smoke() throws IOException + { + SearchRequest searchRequest = new SearchRequest( query( "smoke" ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void g() throws IOException + { + SearchRequest searchRequest = new SearchRequest( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void ga() throws IOException + { + SearchRequest searchRequest = new SearchRequest( and( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ), + FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ) ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void gav() throws IOException + { + SearchRequest searchRequest = new SearchRequest( and( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ), + FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ), FieldQuery.fieldQuery( MAVEN.VERSION, "3.1.0" ) ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void sha1() throws IOException + { + SearchRequest searchRequest = new SearchRequest( + FieldQuery.fieldQuery( MAVEN.SHA1, "8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12" ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void cn() throws IOException + { + SearchRequest searchRequest = new SearchRequest( FieldQuery.fieldQuery( MAVEN.CLASS_NAME, "MavenRepositorySystem" ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void fqcn() throws IOException + { + SearchRequest searchRequest = new SearchRequest( + FieldQuery.fieldQuery( MAVEN.FQ_CLASS_NAME, "org.apache.maven.bridge.MavenRepositorySystem" ) ); + SearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } +} diff --git a/search-backend-smo/README.md b/search-backend-smo/README.md new file mode 100644 index 0000000..3431a0c --- /dev/null +++ b/search-backend-smo/README.md @@ -0,0 +1,13 @@ +Indexer Search SMO Backend +========================== + +Search API SMO (https://search.maven.org/) backend implementation. + +By default uses GSON only, so is Java8, Android and GraalVM (untested) friendly. +The default transport is java.net.HttpUrlConnection, but is pluggable. + +Examples: + +```java + SmoSearchBackendImpl backend = new SmoSearchBackendImpl(); // creates default SMO backend +``` diff --git a/search-backend-smo/header.txt b/search-backend-smo/header.txt new file mode 100644 index 0000000..1a2ef73 --- /dev/null +++ b/search-backend-smo/header.txt @@ -0,0 +1,17 @@ +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. + diff --git a/search-backend-smo/pom.xml b/search-backend-smo/pom.xml new file mode 100644 index 0000000..5a282e2 --- /dev/null +++ b/search-backend-smo/pom.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>maven-indexer</artifactId> + <version>6.1.2-SNAPSHOT</version> + </parent> + + <artifactId>search-backend-smo</artifactId> + + <name>Maven :: Search API SMO Backend</name> + <description> + Indexer Search Backend implemented by SMO. + </description> + + <dependencies> + <dependency> + <groupId>org.apache.maven.indexer</groupId> + <artifactId>search-api</artifactId> + </dependency> + + <!-- SMO --> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.9.0</version> + </dependency> + + <!-- Test --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <resources> + <resource> + <directory>src/main/filtered-resources</directory> + <filtering>true</filtering> + </resource> + </resources> + </build> + +</project> diff --git a/search-backend-smo/src/main/filtered-resources/smo-version.properties b/search-backend-smo/src/main/filtered-resources/smo-version.properties new file mode 100644 index 0000000..62e70a1 --- /dev/null +++ b/search-backend-smo/src/main/filtered-resources/smo-version.properties @@ -0,0 +1,18 @@ +#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. +# +version=${project.version} \ No newline at end of file diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java new file mode 100644 index 0000000..26b27cb --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchBackend.java @@ -0,0 +1,33 @@ +package org.apache.maven.search.backend.smo; + +/* + * 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.maven.search.SearchBackend; + +/** + * The SMO search backend. + */ +public interface SmoSearchBackend extends SearchBackend +{ + /** + * Returns the base "service URI" that is used by this SMO backend. never {@code null}. + */ + String getSmoUri(); +} diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java new file mode 100644 index 0000000..a89e7d2 --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/SmoSearchResponse.java @@ -0,0 +1,38 @@ +package org.apache.maven.search.backend.smo; + +/* + * 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.maven.search.SearchResponse; + +/** + * The SMO search response. + */ +public interface SmoSearchResponse extends SearchResponse +{ + /** + * Returns the full search URI (base + params) that was used for this search, never {@code null}. + */ + String getSearchUri(); + + /** + * Returns "raw" JSON response from SMO endpoint, never {@code null}. + */ + String getRawJsonResponse(); +} diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java new file mode 100644 index 0000000..f3df5e8 --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImpl.java @@ -0,0 +1,235 @@ +package org.apache.maven.search.backend.smo.internal; + +/* + * 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 java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import org.apache.maven.search.MAVEN; +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.backend.smo.SmoSearchBackend; +import org.apache.maven.search.backend.smo.SmoSearchResponse; +import org.apache.maven.search.request.BooleanQuery; +import org.apache.maven.search.request.Field; +import org.apache.maven.search.request.FieldQuery; +import org.apache.maven.search.request.Paging; +import org.apache.maven.search.request.Query; +import org.apache.maven.search.support.SearchBackendSupport; + +import static java.util.Objects.requireNonNull; + +public class SmoSearchBackendImpl extends SearchBackendSupport implements SmoSearchBackend +{ + public static final String DEFAULT_BACKEND_ID = "central-smo"; + + public static final String DEFAULT_REPOSITORY_ID = "central"; + + public static final String DEFAULT_SMO_URI = "https://search.maven.org/solrsearch/select"; + + private static final Map<Field, String> FIELD_TRANSLATION; + + static + { + HashMap<Field, String> map = new HashMap<>(); + map.put( MAVEN.GROUP_ID, "g" ); + map.put( MAVEN.ARTIFACT_ID, "a" ); + map.put( MAVEN.VERSION, "v" ); + map.put( MAVEN.CLASSIFIER, "l" ); + map.put( MAVEN.PACKAGING, "p" ); + map.put( MAVEN.CLASS_NAME, "c" ); + map.put( MAVEN.FQ_CLASS_NAME, "fc" ); + map.put( MAVEN.SHA1, "1" ); + FIELD_TRANSLATION = Collections.unmodifiableMap( map ); + } + + private final String smoUri; + + private final SmoSearchTransportSupport transportSupport; + + /** + * Creates a "default" instance of SMO backend against {@link #DEFAULT_SMO_URI}. + */ + public SmoSearchBackendImpl() + { + this( DEFAULT_BACKEND_ID, DEFAULT_REPOSITORY_ID, DEFAULT_SMO_URI, new UrlConnectionSmoSearchTransport() ); + } + + /** + * Creates a customized instance of SMO backend, like an in-house instances of SMO or different IDs. + */ + public SmoSearchBackendImpl( String backendId, String repositoryId, String smoUri, + SmoSearchTransportSupport transportSupport ) + { + super( backendId, repositoryId ); + this.smoUri = requireNonNull( smoUri ); + this.transportSupport = requireNonNull( transportSupport ); + } + + @Override + public String getSmoUri() + { + return smoUri; + } + + @Override + public SmoSearchResponse search( SearchRequest searchRequest ) throws IOException + { + String searchUri = toURI( searchRequest ); + String payload = transportSupport.fetch( searchRequest, searchUri ); + JsonObject raw = JsonParser.parseString( payload ).getAsJsonObject(); + List<Record> page = new ArrayList<>( searchRequest.getPaging().getPageSize() ); + int totalHits = populateFromRaw( raw, page ); + return new SmoSearchResponseImpl( searchRequest, totalHits, page, searchUri, payload ); + } + + private String toURI( SearchRequest searchRequest ) throws UnsupportedEncodingException + { + Paging paging = searchRequest.getPaging(); + HashSet<Field> searchedFields = new HashSet<>(); + String smoQuery = toSMOQuery( searchedFields, searchRequest.getQuery() ); + smoQuery += "&start=" + paging.getPageSize() * paging.getPageOffset(); + smoQuery += "&rows=" + paging.getPageSize(); + smoQuery += "&wt=json"; + if ( searchedFields.contains( MAVEN.GROUP_ID ) && searchedFields.contains( MAVEN.ARTIFACT_ID ) ) + { + smoQuery += "&core=gav"; + } + return smoUri + "?q=" + smoQuery; + } + + private String toSMOQuery( HashSet<Field> searchedFields, Query query ) throws UnsupportedEncodingException + { + if ( query instanceof BooleanQuery.And ) + { + BooleanQuery bq = (BooleanQuery) query; + return toSMOQuery( searchedFields, bq.getLeft() ) + "%20AND%20" + + toSMOQuery( searchedFields, bq.getRight() ); + } + else if ( query instanceof FieldQuery ) + { + FieldQuery fq = (FieldQuery) query; + String smoFieldName = FIELD_TRANSLATION.get( fq.getField() ); + if ( smoFieldName != null ) + { + searchedFields.add( fq.getField() ); + return smoFieldName + ":" + encodeQueryParameterValue( fq.getValue() ); + } + else + { + throw new IllegalArgumentException( "Unsupported SMO field: " + fq.getField() ); + } + } + return encodeQueryParameterValue( query.getValue() ); + } + + private String encodeQueryParameterValue( String parameterValue ) throws UnsupportedEncodingException + { + return URLEncoder.encode( parameterValue, StandardCharsets.UTF_8.name() ) + .replace( "+", "%20" ); + } + + private int populateFromRaw( JsonObject raw, List<Record> page ) + { + JsonObject response = raw.getAsJsonObject( "response" ); + Number numFound = response.get( "numFound" ).getAsNumber(); + + JsonArray docs = response.getAsJsonArray( "docs" ); + for ( JsonElement doc : docs ) + { + page.add( convert( (JsonObject) doc ) ); + } + return numFound.intValue(); + } + + private Record convert( JsonObject doc ) + { + HashMap<Field, Object> result = new HashMap<>(); + + mayPut( result, MAVEN.GROUP_ID, mayGet( "g", doc ) ); + mayPut( result, MAVEN.ARTIFACT_ID, mayGet( "a", doc ) ); + String version = mayGet( "v", doc ); + if ( version == null ) + { + version = mayGet( "latestVersion", doc ); + } + mayPut( result, MAVEN.VERSION, version ); + mayPut( result, MAVEN.PACKAGING, mayGet( "p", doc ) ); + mayPut( result, MAVEN.CLASSIFIER, mayGet( "l", doc ) ); + + // version count + Number versionCount = doc.has( "versionCount" ) ? doc.get( "versionCount" ).getAsNumber() : null; + if ( versionCount != null ) + { + mayPut( result, MAVEN.VERSION_COUNT, versionCount.intValue() ); + } + // ec + JsonArray ec = doc.getAsJsonArray( "ec" ); + if ( ec != null ) + { + result.put( MAVEN.HAS_SOURCE, ec.contains( EC_SOURCE_JAR ) ); + result.put( MAVEN.HAS_JAVADOC, ec.contains( EC_JAVADOC_JAR ) ); + // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) ); + } + + return new Record( + getBackendId(), + getRepositoryId(), + doc.has( "id" ) ? doc.get( "id" ).getAsString() : null, + doc.has( "timestamp" ) ? doc.get( "timestamp" ).getAsLong() : null, + result + ); + } + + private static final JsonPrimitive EC_SOURCE_JAR = new JsonPrimitive( "-sources.jar" ); + + private static final JsonPrimitive EC_JAVADOC_JAR = new JsonPrimitive( "-javadoc.jar" ); + + private static String mayGet( String field, JsonObject object ) + { + return object.has( field ) ? object.get( field ).getAsString() : null; + } + + private static void mayPut( Map<Field, Object> result, Field fieldName, Object value ) + { + if ( value == null ) + { + return; + } + if ( value instanceof String && ( (String) value ).trim().isEmpty() ) + { + return; + } + result.put( fieldName, value ); + } +} diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java new file mode 100644 index 0000000..3cb6f87 --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchResponseImpl.java @@ -0,0 +1,56 @@ +package org.apache.maven.search.backend.smo.internal; + +/* + * 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 java.util.List; + +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.backend.smo.SmoSearchResponse; +import org.apache.maven.search.support.SearchResponseSupport; + +import static java.util.Objects.requireNonNull; + +public class SmoSearchResponseImpl extends SearchResponseSupport implements SmoSearchResponse +{ + private final String searchUri; + + private final String rawJsonResponse; + + public SmoSearchResponseImpl( SearchRequest searchRequest, int totalHits, List<Record> page, + String searchUri, String rawJsonResponse ) + { + super( searchRequest, totalHits, page ); + this.searchUri = requireNonNull( searchUri ); + this.rawJsonResponse = requireNonNull( rawJsonResponse ); + } + + @Override + public String getSearchUri() + { + return searchUri; + } + + @Override + public String getRawJsonResponse() + { + return rawJsonResponse; + } +} diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java new file mode 100644 index 0000000..07c5402 --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/SmoSearchTransportSupport.java @@ -0,0 +1,81 @@ +package org.apache.maven.search.backend.smo.internal; + +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.apache.maven.search.SearchRequest; + +/** + * A trivial "transport abstraction" to make possible pluggable implementations. + */ +public abstract class SmoSearchTransportSupport +{ + private final String clientVersion; + + public SmoSearchTransportSupport() + { + this.clientVersion = discoverVersion(); + } + + private String discoverVersion() + { + Properties properties = new Properties(); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream( "smo-version.properties" ); + if ( inputStream != null ) + { + try ( InputStream is = inputStream ) + { + properties.load( is ); + } + catch ( IOException e ) + { + // fall through + } + } + return properties.getProperty( "version", "unknown" ); + } + + /** + * Exposes this backend version, for example to be used in HTTP {@code User-Agent} string, never {@code null}. + */ + protected String getClientVersion() + { + return clientVersion; + } + + /** + * Exposes full HTTP {@code User-Agent} string ready to be used by HTTP clients, never {@code null}. + */ + protected String getUserAgent() + { + return "Apache Search SMO/" + getClientVersion(); + } + + /** + * This method should issue a HTTP GET requests using {@code serviceUri} and return body payload as {@link String} + * ONLY if the response was HTTP 200 Ok and there was a payload returned by service. In any other case, it should + * throw, never return {@code null}. The payload is expected to be {@code application/json}, so client may add + * headers to request. Also, the payload is expected to be "relatively small". + */ + public abstract String fetch( SearchRequest searchRequest, String serviceUri ) throws IOException; +} diff --git a/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java new file mode 100644 index 0000000..a40a78f --- /dev/null +++ b/search-backend-smo/src/main/java/org/apache/maven/search/backend/smo/internal/UrlConnectionSmoSearchTransport.java @@ -0,0 +1,59 @@ +package org.apache.maven.search.backend.smo.internal; + +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +import org.apache.maven.search.SearchRequest; + +/** + * {@link java.net.HttpURLConnection} backed transport. + */ +public class UrlConnectionSmoSearchTransport extends SmoSearchTransportSupport +{ + @Override + public String fetch( SearchRequest searchRequest, String serviceUri ) throws IOException + { + HttpURLConnection httpConnection = (HttpURLConnection) new URL( serviceUri ).openConnection(); + httpConnection.setInstanceFollowRedirects( false ); + httpConnection.setRequestProperty( "User-Agent", getUserAgent() ); + httpConnection.setRequestProperty( "Accept", "application/json" ); + int httpCode = httpConnection.getResponseCode(); + if ( httpCode == HttpURLConnection.HTTP_OK ) + { + try ( InputStream inputStream = httpConnection.getInputStream() ) + { + try ( Scanner scanner = new Scanner( inputStream, StandardCharsets.UTF_8.name() ) ) + { + return scanner.useDelimiter( "\\A" ).next(); + } + } + } + else + { + throw new IOException( "Unexpected response code: " + httpCode ); + } + } +} diff --git a/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java b/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java new file mode 100644 index 0000000..4b29f15 --- /dev/null +++ b/search-backend-smo/src/test/java/org/apache/maven/search/backend/smo/internal/SmoSearchBackendImplTest.java @@ -0,0 +1,172 @@ +package org.apache.maven.search.backend.smo.internal; + +/* + * 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 java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.maven.search.MAVEN; +import org.apache.maven.search.Record; +import org.apache.maven.search.SearchRequest; +import org.apache.maven.search.backend.smo.SmoSearchResponse; +import org.apache.maven.search.request.BooleanQuery; +import org.apache.maven.search.request.FieldQuery; +import org.apache.maven.search.request.Query; +import org.junit.Ignore; +import org.junit.Test; + +@Ignore( "This is more a showcase" ) +public class SmoSearchBackendImplTest +{ + private final SmoSearchBackendImpl backend = new SmoSearchBackendImpl(); + + private void dumpSingle( AtomicInteger counter, List<Record> page ) + { + for ( Record record : page ) + { + StringBuilder sb = new StringBuilder(); + sb.append( record.getValue( MAVEN.GROUP_ID ) ).append( ":" ).append( record.getValue( MAVEN.ARTIFACT_ID ) ) + .append( ":" ).append( record.getValue( MAVEN.VERSION ) ); + if ( record.hasField( MAVEN.PACKAGING ) ) + { + if ( record.hasField( MAVEN.CLASSIFIER ) ) + { + sb.append( ":" ).append( record.getValue( MAVEN.CLASSIFIER ) ); + } + sb.append( ":" ).append( record.getValue( MAVEN.PACKAGING ) ); + } + + List<String> remarks = new ArrayList<>(); + if ( record.getLastUpdated() != null ) + { + remarks.add( "lastUpdate=" + Instant.ofEpochMilli( record.getLastUpdated() ) ); + } + if ( record.hasField( MAVEN.VERSION_COUNT ) ) + { + remarks.add( "versionCount=" + record.getValue( MAVEN.VERSION_COUNT ) ); + } + if ( record.hasField( MAVEN.HAS_SOURCE ) ) + { + remarks.add( "hasSource=" + record.getValue( MAVEN.HAS_SOURCE ) ); + } + if ( record.hasField( MAVEN.HAS_JAVADOC ) ) + { + remarks.add( "hasJavadoc=" + record.getValue( MAVEN.HAS_JAVADOC ) ); + } + + System.out.print( counter.incrementAndGet() + ". " + sb ); + if ( !remarks.isEmpty() ) + { + System.out.print( " " + remarks ); + } + System.out.println(); + } + } + + private void dumpPage( SmoSearchResponse searchResponse ) throws IOException + { + AtomicInteger counter = new AtomicInteger( 0 ); + System.out.println( "QUERY: " + searchResponse.getSearchRequest().getQuery().toString() ); + System.out.println( "URL: " + searchResponse.getSearchUri() ); + dumpSingle( counter, searchResponse.getPage() ); + while ( searchResponse.getTotalHits() > searchResponse.getCurrentHits() ) + { + System.out.println( "NEXT PAGE (size " + searchResponse.getSearchRequest().getPaging().getPageSize() + ")" ); + searchResponse = backend.search( searchResponse.getSearchRequest().nextPage() ); + dumpSingle( counter, searchResponse.getPage() ); + if ( counter.get() > 50 ) + { + System.out.println( "ABORTED TO NOT SPAM" ); + break; // do not spam the SMO service + } + } + System.out.println(); + } + + @Test + public void smoke() throws IOException + { + SearchRequest searchRequest = new SearchRequest( Query.query( "smoke" ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void g() throws IOException + { + SearchRequest searchRequest = new SearchRequest( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void ga() throws IOException + { + SearchRequest searchRequest = new SearchRequest( BooleanQuery.and( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ), + FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ) ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void gav() throws IOException + { + SearchRequest searchRequest = new SearchRequest( BooleanQuery.and( FieldQuery.fieldQuery( MAVEN.GROUP_ID, "org.apache.maven.plugins" ), + FieldQuery.fieldQuery( MAVEN.ARTIFACT_ID, "maven-clean-plugin" ), FieldQuery.fieldQuery( MAVEN.VERSION, "3.1.0" ) ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void sha1() throws IOException + { + SearchRequest searchRequest = new SearchRequest( + FieldQuery.fieldQuery( MAVEN.SHA1, "8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12" ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void cn() throws IOException + { + SearchRequest searchRequest = new SearchRequest( FieldQuery.fieldQuery( MAVEN.CLASS_NAME, "MavenRepositorySystem" ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } + + @Test + public void fqcn() throws IOException + { + SearchRequest searchRequest = new SearchRequest( + FieldQuery.fieldQuery( MAVEN.FQ_CLASS_NAME, "org.apache.maven.bridge.MavenRepositorySystem" ) ); + SmoSearchResponse searchResponse = backend.search( searchRequest ); + System.out.println( "TOTAL HITS: " + searchResponse.getTotalHits() ); + dumpPage( searchResponse ); + } +}