mirror of https://github.com/apache/archiva.git
[MRM-1283] intercept requests for browsing and use the repository storage to determine values if not in metadata
git-svn-id: https://svn.apache.org/repos/asf/archiva/branches/MRM-1025@885072 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
c79221c997
commit
61745667ab
|
@ -104,29 +104,57 @@ public class DefaultMetadataResolver
|
||||||
|
|
||||||
public Collection<String> getRootNamespaces( String repoId )
|
public Collection<String> getRootNamespaces( String repoId )
|
||||||
{
|
{
|
||||||
// TODO: is this assumption correct? could a storage mech. actually know all references in a non-Maven scenario?
|
Collection<String> rootNamespaces = metadataRepository.getRootNamespaces( repoId );
|
||||||
// not passed to the storage mechanism as resolving references would require iterating all groups
|
|
||||||
return metadataRepository.getRootNamespaces( repoId );
|
// TODO: may want caching on this
|
||||||
|
Collection<String> storageRootNamespaces = storageResolver.getRootNamespaces( repoId );
|
||||||
|
if ( storageRootNamespaces != null && !storageRootNamespaces.equals( rootNamespaces ) )
|
||||||
|
{
|
||||||
|
// TODO: update the metadata repository
|
||||||
|
rootNamespaces = storageRootNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getNamespaces( String repoId, String namespace )
|
public Collection<String> getNamespaces( String repoId, String namespace )
|
||||||
{
|
{
|
||||||
// TODO: is this assumption correct? could a storage mech. actually know all references in a non-Maven scenario?
|
Collection<String> namespaces = metadataRepository.getNamespaces( repoId, namespace );
|
||||||
// not passed to the storage mechanism as resolving references would require iterating all groups
|
// TODO: may want caching on this
|
||||||
return metadataRepository.getNamespaces( repoId, namespace );
|
Collection<String> storageNamespaces = storageResolver.getNamespaces( repoId, namespace );
|
||||||
|
if ( storageNamespaces != null && !storageNamespaces.equals( namespaces ) )
|
||||||
|
{
|
||||||
|
// TODO: update the metadata repository
|
||||||
|
namespaces = storageNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getProjects( String repoId, String namespace )
|
public Collection<String> getProjects( String repoId, String namespace )
|
||||||
{
|
{
|
||||||
// TODO: is this assumption correct? could a storage mech. actually know all references in a non-Maven scenario?
|
Collection<String> projects = metadataRepository.getProjects( repoId, namespace );
|
||||||
// not passed to the storage mechanism as resolving references would require iterating all projects
|
// TODO: may want caching on this
|
||||||
return metadataRepository.getProjects( repoId, namespace );
|
Collection<String> storageProjects = storageResolver.getProjects( repoId, namespace );
|
||||||
|
if ( storageProjects != null && !storageProjects.equals( projects ) )
|
||||||
|
{
|
||||||
|
// TODO: update the metadata repository
|
||||||
|
projects = storageProjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getProjectVersions( String repoId, String namespace, String projectId )
|
public Collection<String> getProjectVersions( String repoId, String namespace, String projectId )
|
||||||
{
|
{
|
||||||
// TODO: is this assumption correct? could a storage mech. actually know all references in a non-Maven scenario?
|
Collection<String> projectVersions = metadataRepository.getProjectVersions( repoId, namespace, projectId );
|
||||||
// not passed to the storage mechanism as resolving references would require iterating all versions
|
// TODO: may want caching on this
|
||||||
return metadataRepository.getProjectVersions( repoId, namespace, projectId );
|
Collection<String> storageProjectVersions = storageResolver.getProjectVersions( repoId, namespace, projectId );
|
||||||
|
if ( storageProjectVersions != null && !storageProjectVersions.equals( projectVersions ) )
|
||||||
|
{
|
||||||
|
// TODO: update the metadata repository
|
||||||
|
projectVersions = storageProjectVersions;
|
||||||
|
}
|
||||||
|
return projectVersions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,8 @@ public interface RepositoryPathTranslator
|
||||||
File toFile( File basedir, String namespace, String projectId, String projectVersion, String filename );
|
File toFile( File basedir, String namespace, String projectId, String projectVersion, String filename );
|
||||||
|
|
||||||
String toPath( String namespace, String projectId, String projectVersion, String filename );
|
String toPath( String namespace, String projectId, String projectVersion, String filename );
|
||||||
|
|
||||||
|
File toFile( File basedir, String namespace, String projectId );
|
||||||
|
|
||||||
|
File toFile( File basedir, String namespace );
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,11 @@ package org.apache.archiva.metadata.repository.storage.maven2;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.archiva.metadata.model.ProjectMetadata;
|
import org.apache.archiva.metadata.model.ProjectMetadata;
|
||||||
|
@ -72,6 +75,24 @@ public class Maven2RepositoryMetadataResolver
|
||||||
|
|
||||||
private final static Logger log = LoggerFactory.getLogger( Maven2RepositoryMetadataResolver.class );
|
private final static Logger log = LoggerFactory.getLogger( Maven2RepositoryMetadataResolver.class );
|
||||||
|
|
||||||
|
private static final String METADATA_FILENAME = "maven-metadata.xml";
|
||||||
|
|
||||||
|
private static final FilenameFilter DIRECTORY_FILTER = new FilenameFilter()
|
||||||
|
{
|
||||||
|
public boolean accept( File dir, String name )
|
||||||
|
{
|
||||||
|
if ( name.startsWith( "." ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if ( !new File( dir, name ).isDirectory() )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public ProjectMetadata getProject( String repoId, String namespace, String projectId )
|
public ProjectMetadata getProject( String repoId, String namespace, String projectId )
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
@ -90,7 +111,7 @@ public class Maven2RepositoryMetadataResolver
|
||||||
if ( VersionUtil.isSnapshot( projectVersion ) )
|
if ( VersionUtil.isSnapshot( projectVersion ) )
|
||||||
{
|
{
|
||||||
File metadataFile =
|
File metadataFile =
|
||||||
pathTranslator.toFile( basedir, namespace, projectId, projectVersion, "maven-metadata.xml" );
|
pathTranslator.toFile( basedir, namespace, projectId, projectVersion, METADATA_FILENAME );
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MavenRepositoryMetadata metadata = MavenRepositoryMetadataReader.read( metadataFile );
|
MavenRepositoryMetadata metadata = MavenRepositoryMetadataReader.read( metadataFile );
|
||||||
|
@ -285,21 +306,157 @@ public class Maven2RepositoryMetadataResolver
|
||||||
|
|
||||||
public Collection<String> getRootNamespaces( String repoId )
|
public Collection<String> getRootNamespaces( String repoId )
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
File dir = getRepositoryBasedir( repoId );
|
||||||
|
|
||||||
|
String[] files = dir.list( DIRECTORY_FILTER );
|
||||||
|
return files != null ? Arrays.asList( files ) : Collections.<String>emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getNamespaces( String repoId, String namespace )
|
private File getRepositoryBasedir( String repoId )
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
ManagedRepositoryConfiguration repositoryConfiguration =
|
||||||
|
archivaConfiguration.getConfiguration().findManagedRepositoryById( repoId );
|
||||||
|
|
||||||
|
return new File( repositoryConfiguration.getLocation() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getNamespaces( String repoId, String namespace )
|
||||||
|
{
|
||||||
|
File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
|
||||||
|
|
||||||
|
// scan all the directories which are potential namespaces. Any directories known to be projects are excluded
|
||||||
|
Collection<String> namespaces = new ArrayList<String>();
|
||||||
|
File[] files = dir.listFiles( DIRECTORY_FILTER );
|
||||||
|
if ( files != null )
|
||||||
|
{
|
||||||
|
for ( File file : files )
|
||||||
|
{
|
||||||
|
if ( !isProject( file ) )
|
||||||
|
{
|
||||||
|
namespaces.add( file.getName() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return namespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getProjects( String repoId, String namespace )
|
public Collection<String> getProjects( String repoId, String namespace )
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace );
|
||||||
|
|
||||||
|
// scan all directories in the namespace, and only include those that are known to be projects
|
||||||
|
Collection<String> projects = new ArrayList<String>();
|
||||||
|
File[] files = dir.listFiles( DIRECTORY_FILTER );
|
||||||
|
if ( files != null )
|
||||||
|
{
|
||||||
|
for ( File file : files )
|
||||||
|
{
|
||||||
|
if ( isProject( file ) )
|
||||||
|
{
|
||||||
|
projects.add( file.getName() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getProjectVersions( String repoId, String namespace, String projectId )
|
public Collection<String> getProjectVersions( String repoId, String namespace, String projectId )
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException();
|
File dir = pathTranslator.toFile( getRepositoryBasedir( repoId ), namespace, projectId );
|
||||||
|
|
||||||
|
// all directories in a project directory can be considered a version
|
||||||
|
Collection<String> projectVersions = new ArrayList<String>();
|
||||||
|
String[] files = dir.list( DIRECTORY_FILTER );
|
||||||
|
return files != null ? Arrays.asList( files ) : Collections.<String>emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isProject( File dir )
|
||||||
|
{
|
||||||
|
// if a metadata file is present, check if this is the "artifactId" directory, marking it as a project
|
||||||
|
MavenRepositoryMetadata metadata = readMetadata( dir );
|
||||||
|
if ( metadata != null && dir.getName().equals( metadata.getArtifactId() ) )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if metadata is missing, scan directories for a valid project version subdirectory, meaning this must be a
|
||||||
|
// project directory
|
||||||
|
File[] files = dir.listFiles( DIRECTORY_FILTER );
|
||||||
|
if ( files != null )
|
||||||
|
{
|
||||||
|
for ( File file : files )
|
||||||
|
{
|
||||||
|
if ( isProjectVersion( file ) )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isProjectVersion( File dir )
|
||||||
|
{
|
||||||
|
final String artifactId = dir.getParentFile().getName();
|
||||||
|
final String projectVersion = dir.getName();
|
||||||
|
|
||||||
|
// if a metadata file is present, check if this is the "version" directory, marking it as a project version
|
||||||
|
MavenRepositoryMetadata metadata = readMetadata( dir );
|
||||||
|
if ( metadata != null && projectVersion.equals( metadata.getVersion() ) )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if metadata is missing, check if there is a POM artifact file to ensure it is a version directory
|
||||||
|
File[] files;
|
||||||
|
if ( VersionUtil.isSnapshot( projectVersion ) )
|
||||||
|
{
|
||||||
|
files = dir.listFiles( new FilenameFilter()
|
||||||
|
{
|
||||||
|
public boolean accept( File dir, String name )
|
||||||
|
{
|
||||||
|
if ( name.startsWith( artifactId + "-" ) && name.endsWith( ".pom" ) )
|
||||||
|
{
|
||||||
|
String v = name.substring( artifactId.length() + 1, name.length() - 4 );
|
||||||
|
v = VersionUtil.getBaseVersion( v );
|
||||||
|
if ( v.equals( projectVersion ) )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
final String pomFile = artifactId + "-" + projectVersion + ".pom";
|
||||||
|
files = dir.listFiles( new FilenameFilter()
|
||||||
|
{
|
||||||
|
public boolean accept( File dir, String name )
|
||||||
|
{
|
||||||
|
return pomFile.equals( name );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
return files != null && files.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MavenRepositoryMetadata readMetadata( File directory )
|
||||||
|
{
|
||||||
|
MavenRepositoryMetadata metadata = null;
|
||||||
|
File metadataFile = new File( directory, METADATA_FILENAME );
|
||||||
|
if ( metadataFile.exists() )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
metadata = MavenRepositoryMetadataReader.read( metadataFile );
|
||||||
|
}
|
||||||
|
catch ( XMLException e )
|
||||||
|
{
|
||||||
|
// ignore missing or invalid metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,14 +42,52 @@ public class Maven2RepositoryPathTranslator
|
||||||
{
|
{
|
||||||
StringBuilder path = new StringBuilder();
|
StringBuilder path = new StringBuilder();
|
||||||
|
|
||||||
path.append( formatAsDirectory( namespace ) ).append( PATH_SEPARATOR );
|
appendNamespaceAndProject( path, namespace, projectId );
|
||||||
path.append( projectId ).append( PATH_SEPARATOR );
|
|
||||||
path.append( projectVersion ).append( PATH_SEPARATOR );
|
path.append( projectVersion ).append( PATH_SEPARATOR );
|
||||||
path.append( filename );
|
path.append( filename );
|
||||||
|
|
||||||
return path.toString();
|
return path.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String toPath( String namespace )
|
||||||
|
{
|
||||||
|
StringBuilder path = new StringBuilder();
|
||||||
|
|
||||||
|
appendNamespace( path, namespace );
|
||||||
|
|
||||||
|
return path.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toPath( String namespace, String projectId )
|
||||||
|
{
|
||||||
|
StringBuilder path = new StringBuilder();
|
||||||
|
|
||||||
|
appendNamespaceAndProject( path, namespace, projectId );
|
||||||
|
|
||||||
|
return path.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendNamespaceAndProject( StringBuilder path, String namespace, String projectId )
|
||||||
|
{
|
||||||
|
appendNamespace( path, namespace );
|
||||||
|
path.append( projectId ).append( PATH_SEPARATOR );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendNamespace( StringBuilder path, String namespace )
|
||||||
|
{
|
||||||
|
path.append( formatAsDirectory( namespace ) ).append( PATH_SEPARATOR );
|
||||||
|
}
|
||||||
|
|
||||||
|
public File toFile( File basedir, String namespace, String projectId )
|
||||||
|
{
|
||||||
|
return new File( basedir, toPath( namespace, projectId ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public File toFile( File basedir, String namespace )
|
||||||
|
{
|
||||||
|
return new File( basedir, toPath( namespace ) );
|
||||||
|
}
|
||||||
|
|
||||||
private String formatAsDirectory( String directory )
|
private String formatAsDirectory( String directory )
|
||||||
{
|
{
|
||||||
return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
|
return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
|
||||||
|
|
|
@ -220,6 +220,75 @@ public class Maven2RepositoryMetadataResolverTest
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testGetRootNamespaces()
|
||||||
|
{
|
||||||
|
assertEquals( Arrays.asList( "com", "org" ), resolver.getRootNamespaces( TEST_REPO_ID ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetNamespaces()
|
||||||
|
{
|
||||||
|
assertEquals( Arrays.asList( "example" ), resolver.getNamespaces( TEST_REPO_ID, "com" ) );
|
||||||
|
assertEquals( Arrays.asList( "test" ), resolver.getNamespaces( TEST_REPO_ID, "com.example" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getNamespaces( TEST_REPO_ID, "com.example.test" ) );
|
||||||
|
|
||||||
|
assertEquals( Arrays.asList( "apache" ), resolver.getNamespaces( TEST_REPO_ID, "org" ) );
|
||||||
|
assertEquals( Arrays.asList( "archiva", "maven" ), resolver.getNamespaces( TEST_REPO_ID, "org.apache" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getNamespaces( TEST_REPO_ID, "org.apache.archiva" ) );
|
||||||
|
assertEquals( Arrays.asList( "plugins", "shared" ),
|
||||||
|
resolver.getNamespaces( TEST_REPO_ID, "org.apache.maven" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(),
|
||||||
|
resolver.getNamespaces( TEST_REPO_ID, "org.apache.maven.plugins" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(),
|
||||||
|
resolver.getNamespaces( TEST_REPO_ID, "org.apache.maven.shared" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetProjects()
|
||||||
|
{
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getProjects( TEST_REPO_ID, "com" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getProjects( TEST_REPO_ID, "com.example" ) );
|
||||||
|
assertEquals( Arrays.asList( "incomplete-metadata", "invalid-pom", "malformed-metadata", "missing-metadata" ),
|
||||||
|
resolver.getProjects( TEST_REPO_ID, "com.example.test" ) );
|
||||||
|
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getProjects( TEST_REPO_ID, "org" ) );
|
||||||
|
assertEquals( Arrays.asList( "apache" ), resolver.getProjects( TEST_REPO_ID, "org.apache" ) );
|
||||||
|
assertEquals( Arrays.asList( "archiva", "archiva-base", "archiva-common", "archiva-modules", "archiva-parent" ),
|
||||||
|
resolver.getProjects( TEST_REPO_ID, "org.apache.archiva" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(), resolver.getProjects( TEST_REPO_ID, "org.apache.maven" ) );
|
||||||
|
assertEquals( Collections.<String>emptyList(),
|
||||||
|
resolver.getProjects( TEST_REPO_ID, "org.apache.maven.plugins" ) );
|
||||||
|
assertEquals( Arrays.asList( "maven-downloader" ),
|
||||||
|
resolver.getProjects( TEST_REPO_ID, "org.apache.maven.shared" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetProjectVersions()
|
||||||
|
{
|
||||||
|
assertEquals( Arrays.asList( "1.0-SNAPSHOT" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "com.example.test", "incomplete-metadata" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.0-SNAPSHOT" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "com.example.test", "malformed-metadata" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.0-SNAPSHOT" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "com.example.test", "missing-metadata" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.0" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "com.example.test", "invalid-pom" ) );
|
||||||
|
|
||||||
|
assertEquals( Arrays.asList( "4", "5-SNAPSHOT" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache", "apache" ) );
|
||||||
|
|
||||||
|
assertEquals( Arrays.asList( "1.2.1" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.archiva", "archiva" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.2.1" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.archiva", "archiva-base" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.2.1" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.archiva", "archiva-common" ) );
|
||||||
|
assertEquals( Arrays.asList( "1.2.1" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.archiva", "archiva-modules" ) );
|
||||||
|
assertEquals( Arrays.asList( "3" ),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.archiva", "archiva-parent" ) );
|
||||||
|
|
||||||
|
assertEquals( Collections.<String>emptyList(),
|
||||||
|
resolver.getProjectVersions( TEST_REPO_ID, "org.apache.maven.shared", "maven-downloader" ) );
|
||||||
|
}
|
||||||
|
|
||||||
private void assertMailingList( MailingList mailingList, String name, String archive, String post, String subscribe,
|
private void assertMailingList( MailingList mailingList, String name, String archive, String post, String subscribe,
|
||||||
String unsubscribe, List<String> otherArchives, boolean allowPost )
|
String unsubscribe, List<String> otherArchives, boolean allowPost )
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue