Repository: maven-indexer Updated Branches: refs/heads/maven-indexer-5.x c19388828 -> af8783d8f
MINDEXER-96: Indexer reader Project: http://git-wip-us.apache.org/repos/asf/maven-indexer/repo Commit: http://git-wip-us.apache.org/repos/asf/maven-indexer/commit/af8783d8 Tree: http://git-wip-us.apache.org/repos/asf/maven-indexer/tree/af8783d8 Diff: http://git-wip-us.apache.org/repos/asf/maven-indexer/diff/af8783d8 Branch: refs/heads/maven-indexer-5.x Commit: af8783d8fc8dcab6ef6d9a17d04858fd725037a0 Parents: c193888 Author: Tamas Cservenak <[email protected]> Authored: Sat Oct 31 01:29:30 2015 +0100 Committer: Tamas Cservenak <[email protected]> Committed: Sat Oct 31 01:29:30 2015 +0100 ---------------------------------------------------------------------- indexer-reader/README.md | 8 + indexer-reader/header.txt | 17 + indexer-reader/pom.xml | 48 ++ .../apache/maven/index/reader/ChunkReader.java | 454 +++++++++++++++++++ .../apache/maven/index/reader/IndexReader.java | 281 ++++++++++++ .../org/apache/maven/index/reader/Record.java | 247 ++++++++++ .../maven/index/reader/ResourceHandler.java | 45 ++ .../index/reader/WritableResourceHandler.java | 44 ++ .../index/reader/CachingResourceHandler.java | 60 +++ .../maven/index/reader/ChunkReaderTest.java | 66 +++ .../index/reader/DirectoryResourceHandler.java | 72 +++ .../maven/index/reader/HttpResourceHandler.java | 57 +++ .../maven/index/reader/IndexReaderTest.java | 102 +++++ .../resources/nexus-maven-repository-index.gz | Bin 0 -> 319 bytes .../nexus-maven-repository-index.properties | 6 + pom.xml | 1 + 16 files changed, 1508 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/README.md ---------------------------------------------------------------------- diff --git a/indexer-reader/README.md b/indexer-reader/README.md new file mode 100644 index 0000000..95aa1b8 --- /dev/null +++ b/indexer-reader/README.md @@ -0,0 +1,8 @@ +Indexer Reader Notes +================== + +Indexer Reader is a minimal dep-less library that is able to read published (remote) +index with incremental update support, making this library user able to integrate +published Maven Indexes into any engine without depending on maven-indexer-core +and it's transitive dependencies. + http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/header.txt ---------------------------------------------------------------------- diff --git a/indexer-reader/header.txt b/indexer-reader/header.txt new file mode 100644 index 0000000..1a2ef73 --- /dev/null +++ b/indexer-reader/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. + http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/pom.xml ---------------------------------------------------------------------- diff --git a/indexer-reader/pom.xml b/indexer-reader/pom.xml new file mode 100644 index 0000000..daf364c --- /dev/null +++ b/indexer-reader/pom.xml @@ -0,0 +1,48 @@ +<?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>5.1.2-SNAPSHOT</version> + </parent> + + <artifactId>indexer-reader</artifactId> + + <name>Maven :: Indexer Reader</name> + <description> + Indexer Reader is a minimal dep-less library that is able to read published (remote) index with incremental update + support, making user able to integrate published Maven Indexes into any engine without depending on + maven-indexer-core and it's transitive dependencies. + </description> + + <dependencies> + <!-- Test --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/main/java/org/apache/maven/index/reader/ChunkReader.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/main/java/org/apache/maven/index/reader/ChunkReader.java b/indexer-reader/src/main/java/org/apache/maven/index/reader/ChunkReader.java new file mode 100644 index 0000000..89434fd --- /dev/null +++ b/indexer-reader/src/main/java/org/apache/maven/index/reader/ChunkReader.java @@ -0,0 +1,454 @@ +package org.apache.maven.index.reader; + +/* + * 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; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UTFDataFormatException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +import org.apache.maven.index.reader.Record.Type; + +/** + * Maven 2 Index published binary chunk reader. + * + * @since 5.1.2 + */ +public class ChunkReader + implements Closeable, Iterable<Record> +{ + private static final String FIELD_SEPARATOR = "|"; + + private static final String NOT_AVAILABLE = "NA"; + + private static final String UINFO = "u"; + + private static final String INFO = "i"; + + private static final Pattern FS_PATTERN = Pattern.compile(Pattern.quote(FIELD_SEPARATOR)); + + private final String chunkName; + + private final DataInputStream dataInputStream; + + private final int version; + + private final Date timestamp; + + public ChunkReader(final String chunkName, final InputStream inputStream) throws IOException + { + this.chunkName = chunkName.trim(); + this.dataInputStream = new DataInputStream(new GZIPInputStream(inputStream, 2 * 1024)); + this.version = ((int) dataInputStream.readByte()) & 0xff; + this.timestamp = new Date(dataInputStream.readLong()); + } + + /** + * Returns the chunk name. + */ + public String getName() { + return chunkName; + } + + /** + * Returns index getVersion. All releases so far always returned {@code 1}. + */ + public int getVersion() { + return version; + } + + /** + * Returns the getTimestamp of last update of the index. + */ + public Date getTimestamp() { + return timestamp; + } + + /** + * Returns the {@link Record} iterator. + */ + public Iterator<Record> iterator() { + try { + return new IndexIterator(dataInputStream); + } + catch (IOException e) { + throw new RuntimeException("error", e); + } + } + + /** + * Closes this reader and it's underlying input. + */ + public void close() throws IOException { + dataInputStream.close(); + } + + /** + * Low memory footprint index iterator that incrementally parses the underlying stream. + */ + private static class IndexIterator + implements Iterator<Record> + { + private final DataInputStream dataInputStream; + + private Record nextRecord; + + public IndexIterator(final DataInputStream dataInputStream) throws IOException { + this.dataInputStream = dataInputStream; + this.nextRecord = readRecord(); + } + + public boolean hasNext() { + return nextRecord != null; + } + + public Record next() { + if (nextRecord == null) { + throw new NoSuchElementException("chunk depleted"); + } + Record result = nextRecord; + try { + nextRecord = readRecord(); + return result; + } + catch (IOException e) { + throw new RuntimeException("read error", e); + } + } + + /** + * Reads and returns next record from the underlying stream, or {@code null} if no more records. + */ + private Record readRecord() + throws IOException + { + int fieldCount; + try { + fieldCount = dataInputStream.readInt(); + } + catch (EOFException ex) { + return null; // no more documents + } + + Map<String, String> recordMap = new HashMap<String, String>(); + for (int i = 0; i < fieldCount; i++) { + readField(recordMap); + } + + if (recordMap.containsKey("DESCRIPTOR")) { + return new Record(Type.DESCRIPTOR, recordMap, expandDescriptor(recordMap)); + } + else if (recordMap.containsKey("allGroups")) { + return new Record(Type.ALL_GROUPS, recordMap, expandAllGroups(recordMap)); + } + else if (recordMap.containsKey("rootGroups")) { + return new Record(Type.ROOT_GROUPS, recordMap, expandRootGroups(recordMap)); + } + else if (recordMap.containsKey("del")) { + return new Record(Type.ARTIFACT_REMOVE, recordMap, expandDeletedArtifact(recordMap)); + } + else { + // Fix up UINFO field wrt MINDEXER-41 + final String uinfo = recordMap.get(UINFO); + final String info = recordMap.get(INFO); + if (uinfo != null && !(info == null || info.trim().length() == 0)) { + final String[] splitInfo = FS_PATTERN.split(info); + if (splitInfo.length > 6) { + final String extension = splitInfo[6]; + if (uinfo.endsWith(FIELD_SEPARATOR + NOT_AVAILABLE)) { + recordMap.put(UINFO, uinfo + FIELD_SEPARATOR + extension); + } + } + } + return new Record(Type.ARTIFACT_ADD, recordMap, expandAddedArtifact(recordMap)); + } + } + + private void readField(final Map<String, String> record) + throws IOException + { + dataInputStream.read(); // flags: neglect them + String name = dataInputStream.readUTF(); + String value = readUTF(); + record.put(name, value); + } + + private String readUTF() + throws IOException + { + int utflen = dataInputStream.readInt(); + + byte[] bytearr; + char[] chararr; + + try { + bytearr = new byte[utflen]; + chararr = new char[utflen]; + } + catch (OutOfMemoryError e) { + IOException ioex = new IOException("Index data content is corrupt"); + ioex.initCause(e); + throw ioex; + } + + int c, char2, char3; + int count = 0; + int chararr_count = 0; + + dataInputStream.readFully(bytearr, 0, utflen); + + while (count < utflen) { + c = bytearr[count] & 0xff; + if (c > 127) { + break; + } + count++; + chararr[chararr_count++] = (char) c; + } + + while (count < utflen) { + c = bytearr[count] & 0xff; + switch (c >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + /* 0xxxxxxx */ + count++; + chararr[chararr_count++] = (char) c; + break; + + case 12: + case 13: + /* 110x xxxx 10xx xxxx */ + count += 2; + if (count > utflen) { + throw new UTFDataFormatException("malformed input: partial character at end"); + } + char2 = bytearr[count - 1]; + if ((char2 & 0xC0) != 0x80) { + throw new UTFDataFormatException("malformed input around byte " + count); + } + chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + + case 14: + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + count += 3; + if (count > utflen) { + throw new UTFDataFormatException("malformed input: partial character at end"); + } + char2 = bytearr[count - 2]; + char3 = bytearr[count - 1]; + if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80)) { + throw new UTFDataFormatException("malformed input around byte " + (count - 1)); + } + chararr[chararr_count++] = + (char) (((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | (char3 & 0x3F)); + break; + + default: + /* 10xx xxxx, 1111 xxxx */ + throw new UTFDataFormatException("malformed input around byte " + count); + } + } + + // The number of chars produced may be less than utflen + return new String(chararr, 0, chararr_count); + } + + private Map<String, Object> expandDescriptor(final Map<String, String> raw) { + final Map<String, Object> result = new HashMap<String, Object>(); + String[] r = FS_PATTERN.split(raw.get("IDXINFO")); + result.put(Record.REPOSITORY_ID, r[1]); + return result; + } + + private Map<String, Object> expandAllGroups(final Map<String, String> raw) { + final Map<String, Object> result = new HashMap<String, Object>(); + putIfNotNullAsList(raw, Record.ALL_GROUPS_LIST, result, "allGroups"); + return result; + } + + private Map<String, Object> expandRootGroups(final Map<String, String> raw) { + final Map<String, Object> result = new HashMap<String, Object>(); + putIfNotNullAsList(raw, Record.ROOT_GROUPS_LIST, result, "rootGroups"); + return result; + } + + private Map<String, Object> expandDeletedArtifact(final Map<String, String> raw) { + final Map<String, Object> result = new HashMap<String, Object>(); + putIfNotNullTS(raw, "m", result, Record.REC_MODIFIED); + if (raw.containsKey("del")) { + expandUinfo(raw.get("del"), result); + } + return result; + } + + /** + * Expands the "encoded" Maven Indexer record by splitting the synthetic fields and applying expanded field naming. + */ + private Map<String, Object> expandAddedArtifact(final Map<String, String> raw) { + final Map<String, Object> result = new HashMap<String, Object>(); + + // Minimal + expandUinfo(raw.get(UINFO), result); + final String info = raw.get(INFO); + if (info != null) { + String[] r = FS_PATTERN.split(info); + result.put(Record.PACKAGING, renvl(r[0])); + result.put(Record.FILE_MODIFIED, Long.valueOf(r[1])); + result.put(Record.FILE_SIZE, Long.valueOf(r[2])); + result.put(Record.HAS_SOURCES, "1".equals(r[3]) ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); + result.put(Record.HAS_JAVADOC, "1".equals(r[4]) ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); + result.put(Record.HAS_SIGNATURE, "1".equals(r[5]) ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); + if (r.length > 6) { + result.put(Record.FILE_EXTENSION, r[6]); + } + else { + final String packaging = raw.get(Record.PACKAGING); + if (raw.get(Record.CLASSIFIER) != null + || "pom".equals(packaging) + || "war".equals(packaging) + || "ear".equals(packaging)) { + result.put(Record.FILE_EXTENSION, packaging); + } + else { + result.put(Record.FILE_EXTENSION, "jar"); // best guess + } + } + } + putIfNotNullTS(raw, "m", result, Record.REC_MODIFIED); + putIfNotNull(raw, "n", result, Record.NAME); + putIfNotNull(raw, "d", result, Record.DESCRIPTION); + putIfNotNull(raw, "1", result, Record.SHA1); + + // Jar file contents (optional) + putIfNotNullAsList(raw, "classnames", result, Record.CLASSNAMES); + + // Maven Plugin (optional) + putIfNotNull(raw, "px", result, Record.PLUGIN_PREFIX); + putIfNotNullAsList(raw, "gx", result, Record.PLUGIN_GOALS); + + // OSGi (optional) + putIfNotNull(raw, "Bundle-SymbolicName", result, "Bundle-SymbolicName"); + putIfNotNull(raw, "Bundle-Version", result, "Bundle-Version"); + putIfNotNull(raw, "Export-Package", result, "Export-Package"); + putIfNotNull(raw, "Export-Service", result, "Export-Service"); + putIfNotNull(raw, "Bundle-Description", result, "Bundle-Description"); + putIfNotNull(raw, "Bundle-Name", result, "Bundle-Name"); + putIfNotNull(raw, "Bundle-License", result, "Bundle-License"); + putIfNotNull(raw, "Bundle-DocURL", result, "Bundle-DocURL"); + putIfNotNull(raw, "Import-Package", result, "Import-Package"); + putIfNotNull(raw, "Require-Bundle", result, "Require-Bundle"); + putIfNotNull(raw, "Bundle-Version", result, "Bundle-Version"); + + return result; + } + + /** + * Expands UINFO synthetic field. Handles {@code null} String inputs. + */ + private void expandUinfo(final String uinfo, final Map<String, Object> result) { + if (uinfo != null) { + String[] r = FS_PATTERN.split(uinfo); + result.put(Record.GROUP_ID, r[0]); + result.put(Record.ARTIFACT_ID, r[1]); + result.put(Record.VERSION, r[2]); + String classifier = renvl(r[3]); + if (classifier != null) { + result.put(Record.CLASSIFIER, classifier); + if (r.length > 4) { + result.put(Record.FILE_EXTENSION, r[4]); + } + } + else if (r.length > 4) { + result.put(Record.PACKAGING, r[4]); + } + } + } + } + + /** + * Helper to put a value from source map into target map, if not null. + */ + private static void putIfNotNull( + final Map<String, String> source, + final String sourceName, + final Map<String, Object> target, + final String targetName) + { + String value = source.get(sourceName); + if (value != null && value.trim().length() != 0) { + target.put(targetName, value); + } + } + + /** + * Helper to put a {@link Long} value from source map into target map, if not null. + */ + private static void putIfNotNullTS( + final Map<String, String> source, + final String sourceName, + final Map<String, Object> target, + final String targetName) + { + String value = source.get(sourceName); + if (value != null && value.trim().length() != 0) { + target.put(targetName, Long.valueOf(value)); + } + } + + /** + * Helper to put a collection value from source map into target map as {@link java.util.List}, if not null. + */ + private static void putIfNotNullAsList( + final Map<String, String> source, + final String sourceName, + final Map<String, Object> target, + final String targetName) + { + String value = source.get(sourceName); + if (value != null && value.trim().length() != 0) { + target.put(targetName, Arrays.asList(FS_PATTERN.split(value))); + } + } + + /** + * Helper to translate the "NA" (not available) input into {@code null} value. + */ + private static String renvl(final String v) { + return NOT_AVAILABLE.equals(v) ? null : v; + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/main/java/org/apache/maven/index/reader/IndexReader.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/main/java/org/apache/maven/index/reader/IndexReader.java b/indexer-reader/src/main/java/org/apache/maven/index/reader/IndexReader.java new file mode 100644 index 0000000..45514b9 --- /dev/null +++ b/indexer-reader/src/main/java/org/apache/maven/index/reader/IndexReader.java @@ -0,0 +1,281 @@ +package org.apache.maven.index.reader; + +/* + * 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.TimeZone; + +/** + * Maven 2 Index reader that handles incremental updates if possible. + * + * @since 5.1.2 + */ +public class IndexReader + implements Iterable<ChunkReader>, Closeable +{ + private static final String INDEX_FILE_PREFIX = "nexus-maven-repository-index"; + + private static final DateFormat INDEX_DATE_FORMAT; + + static { + INDEX_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss.SSS Z"); + INDEX_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + private final WritableResourceHandler local; + + private final ResourceHandler remote; + + private final Properties localIndexProperties; + + private final Properties remoteIndexProperties; + + private final String indexId; + + private final Date publishedTimestamp; + + private final boolean incremental; + + private final List<String> chunkNames; + + public IndexReader(final WritableResourceHandler local, final ResourceHandler remote) throws IOException { + if (remote == null) { + throw new NullPointerException("remote resource handler null"); + } + this.local = local; + this.remote = remote; + remoteIndexProperties = loadProperties(remote.open(INDEX_FILE_PREFIX + ".properties")); + try { + if (local != null) { + localIndexProperties = loadProperties(local.open(INDEX_FILE_PREFIX + ".properties")); + String remoteIndexId = remoteIndexProperties.getProperty("nexus.index.id"); + String localIndexId = localIndexProperties.getProperty("nexus.index.id"); + if (remoteIndexId == null || localIndexId == null || !remoteIndexId.equals(localIndexId)) { + throw new IllegalArgumentException( + "local and remote index IDs does not match or is null: " + localIndexId + ", " + + remoteIndexId); + } + this.indexId = localIndexId; + this.publishedTimestamp = INDEX_DATE_FORMAT.parse(localIndexProperties.getProperty("nexus.index.timestamp")); + this.incremental = canRetrieveAllChunks(); + this.chunkNames = calculateChunkNames(); + } + else { + localIndexProperties = null; + this.indexId = remoteIndexProperties.getProperty("nexus.index.id"); + this.publishedTimestamp = INDEX_DATE_FORMAT.parse(remoteIndexProperties.getProperty("nexus.index.timestamp")); + this.incremental = false; + this.chunkNames = calculateChunkNames(); + } + } + catch (ParseException e) { + IOException ex = new IOException("Index properties corrupted"); + ex.initCause(e); + throw ex; + } + } + + /** + * Returns the index context ID that published index has set. Usually it is equal to "repository ID" used in {@link + * Record.Type#DESCRIPTOR} but does not have to be. + */ + public String getIndexId() { + return indexId; + } + + /** + * Returns the {@link Date} when remote index was last published. + */ + public Date getPublishedTimestamp() { + return publishedTimestamp; + } + + /** + * Returns {@code true} if incremental update is about to happen. If incremental update, the {@link #iterator()} will + * return only the diff from the last update. + */ + public boolean isIncremental() { + return incremental; + } + + /** + * Returns unmodifiable list of actual chunks that needs to be pulled from remote {@link ResourceHandler}. Those are + * incremental chunks or the big main file, depending on result of {@link #isIncremental()}. Empty list means local + * index is up to date, and {@link #iterator()} will return empty iterator. + */ + public List<String> getChunkNames() { + return chunkNames; + } + + /** + * Closes the underlying {@link ResourceHandler}s. In case of incremental update use, it also assumes that user + * consumed all the iterator and integrated it, hence, it will update the {@link WritableResourceHandler} contents to + * prepare it for future incremental update. If this is not desired (ie. due to aborted update), then this method + * should NOT be invoked, but rather the {@link ResourceHandler}s that caller provided in constructor of + * this class should be closed manually. + */ + public void close() throws IOException { + remote.close(); + if (local != null) { + try { + syncLocalWithRemote(); + } + finally { + local.close(); + } + } + } + + /** + * Returns an {@link Iterator} of {@link ChunkReader}s, that if read in sequence, provide all the (incremental) + * updates from the index. It is caller responsibility to either consume fully this iterator, or to close current + * {@link ChunkReader} if aborting. + */ + public Iterator<ChunkReader> iterator() { + return new ChunkReaderIterator(remote, chunkNames.iterator()); + } + + /** + * Stores the remote index properties into local index properties, preparing local {@link WritableResourceHandler} + * for future incremental updates. + */ + private void syncLocalWithRemote() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + remoteIndexProperties.store(bos, "Maven Indexer Reader"); + local.save(INDEX_FILE_PREFIX + ".properties", new ByteArrayInputStream(bos.toByteArray())); + } + + /** + * Calculates the chunk names that needs to be fetched. + */ + private List<String> calculateChunkNames() { + if (incremental) { + ArrayList<String> chunkNames = new ArrayList<String>(); + int maxCounter = Integer.parseInt(remoteIndexProperties.getProperty("nexus.index.last-incremental")); + int currentCounter = Integer.parseInt(localIndexProperties.getProperty("nexus.index.last-incremental")); + currentCounter++; + while (currentCounter <= maxCounter) { + chunkNames.add(INDEX_FILE_PREFIX + "." + currentCounter++ + ".gz"); + } + return Collections.unmodifiableList(chunkNames); + } + else { + return Collections.singletonList(INDEX_FILE_PREFIX + ".gz"); + } + } + + /** + * Verifies incremental update is possible, as all the diff chunks we need are still enlisted in remote properties. + */ + private boolean canRetrieveAllChunks() + { + String localChainId = localIndexProperties.getProperty("nexus.index.chain-id"); + String remoteChainId = remoteIndexProperties.getProperty("nexus.index.chain-id"); + + // If no chain id, or not the same, do full update + if (localChainId == null || remoteChainId == null || !localChainId.equals(remoteChainId)) { + return false; + } + + try { + int localLastIncremental = Integer.parseInt(localIndexProperties.getProperty("nexus.index.last-incremental")); + String currentLocalCounter = String.valueOf(localLastIncremental); + String nextLocalCounter = String.valueOf(localLastIncremental + 1); + // check remote props for existence of current or next chunk after local + for (Object key : remoteIndexProperties.keySet()) { + String sKey = (String) key; + if (sKey.startsWith("nexus.index.incremental-")) { + String value = remoteIndexProperties.getProperty(sKey); + if (currentLocalCounter.equals(value) || nextLocalCounter.equals(value)) { + return true; + } + } + } + } + catch (NumberFormatException e) { + // fall through + } + return false; + } + + /** + * Internal iterator implementation that lazily opens and closes the returned {@link ChunkReader}s as this iterator + * is being consumed. + */ + private static class ChunkReaderIterator + implements Iterator<ChunkReader> + { + private final ResourceHandler resourceHandler; + + private final Iterator<String> chunkNamesIterator; + + private ChunkReader currentChunkReader; + + private ChunkReaderIterator(final ResourceHandler resourceHandler, final Iterator<String> chunkNamesIterator) { + this.resourceHandler = resourceHandler; + this.chunkNamesIterator = chunkNamesIterator; + } + + public boolean hasNext() { + return chunkNamesIterator.hasNext(); + } + + public ChunkReader next() { + String chunkName = chunkNamesIterator.next(); + try { + if (currentChunkReader != null) { + currentChunkReader.close(); + } + currentChunkReader = new ChunkReader(chunkName, resourceHandler.open(chunkName)); + return currentChunkReader; + } + catch (IOException e) { + throw new RuntimeException("IO problem while switching chunk readers", e); + } + } + } + + /** + * Creates and loads {@link Properties} from provided {@link InputStream} and closes the stream. + */ + private static Properties loadProperties(final InputStream inputStream) throws IOException { + try { + final Properties properties = new Properties(); + properties.load(inputStream); + return properties; + } + finally { + inputStream.close(); + } + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/main/java/org/apache/maven/index/reader/Record.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/main/java/org/apache/maven/index/reader/Record.java b/indexer-reader/src/main/java/org/apache/maven/index/reader/Record.java new file mode 100644 index 0000000..3354008 --- /dev/null +++ b/indexer-reader/src/main/java/org/apache/maven/index/reader/Record.java @@ -0,0 +1,247 @@ +package org.apache.maven.index.reader; + +/* + * 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; + +/** + * Maven 2 Index record. + * + * @since 5.1.2 + */ +public class Record +{ + /** + * Key of repository ID entry, that contains {@link String}. + */ + public static final String REPOSITORY_ID = "repositoryId"; + + /** + * Key of all groups list entry, that contains {@link java.util.List<String>}. + */ + public static final String ALL_GROUPS_LIST = "allGroupsList"; + + /** + * Key of root groups list entry, that contains {@link java.util.List<String>}. + */ + public static final String ROOT_GROUPS_LIST = "rootGroupsList"; + + /** + * Key of index record modification (added to index or removed from index) timestamp entry, that contains {@link + * Long}. + */ + public static final String REC_MODIFIED = "recordModified"; + + /** + * Key of artifact groupId entry, that contains {@link String}. + */ + public static final String GROUP_ID = "groupId"; + + /** + * Key of artifact artifactId entry, that contains {@link String}. + */ + public static final String ARTIFACT_ID = "artifactId"; + + /** + * Key of artifact version entry, that contains {@link String}. + */ + public static final String VERSION = "version"; + + /** + * Key of artifact classifier entry, that contains {@link String}. + */ + public static final String CLASSIFIER = "classifier"; + + /** + * Key of artifact packaging entry, that contains {@link String}. + */ + public static final String PACKAGING = "packaging"; + + /** + * Key of artifact file extension, that contains {@link String}. + */ + public static final String FILE_EXTENSION = "fileExtension"; + + /** + * Key of artifact file last modified timestamp, that contains {@link Long}. + */ + public static final String FILE_MODIFIED = "fileModified"; + + /** + * Key of artifact file size in bytes, that contains {@link Long}. + */ + public static final String FILE_SIZE = "fileSize"; + + /** + * Key of artifact Sources presence flag, that contains {@link Boolean}. + */ + public static final String HAS_SOURCES = "hasSources"; + + /** + * Key of artifact Javadoc presence flag, that contains {@link Boolean}. + */ + public static final String HAS_JAVADOC = "hasJavadoc"; + + /** + * Key of artifact signature presence flag, that contains {@link Boolean}. + */ + public static final String HAS_SIGNATURE = "hasSignature"; + + /** + * Key of artifact name (as set in POM), that contains {@link String}. + */ + public static final String NAME = "name"; + + /** + * Key of artifact description (as set in POM), that contains {@link String}. + */ + public static final String DESCRIPTION = "description"; + + /** + * Key of artifact SHA1 digest, that contains {@link String}. + */ + public static final String SHA1 = "sha1"; + + /** + * Key of artifact contained class names, that contains {@link java.util.List<String>}. + */ + public static final String CLASSNAMES = "classNames"; + + /** + * Key of plugin artifact prefix, that contains {@link String}. + */ + public static final String PLUGIN_PREFIX = "pluginPrefix"; + + /** + * Key of plugin artifact goals, that contains {@link java.util.List<String>}. + */ + public static final String PLUGIN_GOALS = "pluginGoals"; + + /** + * Types of returned records returned from index. + */ + public enum Type + { + /** + * Descriptor record. Can be safely ignored. + * Contains following entries: + * <ul> + * <li>{@link #REPOSITORY_ID}</li> + * </ul> + */ + DESCRIPTOR, + + /** + * Artifact ADD record. Records of this type should be added to your indexing system. + * Contains following entries: + * <ul> + * <li>{@link #REC_MODIFIED} (when record was added/modified on index)</li> + * <li>{@link #GROUP_ID}</li> + * <li>{@link #ARTIFACT_ID}</li> + * <li>{@link #VERSION}</li> + * <li>{@link #CLASSIFIER} (optional)</li> + * <li>{@link #FILE_EXTENSION}</li> + * <li>{@link #FILE_MODIFIED}</li> + * <li>{@link #FILE_SIZE}</li> + * <li>{@link #PACKAGING}</li> + * <li>{@link #HAS_SOURCES}</li> + * <li>{@link #HAS_JAVADOC}</li> + * <li>{@link #HAS_SIGNATURE}</li> + * <li>{@link #NAME}</li> + * <li>{@link #DESCRIPTION}</li> + * <li>{@link #SHA1}</li> + * <li>{@link #CLASSNAMES} (optional)</li> + * <li>{@link #PLUGIN_PREFIX} (optional, for maven-plugins only)</li> + * <li>{@link #PLUGIN_GOALS} (optional, for maven-plugins only)</li> + * </ul> + */ + ARTIFACT_ADD, + + /** + * Artifact REMOTE record. In case of incremental updates, notes that this artifact was removed. Records of this + * type should be removed from your indexing system. + * Contains following entries: + * <ul> + * <li>{@link #REC_MODIFIED} (when record was deleted from index)</li> + * <li>{@link #GROUP_ID}</li> + * <li>{@link #ARTIFACT_ID}</li> + * <li>{@link #VERSION}</li> + * <li>{@link #CLASSIFIER} (optional)</li> + * <li>{@link #FILE_EXTENSION} (if {@link #CLASSIFIER} present)</li> + * <li>{@link #PACKAGING} (optional)</li> + * </ul> + */ + ARTIFACT_REMOVE, + + /** + * Special record, containing all the Maven "groupId"s that are enlisted on the index. Can be safely ignored. + * Contains following entries: + * <ul> + * <li>{@link #ALL_GROUPS_LIST}</li> + * </ul> + */ + ALL_GROUPS, + + /** + * Special record, containing all the root groups of Maven "groupId"s that are enlisted on the index. Can be safely + * ignored. + * Contains following entries: + * <ul> + * <li>{@link #ROOT_GROUPS_LIST}</li> + * </ul> + */ + ROOT_GROUPS + } + + private final Type type; + + private final Map<String, String> raw; + + private final Map<String, Object> expanded; + + public Record(final Type type, final Map<String, String> raw, final Map<String, Object> expanded) { + this.type = type; + this.raw = raw; + this.expanded = expanded; + } + + /** + * Returns the {@link Type} of this record. Usually users would be interested in {@link Type#ARTIFACT_ADD} and {@link + * Type#ARTIFACT_REMOVE} types only to maintain their own index. Still, indexer offers extra records too, see {@link + * Type} for all existing types. + */ + public Type getType() { + return type; + } + + /** + * Returns the "raw", Maven Indexer specific record as a {@link Map}. + */ + public Map<String, String> getRaw() { + return raw; + } + + /** + * Returns the expanded (processed and expanded synthetic fields) record as {@link Map} ready for consumption. + */ + public Map<String, Object> getExpanded() { + return expanded; + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/main/java/org/apache/maven/index/reader/ResourceHandler.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/main/java/org/apache/maven/index/reader/ResourceHandler.java b/indexer-reader/src/main/java/org/apache/maven/index/reader/ResourceHandler.java new file mode 100644 index 0000000..9780067 --- /dev/null +++ b/indexer-reader/src/main/java/org/apache/maven/index/reader/ResourceHandler.java @@ -0,0 +1,45 @@ +package org.apache.maven.index.reader; + +/* + * 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; +import java.io.IOException; +import java.io.InputStream; + +/** + * Maven 2 Index resource abstraction, that should be handled as a resource (is {@link Closeable}. That means, that + * implementations could perform any extra activity as FS locking or so (if uses FS as backing store). If the + * implementation plans to fetch from remote, it could implement very simple "cache" mechanism, to fetch only once + * during the lifespan of the instance, as for indexer there is no reason to re-fetch during single session, nor + * yo have any advanced caching (ie. TTLs etc). + * + * @since 5.1.2 + */ +public interface ResourceHandler + extends Closeable +{ + /** + * Returns the {@link InputStream} of resource with {@code name} or {@code null} if no such resource. Closing the + * stream is the responsibility of the caller. + * + * @param name Resource name, guaranteed to be non-{@code null} and is FS name and URL safe string. + */ + InputStream open(String name) throws IOException; +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/main/java/org/apache/maven/index/reader/WritableResourceHandler.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/main/java/org/apache/maven/index/reader/WritableResourceHandler.java b/indexer-reader/src/main/java/org/apache/maven/index/reader/WritableResourceHandler.java new file mode 100644 index 0000000..7fe3896 --- /dev/null +++ b/indexer-reader/src/main/java/org/apache/maven/index/reader/WritableResourceHandler.java @@ -0,0 +1,44 @@ +package org.apache.maven.index.reader; + +/* + * 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; + +/** + * Maven 2 Index writable {@link ResourceHandler}, is capable of saving resources too. Needed only if incremental index + * updates are wanted, to store the index state locally, and be able to calculate incremental diffs on next {@link + * IndexReader} invocation. + * + * @see ResourceHandler + * @since 5.1.2 + */ +public interface WritableResourceHandler + extends ResourceHandler +{ + /** + * Stores (creates or overwrites if resource with name exists) the resource under {@code name} with content provided + * by the stream. The {@link InputStream} should be closed when method returns. + * + * @param name Resource name, guaranteed to be non-{@code null} and is FS name and URL safe string. + * @param inputStream the content of the resource, guaranteed to be non-{@code null}. + */ + void save(final String name, final InputStream inputStream) throws IOException; +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/java/org/apache/maven/index/reader/CachingResourceHandler.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/java/org/apache/maven/index/reader/CachingResourceHandler.java b/indexer-reader/src/test/java/org/apache/maven/index/reader/CachingResourceHandler.java new file mode 100644 index 0000000..6afe222 --- /dev/null +++ b/indexer-reader/src/test/java/org/apache/maven/index/reader/CachingResourceHandler.java @@ -0,0 +1,60 @@ +package org.apache.maven.index.reader; + +/* + * 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; + +/** + * A trivial caching {@link ResourceHandler} that caches forever during single session (existence of the instance). + */ +public class CachingResourceHandler + implements ResourceHandler +{ + private final DirectoryResourceHandler local; + + private final ResourceHandler remote; + + public CachingResourceHandler(final DirectoryResourceHandler local, final ResourceHandler remote) { + if (local == null || remote == null) { + throw new NullPointerException("null resource handler"); + } + this.local = local; + this.remote = remote; + } + + public InputStream open(final String name) throws IOException { + InputStream inputStream = local.open(name); + if (inputStream != null) { + return inputStream; + } + inputStream = remote.open(name); + if (inputStream == null) { + return null; + } + local.save(name, inputStream); + return local.open(name); + } + + public void close() throws IOException { + remote.close(); + local.close(); + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/java/org/apache/maven/index/reader/ChunkReaderTest.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/java/org/apache/maven/index/reader/ChunkReaderTest.java b/indexer-reader/src/test/java/org/apache/maven/index/reader/ChunkReaderTest.java new file mode 100644 index 0000000..0d1915d --- /dev/null +++ b/indexer-reader/src/test/java/org/apache/maven/index/reader/ChunkReaderTest.java @@ -0,0 +1,66 @@ +package org.apache.maven.index.reader; + +/* + * 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.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.maven.index.reader.Record.Type; +import org.junit.Test; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +/** + * UT for {@link ChunkReader} + */ +public class ChunkReaderTest +{ + @Test + public void simple() throws IOException { + final Map<Type, Integer> recordTypes = new HashMap<Type, Integer>(); + recordTypes.put(Type.DESCRIPTOR, 0); + recordTypes.put(Type.ROOT_GROUPS, 0); + recordTypes.put(Type.ALL_GROUPS, 0); + recordTypes.put(Type.ARTIFACT_ADD, 0); + recordTypes.put(Type.ARTIFACT_REMOVE, 0); + + final ChunkReader chunkReader = new ChunkReader("full", + new FileInputStream("src/test/resources/nexus-maven-repository-index.gz")); + try { + assertThat(chunkReader.getVersion(), equalTo(1)); + assertThat(chunkReader.getTimestamp().getTime(), equalTo(1243533418015L)); + for (Record record : chunkReader) { + recordTypes.put(record.getType(), recordTypes.get(record.getType()) + 1); + } + } + finally { + chunkReader.close(); + } + + assertThat(recordTypes.get(Type.DESCRIPTOR), equalTo(1)); + assertThat(recordTypes.get(Type.ROOT_GROUPS), equalTo(1)); + assertThat(recordTypes.get(Type.ALL_GROUPS), equalTo(1)); + assertThat(recordTypes.get(Type.ARTIFACT_ADD), equalTo(2)); + assertThat(recordTypes.get(Type.ARTIFACT_REMOVE), equalTo(0)); + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/java/org/apache/maven/index/reader/DirectoryResourceHandler.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/java/org/apache/maven/index/reader/DirectoryResourceHandler.java b/indexer-reader/src/test/java/org/apache/maven/index/reader/DirectoryResourceHandler.java new file mode 100644 index 0000000..edb614a --- /dev/null +++ b/indexer-reader/src/test/java/org/apache/maven/index/reader/DirectoryResourceHandler.java @@ -0,0 +1,72 @@ +package org.apache.maven.index.reader; + +/* + * 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.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A trivial {@link File} directory handler that does not perform any locking or extra bits, and just serves up files + * by name from specified existing directory. + */ +public class DirectoryResourceHandler + implements WritableResourceHandler +{ + private final File rootDirectory; + + public DirectoryResourceHandler(final File rootDirectory) { + if (rootDirectory == null) { + throw new NullPointerException("null rootDirectory"); + } + if (!rootDirectory.isDirectory()) { + throw new IllegalArgumentException("rootDirectory exists and is not a directory"); + } + this.rootDirectory = rootDirectory; + } + + public InputStream open(final String name) throws IOException { + return new BufferedInputStream(new FileInputStream(new File(rootDirectory, name))); + } + + public void save(final String name, final InputStream inputStream) throws IOException { + try { + final BufferedOutputStream outputStream = new BufferedOutputStream( + new FileOutputStream(new File(rootDirectory, name))); + int read; + byte[] bytes = new byte[8192]; + while ((read = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, 0, read); + } + outputStream.close(); + } + finally { + inputStream.close(); + } + } + + public void close() throws IOException { + // nop + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/java/org/apache/maven/index/reader/HttpResourceHandler.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/java/org/apache/maven/index/reader/HttpResourceHandler.java b/indexer-reader/src/test/java/org/apache/maven/index/reader/HttpResourceHandler.java new file mode 100644 index 0000000..933cfbd --- /dev/null +++ b/indexer-reader/src/test/java/org/apache/maven/index/reader/HttpResourceHandler.java @@ -0,0 +1,57 @@ +package org.apache.maven.index.reader; + +/* + * 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.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * A trivial HTTP {@link ResourceHandler} that uses {@link URL} to fetch remote content. This implementation does not + * handle any advanced cases, like redirects, authentication, etc. + */ +public class HttpResourceHandler + implements ResourceHandler +{ + private final URI root; + + public HttpResourceHandler(final URL root) throws URISyntaxException { + if (root == null) { + throw new NullPointerException("root URL null"); + } + this.root = root.toURI(); + } + + public InputStream open(final String name) throws IOException { + URL target = root.resolve(name).toURL(); + HttpURLConnection conn = (HttpURLConnection) target.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "ASF Maven-Indexer-Reader/1.0"); + return new BufferedInputStream(conn.getInputStream()); + } + + public void close() throws IOException { + // nop + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/java/org/apache/maven/index/reader/IndexReaderTest.java ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/java/org/apache/maven/index/reader/IndexReaderTest.java b/indexer-reader/src/test/java/org/apache/maven/index/reader/IndexReaderTest.java new file mode 100644 index 0000000..e5d7df1 --- /dev/null +++ b/indexer-reader/src/test/java/org/apache/maven/index/reader/IndexReaderTest.java @@ -0,0 +1,102 @@ +package org.apache.maven.index.reader; + +/* + * 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.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URL; +import java.util.Arrays; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +/** + * UT for {@link IndexReader} + */ +public class IndexReaderTest +{ + @Test + public void simple() throws IOException { + final IndexReader indexReader = new IndexReader( + null, + new DirectoryResourceHandler(new File("src/test/resources/"))); + try { + assertThat(indexReader.getIndexId(), equalTo("apache-snapshots-local")); + assertThat(indexReader.getPublishedTimestamp().getTime(), equalTo(1243533418015L)); + assertThat(indexReader.isIncremental(), equalTo(false)); + assertThat(indexReader.getChunkNames(), equalTo(Arrays.asList("nexus-maven-repository-index.gz"))); + int chunks = 0; + int records = 0; + for (ChunkReader chunkReader : indexReader) { + chunks++; + assertThat(chunkReader.getName(), equalTo("nexus-maven-repository-index.gz")); + assertThat(chunkReader.getVersion(), equalTo(1)); + assertThat(chunkReader.getTimestamp().getTime(), equalTo(1243533418015L)); + for (Record record : chunkReader) { + records++; + } + } + + assertThat(chunks, equalTo(1)); + assertThat(records, equalTo(5)); + } + finally { + indexReader.close(); + } + } + + @Test + @Ignore("Here for example but test depending on external resource is not nice thing to have") + public void central() throws Exception { + final File tempDir = File.createTempFile("index-reader", "tmp"); + tempDir.mkdirs(); + final Writer writer = new OutputStreamWriter(System.out); + final IndexReader indexReader = new IndexReader( + new DirectoryResourceHandler(tempDir), + new HttpResourceHandler(new URL("http://repo1.maven.org/maven2/.index/")) + ); + try { + writer.write("indexRepoId=" + indexReader.getIndexId() + "\n"); + writer.write("indexLastPublished=" + indexReader.getPublishedTimestamp() + "\n"); + writer.write("isIncremental=" + indexReader.isIncremental() + "\n"); + writer.write("indexRequiredChunkNames=" + indexReader.getChunkNames() + "\n"); + for (ChunkReader chunkReader : indexReader) { + writer.write("chunkName=" + chunkReader.getName() + "\n"); + writer.write("chunkVersion=" + chunkReader.getVersion() + "\n"); + writer.write("chunkPublished=" + chunkReader.getTimestamp() + "\n"); + writer.write("= = = = = = \n"); + for (Record record : chunkReader) { + writer.write(record.getExpanded() + "\n"); + writer.write("--------- \n"); + writer.write(record.getRaw() + "\n"); + } + } + } + finally { + indexReader.close(); + writer.close(); + } + } +} http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/resources/nexus-maven-repository-index.gz ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/resources/nexus-maven-repository-index.gz b/indexer-reader/src/test/resources/nexus-maven-repository-index.gz new file mode 100644 index 0000000..490b21c Binary files /dev/null and b/indexer-reader/src/test/resources/nexus-maven-repository-index.gz differ http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/indexer-reader/src/test/resources/nexus-maven-repository-index.properties ---------------------------------------------------------------------- diff --git a/indexer-reader/src/test/resources/nexus-maven-repository-index.properties b/indexer-reader/src/test/resources/nexus-maven-repository-index.properties new file mode 100644 index 0000000..d1f1b48 --- /dev/null +++ b/indexer-reader/src/test/resources/nexus-maven-repository-index.properties @@ -0,0 +1,6 @@ +#Thu May 28 14:56:58 BRT 2009 +nexus.index.time=20090528175658.015 +0000 +nexus.index.chain-id=1243533418968 +nexus.index.id=apache-snapshots-local +nexus.index.timestamp=20090528175658.015 +0000 +nexus.index.last-incremental=0 http://git-wip-us.apache.org/repos/asf/maven-indexer/blob/af8783d8/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index cbeeecc..369fe42 100644 --- a/pom.xml +++ b/pom.xml @@ -160,6 +160,7 @@ under the License. <module>indexer-core</module> <module>indexer-artifact</module> <module>indexer-cli</module> + <module>indexer-reader</module> </modules> <build>
