Index: ivy.xml =================================================================== --- ivy.xml (revision 834895) +++ ivy.xml (working copy) @@ -50,6 +50,7 @@ + Index: META-INF/MANIFEST.MF =================================================================== --- META-INF/MANIFEST.MF (revision 834895) +++ META-INF/MANIFEST.MF (working copy) @@ -48,6 +48,8 @@ org.apache.ivy.core.event.publish;version="2.0.0", org.apache.ivy.core.event.resolve;version="2.0.0", org.apache.ivy.core.event.retrieve;version="2.0.0", + org.apache.ivy.core.index, + org.apache.ivy.core.index.model, org.apache.ivy.core.install;version="2.0.0", org.apache.ivy.core.module.descriptor;version="2.0.0", org.apache.ivy.core.module.id;version="2.0.0", Index: src/java/org/apache/ivy/ant/IvyIndex.java =================================================================== --- src/java/org/apache/ivy/ant/IvyIndex.java (revision 0) +++ src/java/org/apache/ivy/ant/IvyIndex.java (revision 0) @@ -0,0 +1,146 @@ +/* + * 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. + * + */ +package org.apache.ivy.ant; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; + +import org.apache.ivy.Ivy; +import org.apache.ivy.core.IvyPatternHelper; +import org.apache.ivy.core.index.RepositoryIndexManager; +import org.apache.ivy.core.module.id.ModuleRevisionId; +import org.apache.ivy.core.settings.IvySettings; +import org.apache.ivy.plugins.repository.Repository; +import org.apache.ivy.plugins.resolver.ChainResolver; +import org.apache.ivy.plugins.resolver.DependencyResolver; +import org.apache.ivy.plugins.resolver.RepositoryResolver; +import org.apache.ivy.plugins.resolver.util.ResolverHelper; +import org.apache.ivy.tools.analyser.JarModule; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; + +/** + * Completely re-indexes the repository. + */ +public class IvyIndex extends IvyTask { + private Collection/**/ excludeTypes = Arrays.asList(new String[] {"source", "src"}); + + public String[] getTypes() { + return (String[]) excludeTypes.toArray(new String[] {}); + } + + public void setTypes(String type) { + this.excludeTypes = Arrays.asList(type.split(",")); + } + + public void doExecute() throws BuildException { + Ivy ivy = getIvyInstance(); + IvySettings settings = ivy.getSettings(); + + DependencyResolver resolver = settings.getDefaultResolver(); + if(resolver != null) { + if(resolver instanceof RepositoryResolver) { + RepositoryIndexManager indexer = ((RepositoryResolver) resolver).getIndex(); + if(indexer != null) { + Collection/**/ jars = getIndexableRepositoryJars(resolver); + + if(jars.isEmpty()) { + log("No artifact patterns to index found in referenced resolver. ", Project.MSG_WARN); + } + + indexer.index((JarModule[]) jars.toArray(new JarModule[] {})); + } else { + throw new BuildException("No index reference was found on the default resolver named " + resolver.getName()); + } + } else { + throw new BuildException("Default resolver '" + resolver.getName() + "' is not a repository resolver. No indexing is possible."); + } + } else { + throw new BuildException("Default resolver not found."); + } + } + + protected Collection/**/ getIndexableRepositoryJars(DependencyResolver resolver) { + Collection/**/ jars = new HashSet/**/(); + + if (resolver instanceof ChainResolver) { + ChainResolver chainResolver = (ChainResolver) resolver; + for(Iterator iter = chainResolver.getResolvers().iterator(); iter.hasNext();) { + DependencyResolver subResolver = (DependencyResolver) iter.next(); + jars.addAll(getIndexableRepositoryJars(subResolver)); + } + } + + if(resolver instanceof RepositoryResolver) { + RepositoryResolver patternResolver = (RepositoryResolver) resolver; + for(Iterator iter = patternResolver.getArtifactPatterns().iterator(); iter.hasNext();) { + String pattern = (String) iter.next(); + jars.addAll(listAllJarModules(patternResolver.getRepository(), pattern)); + } + } + + return jars; + } + + protected Collection/**/ listAllJarModules(Repository rep, String pattern) { + return listAllJarModules(rep, pattern, null, null, null, null); + } + + private Collection/* */listAllJarModules(Repository rep, String pattern, + String organization, String module, String revision, String type) { + Collection/**/ artifacts = new HashSet/**/(); + String firstToken = IvyPatternHelper.getFirstToken(pattern); + + if(firstToken == null) { + // no more substitutions to perform + if(organization != null && module != null && revision != null && !excludeTypes.contains(type)) { + ModuleRevisionId revisionId = ModuleRevisionId.newInstance(organization, module, revision); + JarModule jar = new JarModule(revisionId, new File(pattern)); + artifacts.add(jar); + } + return artifacts; + } + + String[] substitutes = ResolverHelper.listNextTokenValues(rep, pattern); + if(substitutes != null) { + for(int i = 0; i < substitutes.length; i++) { + // recursively substitute tokens for their valid values + if(firstToken.equals(IvyPatternHelper.ORGANISATION_KEY) || firstToken.equals(IvyPatternHelper.ORGANISATION_KEY2)) { + organization = substitutes[i]; + } + else if(firstToken.equals(IvyPatternHelper.MODULE_KEY)) { + module = substitutes[i]; + } + else if(firstToken.equals(IvyPatternHelper.REVISION_KEY)) { + revision = substitutes[i]; + } + else if(firstToken.equals(IvyPatternHelper.TYPE_KEY)) { + type = substitutes[i]; + } + + artifacts.addAll(listAllJarModules(rep, IvyPatternHelper.substituteToken(pattern, + firstToken, substitutes[i]), organization, module, revision, type)); + } + } + + return artifacts; + } +} Property changes on: src\java\org\apache\ivy\ant\IvyIndex.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/ant/antlib.xml =================================================================== --- src/java/org/apache/ivy/ant/antlib.xml (revision 834895) +++ src/java/org/apache/ivy/ant/antlib.xml (working copy) @@ -43,4 +43,5 @@ + Index: src/java/org/apache/ivy/core/index/ArtifactTypeDocumentFactory.java =================================================================== --- src/java/org/apache/ivy/core/index/ArtifactTypeDocumentFactory.java (revision 0) +++ src/java/org/apache/ivy/core/index/ArtifactTypeDocumentFactory.java (revision 0) @@ -0,0 +1,92 @@ +/* + * 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. + * + */ +package org.apache.ivy.core.index; + +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.apache.ivy.core.module.id.ModuleRevisionId; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; + +/** + * Defines the format of the Lucene document used to store repository index information. + */ +public class ArtifactTypeDocumentFactory { + // Declaring these fields static and reusing the same instance makes createDocument(..) not + // thread safe, but saves a substantial amount of GC time in creating and reclaiming Field instances. + // We could make the factory thread safe by utilizing ThreadLocal. + + private static Field typeField = new Field("type", "", Field.Store.YES, + Field.Index.ANALYZED); + + private static Field fullyQualifiedTypeField = new Field("fullyQualifiedType", "", + Field.Store.YES, Field.Index.ANALYZED); + + private static Field rawFullyQualifiedTypeField = new Field("rawFullyQualifiedType", "", + Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS); + + private static Field moduleRevisionIdField = new Field("moduleRevisionId", "", Field.Store.YES, + Field.Index.NOT_ANALYZED_NO_NORMS); + + // Add the uid as a field, so that the index can be incrementally maintained. + private static Field jarUidField = new Field("jarUid", "", Field.Store.YES, + Field.Index.NOT_ANALYZED); + + /** + * This is not thread safe! + * + * @param jarClassFile + * @param containingJar + * @return + */ + public static Document createDocument(ZipEntry jarClassFile, ModuleRevisionId revisionId, JarFile containingJar, String jarUid) { + Document document = new Document(); + String entryName = jarClassFile.getName(); + if (entryName.endsWith(".class") && (entryName.indexOf('$') == -1)) { + // strip .class off of name + entryName = entryName.substring(0, entryName.lastIndexOf(".class")); + + String fullyQualifiedName = entryName.replace('/', '.'); + String typeName = fullyQualifiedName; + + int pos = fullyQualifiedName.lastIndexOf('$'); + if (pos == -1) { + pos = fullyQualifiedName.lastIndexOf('.'); + } + + if (pos != -1) { + typeName = fullyQualifiedName.substring(pos + 1); + } + + jarUidField.setValue(jarUid); + typeField.setValue(typeName); + fullyQualifiedTypeField.setValue(fullyQualifiedName); + rawFullyQualifiedTypeField.setValue(fullyQualifiedName); + moduleRevisionIdField.setValue(revisionId.encodeToString()); + + document.add(jarUidField); + document.add(typeField); + document.add(fullyQualifiedTypeField); + document.add(rawFullyQualifiedTypeField); + document.add(moduleRevisionIdField); + } + + return document; + } +} Property changes on: src\java\org\apache\ivy\core\index\ArtifactTypeDocumentFactory.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/core/index/DefaultRepositoryIndexManager.java =================================================================== --- src/java/org/apache/ivy/core/index/DefaultRepositoryIndexManager.java (revision 0) +++ src/java/org/apache/ivy/core/index/DefaultRepositoryIndexManager.java (revision 0) @@ -0,0 +1,233 @@ +/* + * 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. + * + */ +package org.apache.ivy.core.index; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.TreeSet; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; + +import org.apache.ivy.core.module.id.ModuleRevisionId; +import org.apache.ivy.tools.analyser.JarModule; +import org.apache.ivy.util.Message; +import org.apache.lucene.document.DateTools; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.LockObtainFailedException; + +public class DefaultRepositoryIndexManager extends RepositoryIndexManager { + private IndexWriter writer; + private IndexReader reader; + private TermEnum uidIter; + private File index; + + /* (non-Javadoc) + * @see org.apache.ivy.core.index.RepositoryIndexer#setStorageDir(java.lang.String) + */ + public void setStorageDir(String indexDir) { + index = new File(indexDir); + } + + /* (non-Javadoc) + * @see org.apache.ivy.core.index.RepositoryIndexer#index(org.apache.ivy.tools.analyser.JarModule) + */ + public synchronized void index(JarModule module) { + try { + // TODO this code could be simplified by deleting stale docs based on the results of a Query + writer = new IndexWriter(FSDirectory.open(index), + new LowerCaseAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED); + reader = IndexReader.open(FSDirectory.open(index), true); + uidIter = reader.terms(new Term("jarUid", "")); + + boolean alreadyIndexed = false; + if(uidIter != null) { + String uidModulePart = getJarUidModulePart(module); + do { + if(uidIter.term() != null && uidIter.term().text().contains(uidModulePart)) { + if(uidIter.term().text().equals(getJarUid(module))) { + // Jar is already indexed. + alreadyIndexed = true; + break; + } + else { + // An old version of the jar is indexed. We remove all types derived + // from this stale jar from the index. + writer.deleteDocuments(uidIter.term()); + break; + } + } + } while(uidIter.next()); + } + if(!alreadyIndexed) + addNewDocument(module, getJarUid(module)); + + reader.close(); + writer.optimize(); + writer.close(); + } catch (CorruptIndexException e) { + e.printStackTrace(); + } catch (LockObtainFailedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /* (non-Javadoc) + * @see org.apache.ivy.core.index.RepositoryIndexer#index(org.apache.ivy.tools.analyser.JarModule[]) + */ + public synchronized void index(JarModule[] modules) { + try { + writer = new IndexWriter(FSDirectory.open(index), + new LowerCaseAnalyzer(), IndexWriter.MaxFieldLength.LIMITED); + reader = IndexReader.open(FSDirectory.open(index), true); + uidIter = reader.terms(new Term("jarUid", "")); + + int jarsIndexed = 0; + long startTime = System.currentTimeMillis(); + + // sort the modules by the lexicographical ordering of their uids + TreeSet/**/ sortedModules = new TreeSet/**/(new Comparator() { + public int compare(Object m1, Object m2) { + return getJarUid((JarModule) m1).compareTo(getJarUid((JarModule) m2)); + } + }); + sortedModules.addAll(Arrays.asList(modules)); + + for(Iterator moduleIter = sortedModules.iterator(); moduleIter.hasNext();) { + JarModule jarModule = (JarModule) moduleIter.next(); + + String uid = getJarUid(jarModule); + + if (uidIter != null) { + // delete stale documents -- we can do this in constant time because the uids were built + // such that their lexicographical ordering yields the same ordering as the jar module + // finder. + while (uidIter.term() != null && uidIter.term().field().equals("jarUid") + && uidIter.term().text().compareTo(uid) < 0) { + Message.info("Deleting " + uidIter.term().text()); + System.out.println("Deleting " + uidIter.term().text()); + writer.deleteDocuments(uidIter.term()); + uidIter.next(); + } + + // this jar has already been indexed + if (uidIter.term() != null && uidIter.term().field().equals("jarUid") + && uidIter.term().text().equals(uid)) { + uidIter.next(); + continue; + } + } + + // this jar is new to the index + addNewDocument(jarModule, uid); + jarsIndexed++; + } + + // any remaining uids that weren't matched must be stale + while(uidIter.term() != null && uidIter.term().field().equals("jarUid")) { + Message.info("Deleting " + uidIter.term().text()); + System.out.println("Deleting " + uidIter.term().text()); + writer.deleteDocuments(uidIter.term()); + uidIter.next(); + } + + Message.info("Indexed " + jarsIndexed + " jars in " + + new Long(System.currentTimeMillis() - startTime) + " ms"); + System.out.println("Indexed " + jarsIndexed + " jars in " + + new Long(System.currentTimeMillis() - startTime) + " ms"); + + reader.close(); + writer.optimize(); + writer.close(); + } catch (CorruptIndexException e) { + e.printStackTrace(); + } catch (LockObtainFailedException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void addNewDocument(JarModule jarModule, String jarUid) { + try { + JarFile jar = new JarFile(jarModule.getJar()); + + + Message.info("indexing " + jarModule.getJar().toString() + "(Organization: " + + jarModule.getMrid().getOrganisation() + ", Module: " + + jarModule.getMrid().getName() + ", Revision: " + + jarModule.getMrid().getRevision() + ")"); + System.out.println("indexing " + jarModule.getJar().toString() + "(Organization: " + + jarModule.getMrid().getOrganisation() + ", Module: " + + jarModule.getMrid().getName() + ", Revision: " + + jarModule.getMrid().getRevision() + ")"); + + Enumeration entries = jar.entries(); + ZipEntry entry; + + while (entries.hasMoreElements()) { + entry = (ZipEntry) entries.nextElement(); + Document entryDocument = ArtifactTypeDocumentFactory.createDocument( + entry, jarModule.getMrid(), jar, jarUid); + writer.addDocument(entryDocument); + } + } catch (ZipException ignore) { + // If the jar itself is corrupt, we want to continue gracefully. + // We won't be able to extract class types from a corrupt jar anyway + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Append the module revision id and date into a string in such a way that + * lexicographic sorting gives the same results as the jar module finder. + * Thus, null (\u0000) is used to separate the elements of the module + * revision id and the date.
+ * + * @param containingJar + * @return + */ + private String getJarUid(JarModule jar) { + return getJarUidModulePart(jar) + + "\u0000" + + DateTools.timeToString(jar.getJar().lastModified(), + DateTools.Resolution.SECOND); + } + + private String getJarUidModulePart(JarModule jar) { + ModuleRevisionId mrid = jar.getMrid(); + return mrid.getOrganisation() + + "\u0000" + + mrid.getName() + + "\u0000" + + mrid.getRevision(); + } +} Property changes on: src\java\org\apache\ivy\core\index\DefaultRepositoryIndexManager.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/core/index/DefaultRepositorySearcher.java =================================================================== --- src/java/org/apache/ivy/core/index/DefaultRepositorySearcher.java (revision 0) +++ src/java/org/apache/ivy/core/index/DefaultRepositorySearcher.java (revision 0) @@ -0,0 +1,92 @@ +/* + * 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. + * + */ +package org.apache.ivy.core.index; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; + +import org.apache.ivy.core.index.model.ClassTypeComposite; +import org.apache.ivy.core.module.id.ModuleRevisionId; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.store.FSDirectory; + +public class DefaultRepositorySearcher { + private IndexReader reader; + private Searcher searcher; + private QueryParser parser; + + private abstract class CollectorAdapter extends Collector { + public boolean acceptsDocsOutOfOrder() { + return true; + } + public void setNextReader(IndexReader reader, int docBase) throws IOException {} + public void setScorer(Scorer scorer) throws IOException {} + }; + + private class TypeSearchCollector extends CollectorAdapter { + private Collection/**/ collected = new HashSet/**/(); + + public void collect(int doc) throws IOException { + Document document = reader.document(doc); + ModuleRevisionId mrid = ModuleRevisionId.decode(document.get("moduleRevisionId")); + ClassTypeComposite.addClassType(collected, document.get("rawFullyQualifiedType"), mrid); + } + + public ClassTypeComposite[] getCollected() { + return (ClassTypeComposite[]) collected.toArray(new ClassTypeComposite[collected.size()]); + } + } + + public DefaultRepositorySearcher(File index) throws CorruptIndexException, IOException { + reader = IndexReader.open(FSDirectory.open(index), true); + searcher = new IndexSearcher(reader); + parser = new QueryParser("type", new LowerCaseAnalyzer()); + } + + /** + * + * @param typeSubstring + * @return A map of fully qualified type names and the module revisions they are found in. + */ + public ClassTypeComposite[] searchForType(String typeSubstring) { + try { + Query query = parser.parse("type:" + typeSubstring + "*"); + TypeSearchCollector searchResults = new TypeSearchCollector(); + + searcher.search(query, searchResults); + return searchResults.getCollected(); + } catch (ParseException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return new ClassTypeComposite[] {}; + } +} Property changes on: src\java\org\apache\ivy\core\index\DefaultRepositorySearcher.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/core/index/LowerCaseAnalyzer.java =================================================================== --- src/java/org/apache/ivy/core/index/LowerCaseAnalyzer.java (revision 0) +++ src/java/org/apache/ivy/core/index/LowerCaseAnalyzer.java (revision 0) @@ -0,0 +1,45 @@ +/* + * 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. + * + */ +package org.apache.ivy.core.index; + +import java.io.Reader; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CharTokenizer; +import org.apache.lucene.analysis.TokenStream; + +public class LowerCaseAnalyzer extends Analyzer { + // This tokenizer accepts the whole input string as one token + private class WholeTokenizer extends CharTokenizer { + public WholeTokenizer(Reader in) { + super(in); + } + + protected boolean isTokenChar(char c) { + return true; + } + + protected char normalize(char c) { + return Character.toLowerCase(c); + } + } + + public TokenStream tokenStream(String s, Reader reader) { + return new WholeTokenizer(reader); + } +} \ No newline at end of file Property changes on: src\java\org\apache\ivy\core\index\LowerCaseAnalyzer.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/core/index/RepositoryIndexManager.java =================================================================== --- src/java/org/apache/ivy/core/index/RepositoryIndexManager.java (revision 0) +++ src/java/org/apache/ivy/core/index/RepositoryIndexManager.java (revision 0) @@ -0,0 +1,42 @@ +/* + * 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. + * + */ +package org.apache.ivy.core.index; + +import org.apache.ivy.tools.analyser.JarModule; + +public abstract class RepositoryIndexManager { + private String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public abstract void setStorageDir(String indexDir); + + public abstract void index(JarModule module); + + public abstract void index(JarModule[] modules); + + public String toString() { + return name; + } +} \ No newline at end of file Property changes on: src\java\org\apache\ivy\core\index\RepositoryIndexManager.java ___________________________________________________________________ Added: svn:eol-style + native Index: src/java/org/apache/ivy/core/settings/IvySettings.java =================================================================== --- src/java/org/apache/ivy/core/settings/IvySettings.java (revision 834895) +++ src/java/org/apache/ivy/core/settings/IvySettings.java (working copy) @@ -48,6 +48,7 @@ import org.apache.ivy.core.cache.ResolutionCacheManager; import org.apache.ivy.core.check.CheckEngineSettings; import org.apache.ivy.core.deliver.DeliverEngineSettings; +import org.apache.ivy.core.index.RepositoryIndexManager; import org.apache.ivy.core.install.InstallEngineSettings; import org.apache.ivy.core.module.id.ModuleId; import org.apache.ivy.core.module.id.ModuleRevisionId; @@ -154,7 +155,10 @@ private Map circularDependencyStrategies = new HashMap(); // Map (String name -> RepositoryCacheManager) - private Map repositoryCacheManagers = new HashMap(); + private Map repositoryCacheManagers = new HashMap(); + + // Map (String name -> RepositoryIndexManager) + private Map repositoryIndexManagers = new HashMap(); // List (Trigger) private List triggers = new ArrayList(); @@ -1015,7 +1019,24 @@ init(m); matchers.put(m.getName(), m); } - + + public void addConfigured(RepositoryIndexManager i) { + addRepositoryIndexManager(i); + } + + public Map getRepositoryIndexManagers() { + return repositoryIndexManagers; + } + + public RepositoryIndexManager getRepositoryIndexManager(String name) { + return (RepositoryIndexManager) repositoryIndexManagers.get(name); + } + + public void addRepositoryIndexManager(RepositoryIndexManager i) { + init(i); + repositoryIndexManagers.put(i.getName(), i); + } + public void addConfigured(RepositoryCacheManager c) { addRepositoryCacheManager(c); } @@ -1228,7 +1249,7 @@ } return resolutionCacheManager; } - + public void setResolutionCacheManager(ResolutionCacheManager resolutionCacheManager) { this.resolutionCacheManager = resolutionCacheManager; } Index: src/java/org/apache/ivy/core/settings/XmlSettingsParser.java =================================================================== --- src/java/org/apache/ivy/core/settings/XmlSettingsParser.java (revision 834895) +++ src/java/org/apache/ivy/core/settings/XmlSettingsParser.java (working copy) @@ -107,7 +107,7 @@ private List configuratorTags = Arrays.asList(new String[] {"resolvers", "namespaces", "parsers", "latest-strategies", "conflict-managers", "outputters", "version-matchers", "statuses", "circular-dependency-strategies", "triggers", "lock-strategies", - "caches"}); + "caches", "indexes"}); private IvySettings ivy; @@ -226,6 +226,7 @@ } else if ("credentials".equals(qName)) { credentialsStarted(attributes); } + } catch (ParseException ex) { SAXException sax = new SAXException("problem in config file: " + ex.getMessage(), ex); sax.initCause(ex); Index: src/java/org/apache/ivy/core/settings/typedef.properties =================================================================== --- src/java/org/apache/ivy/core/settings/typedef.properties (revision 834895) +++ src/java/org/apache/ivy/core/settings/typedef.properties (working copy) @@ -54,4 +54,6 @@ ant-call = org.apache.ivy.ant.AntCallTrigger log = org.apache.ivy.plugins.trigger.LogTrigger -cache = org.apache.ivy.core.cache.DefaultRepositoryCacheManager \ No newline at end of file +cache = org.apache.ivy.core.cache.DefaultRepositoryCacheManager + +index = org.apache.ivy.core.index.DefaultRepositoryIndexManager \ No newline at end of file Index: src/java/org/apache/ivy/plugins/resolver/RepositoryResolver.java =================================================================== --- src/java/org/apache/ivy/plugins/resolver/RepositoryResolver.java (revision 834895) +++ src/java/org/apache/ivy/plugins/resolver/RepositoryResolver.java (working copy) @@ -31,6 +31,7 @@ import org.apache.ivy.core.IvyPatternHelper; import org.apache.ivy.core.event.EventManager; +import org.apache.ivy.core.index.RepositoryIndexManager; import org.apache.ivy.core.module.descriptor.Artifact; import org.apache.ivy.core.module.descriptor.DefaultArtifact; import org.apache.ivy.core.module.descriptor.ModuleDescriptor; @@ -46,6 +47,7 @@ import org.apache.ivy.plugins.resolver.util.ResolverHelper; import org.apache.ivy.plugins.resolver.util.ResourceMDParser; import org.apache.ivy.plugins.version.VersionMatcher; +import org.apache.ivy.tools.analyser.JarModule; import org.apache.ivy.util.ChecksumHelper; import org.apache.ivy.util.FileUtil; import org.apache.ivy.util.Message; @@ -56,6 +58,10 @@ public class RepositoryResolver extends AbstractPatternsBasedResolver { private Repository repository; + + private String indexerName; + + private RepositoryIndexManager indexer; private Boolean alwaysCheckExactRevision = null; @@ -208,6 +214,10 @@ put(artifact, src, dest, overwrite); Message.info("\tpublished " + artifact.getName() + " to " + hidePassword(dest)); + + if(indexer != null) { + indexer.index(new JarModule(artifact.getModuleRevisionId(), src)); + } } protected String getDestination(String pattern, Artifact artifact, ModuleRevisionId mrid) { @@ -302,6 +312,14 @@ } } + public void setIndex(String indexerName) { + this.indexerName = indexerName; + } + + public RepositoryIndexManager getIndex() { + return indexer; + } + public boolean isAlwaysCheckExactRevision() { return alwaysCheckExactRevision == null ? true : alwaysCheckExactRevision.booleanValue(); } @@ -310,4 +328,20 @@ this.alwaysCheckExactRevision = Boolean.valueOf(alwaysCheckExactRevision); } + public void validate() { + super.validate(); + initRepositoryIndexManagerFromSettings(); + } + + private void initRepositoryIndexManagerFromSettings() { + if (indexerName != null) { + indexer = getSettings().getRepositoryIndexManager(indexerName); + if(indexer == null) { + throw new IllegalStateException( + "unknown index manager '" + indexerName + + "'. Available indexes are " + + Arrays.asList(getSettings().getRepositoryCacheManagers())); + } + } + } } Index: src/java/org/apache/ivy/plugins/resolver/ResolverSettings.java =================================================================== --- src/java/org/apache/ivy/plugins/resolver/ResolverSettings.java (revision 834895) +++ src/java/org/apache/ivy/plugins/resolver/ResolverSettings.java (working copy) @@ -20,6 +20,7 @@ import java.util.Collection; import org.apache.ivy.core.cache.RepositoryCacheManager; +import org.apache.ivy.core.index.RepositoryIndexManager; import org.apache.ivy.core.module.id.ModuleId; import org.apache.ivy.plugins.latest.LatestStrategy; import org.apache.ivy.plugins.namespace.Namespace; @@ -52,4 +53,5 @@ void filterIgnore(Collection names); + RepositoryIndexManager getRepositoryIndexManager(String name); } Index: src/java/org/apache/ivy/plugins/resolver/util/ResolverHelper.java =================================================================== --- src/java/org/apache/ivy/plugins/resolver/util/ResolverHelper.java (revision 834895) +++ src/java/org/apache/ivy/plugins/resolver/util/ResolverHelper.java (working copy) @@ -22,6 +22,8 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; @@ -37,6 +39,73 @@ public final class ResolverHelper { private ResolverHelper() { } + + /** + * Identifies the first token in pathPattern and returns valid value matches + * according to the file/folder listing of the repository. + * + * @param rep + * @param pathPattern + * @return + */ + public static String[] listNextTokenValues(Repository rep, String pathPattern) { + Collection/**/ values = new HashSet/**/(); + String fileSep = rep.getFileSeparator(); + String beforeToken = IvyPatternHelper.getTokenRoot(pathPattern); + + // identify the part of the URL that will be matched against files/folders in root + String matchPart; + int firstSepPastRoot = pathPattern.indexOf(fileSep, beforeToken.length()); + if(firstSepPastRoot == -1) { + matchPart = pathPattern.substring(beforeToken.length()); + } else { + matchPart = pathPattern.substring(beforeToken.length(), firstSepPastRoot); + } + + // identify the part of the URL that is the directory to search in -- in most cases + // searchRoot == beforeToken, but not when the token is not at the beginning of its containing + // URL segment (e.g. for pattern "root/prefix-[token]/", beforeToken == "root/prefix-", but + // searchRoot = "root") + int firstSepBeforeRoot = pathPattern.lastIndexOf(fileSep, beforeToken.length()); + String searchRoot = ""; + if(firstSepBeforeRoot != -1) { + searchRoot = beforeToken.substring(0, beforeToken.lastIndexOf(fileSep)); + } + + // prepare the regexp + matchPart = matchPart.replaceAll("\\.", "\\\\."); // escape .'s + while(IvyPatternHelper.getFirstToken(matchPart) != null) { + // we temporarily replace the token with a placeholder, since the regexp contains a '[', causing an infinite loop + matchPart = IvyPatternHelper.substituteToken(matchPart, IvyPatternHelper.getFirstToken(matchPart), ""); + } + matchPart = matchPart.replaceAll("", "([^" + fileSep + "]+)"); + + // fileSep + "?" is included for when the pattern starts with a token since rep.list(..) + // adds a leading fileSep in this case + Pattern p = Pattern.compile(beforeToken.replaceAll("\\\\", "\\\\\\\\") + fileSep + "?" + matchPart + "($|" + fileSep + ".*)"); + + // match directory listing against the regexp and extract the relevant group + try { + List/**/ all = rep.list(searchRoot); + if (all != null) { + for (Iterator iter = all.iterator(); iter.hasNext();) { + String path = (String) iter.next(); + Matcher m = p.matcher(path); + if (m.matches()) { + String value = m.group(1); + values.add(value); + } + } + } + + return (String[]) values.toArray(new String[] {}); + } catch (IOException e) { + Message.verbose( + "problem while listing resources in " + beforeToken + " with " + rep + ":"); + Message.verbose(" " + e.getClass().getName() + " " + e.getMessage()); + return null; + } + } // lists all the values a token can take in a pattern, as listed by a given url lister public static String[] listTokenValues(Repository rep, String pattern, String token) { @@ -49,15 +118,27 @@ + ": token not found in pattern"); return null; } - if (((pattern.length() <= index + tokenString.length()) || fileSep.equals(pattern - .substring(index + tokenString.length(), index + tokenString.length() + 1))) - && (index == 0 || fileSep.equals(pattern.substring(index - 1, index)))) { + if ( + ( + (pattern.length() <= index + tokenString.length()) /* the pattern ends in the token */ + || fileSep.equals(pattern.substring(index + tokenString.length(), index + tokenString.length() + 1)) /* the path segment ends with the token */ + ) + && + ( + index == 0 /* the pattern begins with the token */ + || fileSep.equals(pattern.substring(index - 1, index)) /* the path segment begins with the token */ + ) + ) { // the searched token is a whole name String root = pattern.substring(0, index); return listAll(rep, root); } else { - int slashIndex = pattern.substring(0, index).lastIndexOf(fileSep); - String root = slashIndex == -1 ? "" : pattern.substring(0, slashIndex); + // last file separator before the token + int slashIndex = pattern.substring(0, index).lastIndexOf(fileSep); + + String root = slashIndex == -1 ? + "" /* pattern begins with the token */ + : pattern.substring(0, slashIndex); try { Message.debug("\tusing " + rep + " to list all in " + root); @@ -65,12 +146,13 @@ if (all != null) { Message.debug("\t\tfound " + all.size() + " urls"); List ret = new ArrayList(all.size()); - int endNameIndex = pattern.indexOf(fileSep, slashIndex + 1); + + int endNameIndex = pattern.indexOf(fileSep, slashIndex + 1); // search for the next file separator String namePattern; if (endNameIndex != -1) { - namePattern = pattern.substring(slashIndex + 1, endNameIndex); + namePattern = pattern.substring(slashIndex + 1, endNameIndex); // end at the next file separator } else { - namePattern = pattern.substring(slashIndex + 1); + namePattern = pattern.substring(slashIndex + 1); // pattern ends with the token } namePattern = namePattern.replaceAll("\\.", "\\\\."); String acceptNamePattern = ".*?" @@ -189,67 +271,6 @@ return null; } - // public static ResolvedResource[] findAll(Repository rep, ModuleRevisionId mrid, String - // pattern, Artifact artifact, VersionMatcher versionMatcher, ResourceMDParser mdParser) { - // // substitute all but revision - // String partiallyResolvedPattern = IvyPatternHelper.substitute(pattern, new - // ModuleRevisionId(mrid.getModuleId(), - // IvyPatternHelper.getTokenString(IvyPatternHelper.REVISION_KEY), mrid.getExtraAttributes()), - // artifact); - // Message.debug("\tlisting all in "+partiallyResolvedPattern); - // - // String[] revs = listTokenValues(rep, partiallyResolvedPattern, - // IvyPatternHelper.REVISION_KEY); - // if (revs != null) { - // Message.debug("\tfound revs: "+Arrays.asList(revs)); - // List ret = new ArrayList(revs.length); - // String rres = null; - // for (int i = 0; i < revs.length; i++) { - // ModuleRevisionId foundMrid = new ModuleRevisionId(mrid.getModuleId(), revs[i], - // mrid.getExtraAttributes()); - // if (versionMatcher.accept(mrid, foundMrid)) { - // rres = IvyPatternHelper.substituteToken(partiallyResolvedPattern, - // IvyPatternHelper.REVISION_KEY, revs[i]); - // try { - // ResolvedResource resolvedResource; - // if (versionMatcher.needModuleDescriptor(mrid, foundMrid)) { - // resolvedResource = mdParser.parse(rep.getResource(rres), revs[i]); - // if (!versionMatcher.accept(mrid, - // ((MDResolvedResource)resolvedResource).getResolvedModuleRevision().getDescriptor())) { - // continue; - // } - // } else { - // resolvedResource = new ResolvedResource(rep.getResource(rres), revs[i]); - // } - // ret.add(resolvedResource); - // } catch (IOException e) { - // Message.warn("impossible to get resource from name listed by repository: "+rres+": - // "+e.getMessage()); - // } - // } - // } - // if (revs.length != ret.size()) { - // Message.debug("\tfound resolved res: "+ret); - // } - // return (ResolvedResource[])ret.toArray(new ResolvedResource[ret.size()]); - // } else { - // // maybe the partially resolved pattern is completely resolved ? - // try { - // Resource res = rep.getResource(partiallyResolvedPattern); - // if (res.exists()) { - // Message.debug("\tonly one resource found without real listing: using and defining it as - // working@"+rep.getName()+" revision: "+res.getName()); - // return new ResolvedResource[] {new ResolvedResource(res, "working@"+rep.getName())}; - // } - // } catch (IOException e) { - // Message.debug("\timpossible to get resource from name listed by repository: - // "+partiallyResolvedPattern+": "+e.getMessage()); - // } - // Message.debug("\tno revision found"); - // } - // return null; - // } - // lists all the values a token can take in a pattern, as listed by a given url lister public static String[] listTokenValues(URLLister lister, String pattern, String token) { pattern = standardize(pattern); Index: test/java/org/apache/ivy/ant/IvyIndexTest.java =================================================================== --- test/java/org/apache/ivy/ant/IvyIndexTest.java (revision 0) +++ test/java/org/apache/ivy/ant/IvyIndexTest.java (revision 0) @@ -0,0 +1,71 @@ +/* + * 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. + * + */ +package org.apache.ivy.ant; + +import java.io.File; +import java.util.Collection; + +import junit.framework.TestCase; + +import org.apache.ivy.plugins.repository.file.FileRepository; +import org.apache.ivy.util.DefaultMessageLogger; +import org.apache.ivy.util.FileUtil; +import org.apache.ivy.util.Message; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.types.Reference; + +public class IvyIndexTest extends TestCase { + private IvyIndex index; + private Project project; + + // This should match the path defined in ivy settings. + private final String indexLoc = "test/repositories/indexable/.index"; + + public void testListAllArtifacts() { + File repositoryBase = new File("test/repositories/indexable"); + Collection/**/ artifactPaths = new IvyIndex().listAllJarModules( + new FileRepository(repositoryBase.getAbsoluteFile()), "[organisation]/[module]/[type]/[artifact]-[revision].[ext]"); + + assertTrue(artifactPaths.size() > 0); + } + + public void testIndex() { + project = new Project(); + project.init(); + project.setProperty("ivy.settings.file", "test/repositories/indexable/ivysettings-index.xml"); + project.setProperty("build", "build/test/index"); + + IvyConfigure configure = new IvyConfigure(); + configure.setFile(new File("test/repositories/indexable/ivysettings-index.xml")); + configure.setProject(project); + configure.setSettingsId("settings"); + configure.execute(); + + index = new IvyIndex(); + index.setProject(project); + index.setSettingsRef(new Reference(configure.getSettingsId())); + + Message.setDefaultLogger(new DefaultMessageLogger(10)); + + index.execute(); + } + + protected void tearDown() throws Exception { + FileUtil.forceDelete(new File(indexLoc)); + } +} Property changes on: test\java\org\apache\ivy\ant\IvyIndexTest.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/java/org/apache/ivy/core/settings/XmlSettingsParserTest.java =================================================================== --- test/java/org/apache/ivy/core/settings/XmlSettingsParserTest.java (revision 834895) +++ test/java/org/apache/ivy/core/settings/XmlSettingsParserTest.java (working copy) @@ -186,6 +186,14 @@ assertEquals("default", settings.getResolveMode(new ModuleId("apache", "ivy"))); } + public void testIndex() throws Exception { + IvySettings settings = new IvySettings(); + XmlSettingsParser parser = new XmlSettingsParser(settings); + parser.parse(XmlSettingsParserTest.class.getResource("ivysettings-index.xml")); + + assertNotNull(settings.getRepositoryIndexManager("index1")); + } + public void testCache() throws Exception { IvySettings settings = new IvySettings(); XmlSettingsParser parser = new XmlSettingsParser(settings); Index: test/java/org/apache/ivy/core/settings/ivysettings-index.xml =================================================================== --- test/java/org/apache/ivy/core/settings/ivysettings-index.xml (revision 0) +++ test/java/org/apache/ivy/core/settings/ivysettings-index.xml (revision 0) @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + Property changes on: test\java\org\apache\ivy\core\settings\ivysettings-index.xml ___________________________________________________________________ Added: svn:eol-style + native Index: test/java/org/apache/ivy/plugins/resolver/util/ResolverHelperTest.java =================================================================== --- test/java/org/apache/ivy/plugins/resolver/util/ResolverHelperTest.java (revision 0) +++ test/java/org/apache/ivy/plugins/resolver/util/ResolverHelperTest.java (revision 0) @@ -0,0 +1,82 @@ +/* + * 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. + * + */ +package org.apache.ivy.plugins.resolver.util; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; + +import junit.framework.TestCase; + +import org.apache.ivy.plugins.repository.file.FileRepository; + +public class ResolverHelperTest extends TestCase { + public void testListTokenValues() { + File repositoryBase = new File("test/repositories/1"); + FileRepository repository = new FileRepository(repositoryBase.getAbsoluteFile()); + + Collection/**/ values; + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "[organisation]/[module]/[type]s/[module]-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("org1")); + assertTrue(values.contains("org2")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "org1/[module]/[type]s/[module]-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("mod1.1")); + assertTrue(values.contains("mod1.2")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "org1/mod1.1/[type]s/[module]-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("ivy")); + assertTrue(values.contains("jar")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "org1/mod1.1/jars/[module]-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("mod1.1")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "org1/mod1.1/jars/mod1.1-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("1.0")); + assertTrue(values.contains("1.1")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "org1/mod1.1/jars/mod1.1-1.0.[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("jar")); + } + + public void testOptionalTokenValues() { + File repositoryBase = new File("test/repositories/indexable"); + FileRepository repository = new FileRepository(repositoryBase.getAbsoluteFile()); + + Collection/**/ values; + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "junit/junit/source/[artifact](-)([type])-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("junit")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "junit/junit/source/junit(-)([type])-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("-")); + + values = Arrays.asList(ResolverHelper.listNextTokenValues(repository, "junit/junit/source/junit-([type])-[revision].[ext]")); + assertTrue(values.size() > 0); + assertTrue(values.contains("src")); + } +} Property changes on: test\java\org\apache\ivy\plugins\resolver\util\ResolverHelperTest.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/repositories/indexable/ivysettings-index.xml =================================================================== --- test/repositories/indexable/ivysettings-index.xml (revision 0) +++ test/repositories/indexable/ivysettings-index.xml (revision 0) @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + Property changes on: test\repositories\indexable\ivysettings-index.xml ___________________________________________________________________ Added: svn:eol-style + native