mirror of https://github.com/apache/archiva.git
remove old consumers for indexing
git-svn-id: https://svn.apache.org/repos/asf/archiva/trunk@741493 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
9cb1a3f329
commit
b521fd00d2
|
@ -1,216 +0,0 @@
|
||||||
package org.apache.maven.archiva.consumers.lucene;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.apache.maven.archiva.configuration.ArchivaConfiguration;
|
|
||||||
import org.apache.maven.archiva.configuration.ConfigurationNames;
|
|
||||||
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
|
|
||||||
import org.apache.maven.archiva.consumers.AbstractMonitoredConsumer;
|
|
||||||
import org.apache.maven.archiva.consumers.ConsumerException;
|
|
||||||
import org.apache.maven.archiva.database.updater.DatabaseUnprocessedArtifactConsumer;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndex;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndexFactory;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryIndexException;
|
|
||||||
import org.apache.maven.archiva.indexer.hashcodes.HashcodesRecord;
|
|
||||||
import org.apache.maven.archiva.model.ArchivaArtifact;
|
|
||||||
import org.apache.maven.archiva.repository.ManagedRepositoryContent;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryContentFactory;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryException;
|
|
||||||
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
|
|
||||||
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
|
|
||||||
import org.codehaus.plexus.registry.Registry;
|
|
||||||
import org.codehaus.plexus.registry.RegistryListener;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IndexArtifactConsumer
|
|
||||||
*
|
|
||||||
* @version $Id$
|
|
||||||
* @plexus.component role="org.apache.maven.archiva.database.updater.DatabaseUnprocessedArtifactConsumer"
|
|
||||||
* role-hint="index-artifact"
|
|
||||||
* instantiation-strategy="per-lookup"
|
|
||||||
*/
|
|
||||||
public class IndexArtifactConsumer
|
|
||||||
extends AbstractMonitoredConsumer
|
|
||||||
implements DatabaseUnprocessedArtifactConsumer, RegistryListener, Initializable
|
|
||||||
{
|
|
||||||
private Logger log = LoggerFactory.getLogger( IndexArtifactConsumer.class );
|
|
||||||
|
|
||||||
private static final String INDEX_ERROR = "indexing_error";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="index-artifact"
|
|
||||||
*/
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="Index the artifact checksums for Find functionality."
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private ArchivaConfiguration configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private RepositoryContentFactory repositoryFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement role-hint="lucene"
|
|
||||||
*/
|
|
||||||
private RepositoryContentIndexFactory indexFactory;
|
|
||||||
|
|
||||||
private Map<String, IndexedRepositoryDetails> repositoryMap = new HashMap<String, IndexedRepositoryDetails>();
|
|
||||||
|
|
||||||
public void beginScan()
|
|
||||||
{
|
|
||||||
/* nothing to do here */
|
|
||||||
}
|
|
||||||
|
|
||||||
public void completeScan()
|
|
||||||
{
|
|
||||||
/* nothing to do here */
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getIncludedTypes()
|
|
||||||
{
|
|
||||||
return null; // TODO: define these as a list of artifacts.
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processArchivaArtifact( ArchivaArtifact artifact )
|
|
||||||
throws ConsumerException
|
|
||||||
{
|
|
||||||
HashcodesRecord record = new HashcodesRecord();
|
|
||||||
record.setRepositoryId( artifact.getModel().getRepositoryId() );
|
|
||||||
record.setArtifact( artifact );
|
|
||||||
|
|
||||||
IndexedRepositoryDetails pnl = getIndexedRepositoryDetails( artifact );
|
|
||||||
|
|
||||||
String artifactPath = pnl.repository.toPath( artifact );
|
|
||||||
record.setFilename( artifactPath );
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pnl.index.modifyRecord( record );
|
|
||||||
}
|
|
||||||
catch ( RepositoryIndexException e )
|
|
||||||
{
|
|
||||||
triggerConsumerError( INDEX_ERROR, "Unable to index hashcodes: " + e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IndexedRepositoryDetails getIndexedRepositoryDetails( ArchivaArtifact artifact )
|
|
||||||
{
|
|
||||||
String repoId = artifact.getModel().getRepositoryId();
|
|
||||||
if ( StringUtils.isBlank( repoId ) )
|
|
||||||
{
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Unable to process artifact [" + artifact + "] as it has no repository id associated with it." );
|
|
||||||
}
|
|
||||||
|
|
||||||
return getIndexedRepositoryDetails( repoId );
|
|
||||||
}
|
|
||||||
|
|
||||||
private IndexedRepositoryDetails getIndexedRepositoryDetails( String id )
|
|
||||||
{
|
|
||||||
return this.repositoryMap.get( id );
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription()
|
|
||||||
{
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId()
|
|
||||||
{
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPermanent()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue )
|
|
||||||
{
|
|
||||||
if ( ConfigurationNames.isManagedRepositories( propertyName ) )
|
|
||||||
{
|
|
||||||
initRepositoryMap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue )
|
|
||||||
{
|
|
||||||
/* do nothing */
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initialize()
|
|
||||||
throws InitializationException
|
|
||||||
{
|
|
||||||
initRepositoryMap();
|
|
||||||
configuration.addChangeListener( this );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initRepositoryMap()
|
|
||||||
{
|
|
||||||
synchronized ( this.repositoryMap )
|
|
||||||
{
|
|
||||||
this.repositoryMap.clear();
|
|
||||||
|
|
||||||
Iterator<ManagedRepositoryConfiguration> it = configuration.getConfiguration().getManagedRepositories().iterator();
|
|
||||||
while ( it.hasNext() )
|
|
||||||
{
|
|
||||||
ManagedRepositoryConfiguration repository = it.next();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IndexedRepositoryDetails pnl = new IndexedRepositoryDetails();
|
|
||||||
|
|
||||||
pnl.repository = repositoryFactory.getManagedRepositoryContent( repository.getId() );
|
|
||||||
|
|
||||||
pnl.index = indexFactory.createHashcodeIndex( repository );
|
|
||||||
|
|
||||||
this.repositoryMap.put( repository.getId(), pnl );
|
|
||||||
}
|
|
||||||
catch ( RepositoryException e )
|
|
||||||
{
|
|
||||||
log.error( "Unable to load repository content object: " + e.getMessage(), e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IndexedRepositoryDetails
|
|
||||||
{
|
|
||||||
public ManagedRepositoryContent repository;
|
|
||||||
|
|
||||||
public RepositoryContentIndex index;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,218 +0,0 @@
|
||||||
package org.apache.maven.archiva.consumers.lucene;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.apache.maven.archiva.configuration.ArchivaConfiguration;
|
|
||||||
import org.apache.maven.archiva.configuration.ConfigurationNames;
|
|
||||||
import org.apache.maven.archiva.configuration.FileTypes;
|
|
||||||
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
|
|
||||||
import org.apache.maven.archiva.consumers.AbstractMonitoredConsumer;
|
|
||||||
import org.apache.maven.archiva.consumers.ConsumerException;
|
|
||||||
import org.apache.maven.archiva.consumers.KnownRepositoryContentConsumer;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndex;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndexFactory;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryIndexException;
|
|
||||||
import org.apache.maven.archiva.indexer.filecontent.FileContentRecord;
|
|
||||||
import org.apache.maven.archiva.model.ArchivaArtifact;
|
|
||||||
import org.apache.maven.archiva.model.ArtifactReference;
|
|
||||||
import org.apache.maven.archiva.repository.ManagedRepositoryContent;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryContentFactory;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryException;
|
|
||||||
import org.apache.maven.archiva.repository.layout.LayoutException;
|
|
||||||
import org.apache.maven.archiva.repository.metadata.MetadataTools;
|
|
||||||
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
|
|
||||||
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
|
|
||||||
import org.codehaus.plexus.registry.Registry;
|
|
||||||
import org.codehaus.plexus.registry.RegistryListener;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IndexContentConsumer - generic full file content indexing consumer.
|
|
||||||
*
|
|
||||||
* @version $Id$
|
|
||||||
* @plexus.component role="org.apache.maven.archiva.consumers.KnownRepositoryContentConsumer"
|
|
||||||
* role-hint="index-content"
|
|
||||||
* instantiation-strategy="per-lookup"
|
|
||||||
*/
|
|
||||||
public class IndexContentConsumer
|
|
||||||
extends AbstractMonitoredConsumer
|
|
||||||
implements KnownRepositoryContentConsumer, RegistryListener, Initializable
|
|
||||||
{
|
|
||||||
private Logger log = LoggerFactory.getLogger( IndexContentConsumer.class );
|
|
||||||
|
|
||||||
private static final String READ_CONTENT = "read_content";
|
|
||||||
|
|
||||||
private static final String INDEX_ERROR = "indexing_error";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="index-content"
|
|
||||||
*/
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="Text and XML file contents indexing"
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private ArchivaConfiguration configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private FileTypes filetypes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private RepositoryContentFactory repositoryFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement role-hint="lucene"
|
|
||||||
*/
|
|
||||||
private RepositoryContentIndexFactory indexFactory;
|
|
||||||
|
|
||||||
private List<String> includes = new ArrayList<String>();
|
|
||||||
|
|
||||||
private RepositoryContentIndex index;
|
|
||||||
|
|
||||||
private ManagedRepositoryContent repository;
|
|
||||||
|
|
||||||
private File repositoryDir;
|
|
||||||
|
|
||||||
public String getId()
|
|
||||||
{
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription()
|
|
||||||
{
|
|
||||||
return this.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPermanent()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getExcludes()
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getIncludes()
|
|
||||||
{
|
|
||||||
return this.includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beginScan( ManagedRepositoryConfiguration repo, Date whenGathered )
|
|
||||||
throws ConsumerException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.repository = repositoryFactory.getManagedRepositoryContent( repo.getId() );
|
|
||||||
this.repositoryDir = new File( repository.getRepoRoot() );
|
|
||||||
this.index = indexFactory.createFileContentIndex( repository.getRepository() );
|
|
||||||
}
|
|
||||||
catch ( RepositoryException e )
|
|
||||||
{
|
|
||||||
throw new ConsumerException( "Unable to start IndexContentConsumer: " + e.getMessage(), e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processFile( String path )
|
|
||||||
throws ConsumerException
|
|
||||||
{
|
|
||||||
if ( path.endsWith( "/" + MetadataTools.MAVEN_METADATA ) )
|
|
||||||
{
|
|
||||||
log.debug( "File is a metadata file. Not indexing." );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileContentRecord record = new FileContentRecord();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
record.setRepositoryId( this.repository.getId() );
|
|
||||||
record.setFilename( path );
|
|
||||||
|
|
||||||
// Test for possible artifact reference syntax.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ArtifactReference ref = repository.toArtifactReference( path );
|
|
||||||
ArchivaArtifact artifact = new ArchivaArtifact( ref );
|
|
||||||
artifact.getModel().setRepositoryId( repository.getId() );
|
|
||||||
record.setArtifact( artifact );
|
|
||||||
}
|
|
||||||
catch ( LayoutException e )
|
|
||||||
{
|
|
||||||
// Not an artifact.
|
|
||||||
}
|
|
||||||
|
|
||||||
index.modifyRecord( record );
|
|
||||||
}
|
|
||||||
catch ( RepositoryIndexException e )
|
|
||||||
{
|
|
||||||
triggerConsumerError( INDEX_ERROR, "Unable to index file contents: " + e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void completeScan()
|
|
||||||
{
|
|
||||||
/* do nothing */
|
|
||||||
}
|
|
||||||
|
|
||||||
public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue )
|
|
||||||
{
|
|
||||||
if ( ConfigurationNames.isRepositoryScanning( propertyName ) )
|
|
||||||
{
|
|
||||||
initIncludes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue )
|
|
||||||
{
|
|
||||||
/* do nothing */
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initIncludes()
|
|
||||||
{
|
|
||||||
includes.clear();
|
|
||||||
|
|
||||||
includes.addAll( filetypes.getFileTypePatterns( FileTypes.INDEXABLE_CONTENT ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initialize()
|
|
||||||
throws InitializationException
|
|
||||||
{
|
|
||||||
configuration.addChangeListener( this );
|
|
||||||
|
|
||||||
initIncludes();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,268 +0,0 @@
|
||||||
package org.apache.maven.archiva.consumers.lucene;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.apache.maven.archiva.consumers.AbstractMonitoredConsumer;
|
|
||||||
import org.apache.maven.archiva.consumers.ConsumerException;
|
|
||||||
import org.apache.maven.archiva.database.updater.DatabaseUnprocessedArtifactConsumer;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndex;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndexFactory;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryIndexException;
|
|
||||||
import org.apache.maven.archiva.indexer.bytecode.BytecodeRecord;
|
|
||||||
import org.apache.maven.archiva.model.ArchivaArtifact;
|
|
||||||
import org.apache.maven.archiva.repository.ManagedRepositoryContent;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryContentFactory;
|
|
||||||
import org.apache.maven.archiva.repository.RepositoryException;
|
|
||||||
|
|
||||||
import com.sun.org.apache.bcel.internal.classfile.ClassParser;
|
|
||||||
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
|
|
||||||
import com.sun.org.apache.bcel.internal.classfile.Method;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IndexJavaPublicMethodsConsumer
|
|
||||||
*
|
|
||||||
* <a href="mailto:oching@apache.org">Maria Odea Ching</a>
|
|
||||||
* @version $Id$
|
|
||||||
*
|
|
||||||
* @plexus.component role="org.apache.maven.archiva.database.updater.DatabaseUnprocessedArtifactConsumer"
|
|
||||||
* role-hint="index-public-methods"
|
|
||||||
* instantiation-strategy="per-lookup"
|
|
||||||
*/
|
|
||||||
public class IndexJavaPublicMethodsConsumer
|
|
||||||
extends AbstractMonitoredConsumer
|
|
||||||
implements DatabaseUnprocessedArtifactConsumer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="index-public-methods"
|
|
||||||
*/
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.configuration default-value="Index the java public methods for Full Text Search."
|
|
||||||
*/
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement role-hint="lucene"
|
|
||||||
*/
|
|
||||||
private RepositoryContentIndexFactory repoIndexFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @plexus.requirement
|
|
||||||
*/
|
|
||||||
private RepositoryContentFactory repoFactory;
|
|
||||||
|
|
||||||
private static final String CLASSES = "classes";
|
|
||||||
|
|
||||||
private static final String METHODS = "methods";
|
|
||||||
|
|
||||||
private List<String> includes = new ArrayList<String>();
|
|
||||||
|
|
||||||
public IndexJavaPublicMethodsConsumer()
|
|
||||||
{
|
|
||||||
includes.add( "jar" );
|
|
||||||
includes.add( "war" );
|
|
||||||
includes.add( "ear" );
|
|
||||||
includes.add( "zip" );
|
|
||||||
includes.add( "tar.gz" );
|
|
||||||
includes.add( "tar.bz2" );
|
|
||||||
includes.add( "car" );
|
|
||||||
includes.add( "sar" );
|
|
||||||
includes.add( "mar" );
|
|
||||||
includes.add( "rar" );
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beginScan()
|
|
||||||
{
|
|
||||||
// TODO Auto-generated method stubx
|
|
||||||
}
|
|
||||||
|
|
||||||
public void completeScan()
|
|
||||||
{
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getIncludedTypes()
|
|
||||||
{
|
|
||||||
return includes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processArchivaArtifact( ArchivaArtifact artifact )
|
|
||||||
throws ConsumerException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ManagedRepositoryContent repoContent =
|
|
||||||
repoFactory.getManagedRepositoryContent( artifact.getModel().getRepositoryId() );
|
|
||||||
File file = new File( repoContent.getRepoRoot(), repoContent.toPath( artifact ) );
|
|
||||||
|
|
||||||
if( file.getAbsolutePath().endsWith( ".jar" ) || file.getAbsolutePath().endsWith( ".war" ) ||
|
|
||||||
file.getAbsolutePath().endsWith( ".ear" ) || file.getAbsolutePath().endsWith( ".zip" ) ||
|
|
||||||
file.getAbsolutePath().endsWith( ".tar.gz" ) || file.getAbsolutePath().endsWith( ".tar.bz2" ) ||
|
|
||||||
file.getAbsolutePath().endsWith( ".car" ) || file.getAbsolutePath().endsWith( ".sar" ) ||
|
|
||||||
file.getAbsolutePath().endsWith( ".mar" ) || file.getAbsolutePath().endsWith( ".rar" ) )
|
|
||||||
{
|
|
||||||
if( file.exists() )
|
|
||||||
{
|
|
||||||
List<String> files = readFilesInArchive( file );
|
|
||||||
Map<String, List<String>> mapOfClassesAndMethods =
|
|
||||||
getPublicClassesAndMethodsFromFiles( file.getAbsolutePath(), files );
|
|
||||||
|
|
||||||
// NOTE: what about public variables? should these be indexed too?
|
|
||||||
RepositoryContentIndex bytecodeIndex = repoIndexFactory.createBytecodeIndex( repoContent.getRepository() );
|
|
||||||
|
|
||||||
artifact.getModel().setRepositoryId( repoContent.getId() );
|
|
||||||
|
|
||||||
BytecodeRecord bytecodeRecord = new BytecodeRecord();
|
|
||||||
bytecodeRecord.setFilename( file.getName() );
|
|
||||||
bytecodeRecord.setClasses( mapOfClassesAndMethods.get( CLASSES ) );
|
|
||||||
bytecodeRecord.setFiles( files );
|
|
||||||
bytecodeRecord.setMethods( mapOfClassesAndMethods.get( METHODS ) );
|
|
||||||
bytecodeRecord.setArtifact( artifact );
|
|
||||||
bytecodeRecord.setRepositoryId( repoContent.getId() );
|
|
||||||
bytecodeIndex.modifyRecord( bytecodeRecord );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( RepositoryException e )
|
|
||||||
{
|
|
||||||
throw new ConsumerException( "Can't run index cleanup consumer: " + e.getMessage() );
|
|
||||||
}
|
|
||||||
catch ( RepositoryIndexException e )
|
|
||||||
{
|
|
||||||
throw new ConsumerException( "Error encountered while adding artifact to index: " + e.getMessage() );
|
|
||||||
}
|
|
||||||
catch ( IOException e )
|
|
||||||
{
|
|
||||||
throw new ConsumerException( "Error encountered while getting file contents: " + e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription()
|
|
||||||
{
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId()
|
|
||||||
{
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPermanent()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> readFilesInArchive( File file )
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
ZipFile zipFile = new ZipFile( file );
|
|
||||||
List<String> files;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
files = new ArrayList<String>( zipFile.size() );
|
|
||||||
for ( Enumeration entries = zipFile.entries(); entries.hasMoreElements(); )
|
|
||||||
{
|
|
||||||
ZipEntry entry = (ZipEntry) entries.nextElement();
|
|
||||||
files.add( entry.getName() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
closeQuietly( zipFile );
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closeQuietly( ZipFile zipFile )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if ( zipFile != null )
|
|
||||||
{
|
|
||||||
zipFile.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( IOException e )
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isClass( String name )
|
|
||||||
{
|
|
||||||
return name.endsWith( ".class" ) && name.lastIndexOf( "$" ) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, List<String>> getPublicClassesAndMethodsFromFiles( String zipFile, List<String> files )
|
|
||||||
{
|
|
||||||
Map<String, List<String>> map = new HashMap<String, List<String>>();
|
|
||||||
List<String> methods = new ArrayList<String>();
|
|
||||||
List<String> classes = new ArrayList<String>();
|
|
||||||
|
|
||||||
for( String file : files )
|
|
||||||
{
|
|
||||||
if( isClass( file ) )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ClassParser parser = new ClassParser( zipFile, file );
|
|
||||||
JavaClass javaClass = parser.parse();
|
|
||||||
|
|
||||||
if( javaClass.isPublic() )
|
|
||||||
{
|
|
||||||
classes.add( javaClass.getClassName() );
|
|
||||||
}
|
|
||||||
|
|
||||||
Method[] methodsArr = javaClass.getMethods();
|
|
||||||
for( Method method : methodsArr )
|
|
||||||
{
|
|
||||||
if( method.isPublic() )
|
|
||||||
{
|
|
||||||
methods.add( method.getName() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( IOException e )
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.put( CLASSES, classes );
|
|
||||||
map.put( METHODS, methods );
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package org.apache.maven.archiva.consumers.lucene;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.maven.archiva.configuration.ArchivaConfiguration;
|
|
||||||
import org.apache.maven.archiva.configuration.Configuration;
|
|
||||||
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
|
|
||||||
import org.apache.maven.archiva.database.updater.DatabaseUnprocessedArtifactConsumer;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndexFactory;
|
|
||||||
import org.apache.maven.archiva.indexer.search.SearchResultLimits;
|
|
||||||
import org.apache.maven.archiva.indexer.search.SearchResults;
|
|
||||||
import org.apache.maven.archiva.model.ArchivaArtifact;
|
|
||||||
import org.apache.maven.archiva.model.ArchivaArtifactModel;
|
|
||||||
import org.codehaus.plexus.spring.PlexusInSpringTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @version
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class IndexJavaPublicMethodsConsumerTest
|
|
||||||
extends PlexusInSpringTestCase
|
|
||||||
{
|
|
||||||
DatabaseUnprocessedArtifactConsumer indexMethodsConsumer;
|
|
||||||
|
|
||||||
IndexJavaPublicMethodsCrossRepositorySearch searcher;
|
|
||||||
|
|
||||||
private RepositoryContentIndexFactory indexFactory;
|
|
||||||
|
|
||||||
public void setUp()
|
|
||||||
throws Exception
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
indexMethodsConsumer =
|
|
||||||
(DatabaseUnprocessedArtifactConsumer) lookup( DatabaseUnprocessedArtifactConsumer.class,
|
|
||||||
"index-public-methods" );
|
|
||||||
|
|
||||||
ManagedRepositoryConfiguration config = new ManagedRepositoryConfiguration();
|
|
||||||
config.setId( "test-repo" );
|
|
||||||
config.setLayout( "default" );
|
|
||||||
config.setLocation( getBasedir() + "/target/test-classes/test-repo" );
|
|
||||||
config.setName( "Test Repository" );
|
|
||||||
|
|
||||||
addRepoToConfiguration( "index-public-methods", config );
|
|
||||||
|
|
||||||
indexFactory = (RepositoryContentIndexFactory) lookup (RepositoryContentIndexFactory.class, "lucene" );
|
|
||||||
searcher = new IndexJavaPublicMethodsCrossRepositorySearch( config, indexFactory );
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addRepoToConfiguration( String configHint, ManagedRepositoryConfiguration repoConfiguration )
|
|
||||||
throws Exception
|
|
||||||
{
|
|
||||||
ArchivaConfiguration archivaConfiguration =
|
|
||||||
(ArchivaConfiguration) lookup( ArchivaConfiguration.class, configHint );
|
|
||||||
Configuration configuration = archivaConfiguration.getConfiguration();
|
|
||||||
configuration.removeManagedRepository( configuration.findManagedRepositoryById( repoConfiguration.getId() ) );
|
|
||||||
configuration.addManagedRepository( repoConfiguration );
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testJarPublicMethods()
|
|
||||||
throws Exception
|
|
||||||
{
|
|
||||||
ArchivaArtifact artifact =
|
|
||||||
createArtifact( "org.apache.archiva", "archiva-index-methods-jar-test", "1.0", "jar" );
|
|
||||||
indexMethodsConsumer.processArchivaArtifact( artifact );
|
|
||||||
|
|
||||||
List<String> selectedRepos = new ArrayList<String>();
|
|
||||||
selectedRepos.add( "test-repo" );
|
|
||||||
|
|
||||||
// search for class names
|
|
||||||
SearchResults results = searcher.searchForBytecode( "", selectedRepos, "FirstPackageApp", new SearchResultLimits( 0 ) );
|
|
||||||
assertEquals( 1, results.getTotalHits() );
|
|
||||||
|
|
||||||
results = searcher.searchForBytecode( "", selectedRepos, "SecondPackageApp", new SearchResultLimits( 0 ) );
|
|
||||||
assertEquals( 1, results.getTotalHits() );
|
|
||||||
|
|
||||||
// search for public methods
|
|
||||||
results = searcher.searchForBytecode( "", selectedRepos, "appMethodOne", new SearchResultLimits( 0 ) );
|
|
||||||
assertEquals( 1, results.getTotalHits() );
|
|
||||||
|
|
||||||
// should return only the overridding public method in SecondPackageApp
|
|
||||||
results = searcher.searchForBytecode( "", selectedRepos, "protectedMethod", new SearchResultLimits( 0 ) );
|
|
||||||
assertEquals( 1, results.getTotalHits() );
|
|
||||||
|
|
||||||
// should not return any private methods
|
|
||||||
results = searcher.searchForBytecode( "", selectedRepos, "privMethod", new SearchResultLimits( 0 ) );
|
|
||||||
assertEquals( 0, results.getTotalHits() );
|
|
||||||
|
|
||||||
// test for public variables?
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchivaArtifact createArtifact( String groupId, String artifactId, String version, String type )
|
|
||||||
{
|
|
||||||
ArchivaArtifactModel model = new ArchivaArtifactModel();
|
|
||||||
model.setGroupId( groupId );
|
|
||||||
model.setArtifactId( artifactId );
|
|
||||||
model.setVersion( version );
|
|
||||||
model.setType( type );
|
|
||||||
model.setRepositoryId( "test-repo" );
|
|
||||||
|
|
||||||
return new ArchivaArtifact( model );
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
package org.apache.maven.archiva.consumers.lucene;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.lucene.document.Document;
|
|
||||||
import org.apache.lucene.queryParser.ParseException;
|
|
||||||
import org.apache.lucene.queryParser.QueryParser;
|
|
||||||
import org.apache.lucene.search.Hits;
|
|
||||||
import org.apache.lucene.search.MultiSearcher;
|
|
||||||
import org.apache.lucene.search.Searchable;
|
|
||||||
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndex;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryContentIndexFactory;
|
|
||||||
import org.apache.maven.archiva.indexer.RepositoryIndexSearchException;
|
|
||||||
import org.apache.maven.archiva.indexer.bytecode.BytecodeHandlers;
|
|
||||||
import org.apache.maven.archiva.indexer.lucene.LuceneEntryConverter;
|
|
||||||
import org.apache.maven.archiva.indexer.lucene.LuceneQuery;
|
|
||||||
import org.apache.maven.archiva.indexer.lucene.LuceneRepositoryContentRecord;
|
|
||||||
import org.apache.maven.archiva.indexer.search.SearchResultLimits;
|
|
||||||
import org.apache.maven.archiva.indexer.search.SearchResults;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searcher used for testing purposes only.
|
|
||||||
*
|
|
||||||
* @version
|
|
||||||
*/
|
|
||||||
public class IndexJavaPublicMethodsCrossRepositorySearch
|
|
||||||
{
|
|
||||||
private Logger log = LoggerFactory.getLogger( IndexJavaPublicMethodsCrossRepositorySearch.class );
|
|
||||||
|
|
||||||
private ManagedRepositoryConfiguration localIndexedRepo;
|
|
||||||
|
|
||||||
private RepositoryContentIndexFactory indexFactory;
|
|
||||||
|
|
||||||
public IndexJavaPublicMethodsCrossRepositorySearch( ManagedRepositoryConfiguration localIndexedRepo, RepositoryContentIndexFactory indexFactory )
|
|
||||||
{
|
|
||||||
this.localIndexedRepo = localIndexedRepo;
|
|
||||||
this.indexFactory = indexFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SearchResults searchForBytecode( String principal, List<String> selectedRepos, String term,
|
|
||||||
SearchResultLimits limits ) throws ParseException
|
|
||||||
{
|
|
||||||
List<RepositoryContentIndex> indexes = new ArrayList<RepositoryContentIndex>();
|
|
||||||
indexes.add( indexFactory.createBytecodeIndex( localIndexedRepo ) );
|
|
||||||
|
|
||||||
QueryParser parser = new BytecodeHandlers().getQueryParser();
|
|
||||||
LuceneQuery query = new LuceneQuery( parser.parse( term ) );
|
|
||||||
SearchResults results = searchAll( query, limits, indexes );
|
|
||||||
results.getRepositories().add( localIndexedRepo );
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SearchResults searchAll( LuceneQuery luceneQuery, SearchResultLimits limits, List<RepositoryContentIndex> indexes )
|
|
||||||
{
|
|
||||||
org.apache.lucene.search.Query specificQuery = luceneQuery.getLuceneQuery();
|
|
||||||
|
|
||||||
SearchResults results = new SearchResults();
|
|
||||||
|
|
||||||
if ( indexes.isEmpty() )
|
|
||||||
{
|
|
||||||
// No point going any further.
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the converter
|
|
||||||
LuceneEntryConverter converter = null;
|
|
||||||
RepositoryContentIndex index = indexes.get( 0 );
|
|
||||||
converter = index.getEntryConverter();
|
|
||||||
|
|
||||||
// Process indexes into an array of Searchables.
|
|
||||||
List<Searchable> searchableList = toSearchables( indexes );
|
|
||||||
|
|
||||||
Searchable searchables[] = new Searchable[searchableList.size()];
|
|
||||||
searchableList.toArray( searchables );
|
|
||||||
|
|
||||||
MultiSearcher searcher = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Create a multi-searcher for looking up the information.
|
|
||||||
searcher = new MultiSearcher( searchables );
|
|
||||||
|
|
||||||
// Perform the search.
|
|
||||||
Hits hits = searcher.search( specificQuery );
|
|
||||||
|
|
||||||
int hitCount = hits.length();
|
|
||||||
|
|
||||||
// Now process the limits.
|
|
||||||
results.setLimits( limits );
|
|
||||||
results.setTotalHits( hitCount );
|
|
||||||
|
|
||||||
int fetchCount = limits.getPageSize();
|
|
||||||
int offset = ( limits.getSelectedPage() * limits.getPageSize() );
|
|
||||||
|
|
||||||
if ( limits.getSelectedPage() == SearchResultLimits.ALL_PAGES )
|
|
||||||
{
|
|
||||||
fetchCount = hitCount;
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Goto offset.
|
|
||||||
if ( offset < hitCount )
|
|
||||||
{
|
|
||||||
// only process if the offset is within the hit count.
|
|
||||||
for ( int i = 0; i <= fetchCount; i++ )
|
|
||||||
{
|
|
||||||
// Stop fetching if we are past the total # of available hits.
|
|
||||||
if ( offset + i >= hitCount )
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Document doc = hits.doc( offset + i );
|
|
||||||
LuceneRepositoryContentRecord record = converter.convert( doc );
|
|
||||||
results.addHit( record );
|
|
||||||
}
|
|
||||||
catch ( java.text.ParseException e )
|
|
||||||
{
|
|
||||||
log.error( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch ( IOException e )
|
|
||||||
{
|
|
||||||
log.error( e.getMessage() );
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if ( searcher != null )
|
|
||||||
{
|
|
||||||
searcher.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( IOException ie )
|
|
||||||
{
|
|
||||||
log.error( ie.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Searchable> toSearchables( List<RepositoryContentIndex> indexes )
|
|
||||||
{
|
|
||||||
List<Searchable> searchableList = new ArrayList<Searchable>();
|
|
||||||
for ( RepositoryContentIndex contentIndex : indexes )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
searchableList.add( contentIndex.getSearchable() );
|
|
||||||
}
|
|
||||||
catch ( RepositoryIndexSearchException e )
|
|
||||||
{
|
|
||||||
log.error( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return searchableList;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue