/*******************************************************************************
* Copyright (c) 2015 IBH SYSTEMS GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBH SYSTEMS GmbH - initial API and implementation
*******************************************************************************/
package org.eclipse.packagedrone.repo.adapter.unzip;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.activation.FileTypeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.channel.ArtifactInformation;
import org.eclipse.packagedrone.repo.channel.ChannelArtifactInformation;
import org.eclipse.packagedrone.repo.channel.ChannelId;
import org.eclipse.packagedrone.repo.channel.ChannelNotFoundException;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.channel.ChannelService.By;
import org.eclipse.packagedrone.repo.channel.ReadableChannel;
import org.eclipse.packagedrone.repo.channel.servlet.AbstractChannelServiceServlet;
import org.eclipse.packagedrone.repo.channel.util.DownloadHelper;
import org.eclipse.packagedrone.utils.io.IOConsumer;
import org.eclipse.scada.utils.str.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.ByteStreams;
public class UnzipServlet extends AbstractChannelServiceServlet
{
private static final MetaKey MK_MIME_TYPE = new MetaKey ( "mime", "type" );
private static final MetaKey MK_MVN_EXTENSION = new MetaKey ( "mvn", "extension" );
private static final long serialVersionUID = 1L;
private final static Logger logger = LoggerFactory.getLogger ( UnzipServlet.class );
private static final MetaKey MK_GROUP_ID = new MetaKey ( "mvn", "groupId" );
private static final MetaKey MK_ARTIFACT_ID = new MetaKey ( "mvn", "artifactId" );
private static final MetaKey MK_CLASSIFIER = new MetaKey ( "mvn", "classifier" );
private static final MetaKey MK_VERSION = new MetaKey ( "mvn", "version" );
private static final MetaKey MK_SNAPSHOT_VERSION = new MetaKey ( "mvn", "snapshotVersion" );
private FileTypeMap fileTypeMap;
@Override
public void init () throws ServletException
{
super.init ();
this.fileTypeMap = FileTypeMap.getDefaultFileTypeMap ();
}
@Override
protected void doGet ( final HttpServletRequest request, final HttpServletResponse response ) throws ServletException, IOException
{
String pathString = request.getPathInfo ();
if ( pathString == null )
{
handleNotFound ( "", response );
return;
}
if ( pathString.startsWith ( "/" ) )
{
pathString = pathString.substring ( 1 );
}
final String[] toks = pathString.split ( "/" );
if ( toks.length < 1 )
{
handleNotFound ( request.getPathInfo (), response );
return;
}
final LinkedList<String> path = new LinkedList<> ( Arrays.asList ( toks ) );
try
{
final String type = path.pop ();
switch ( type )
{
case "artifact":
handleArtifact ( request, response, path );
return;
case "newest":
handleNewest ( request, response, path );
return;
case "newestZip":
handleNewestZip ( request, response, path );
return;
case "newestByName":
handleNewestByName ( request, response, path );
return;
case "maven":
handleMaven ( request, response, path );
return;
default:
handleNotFoundError ( response, String.format ( "Unzip target type '%s' unknown.", type ) );
return;
}
}
catch ( final IllegalStateException e )
{
response.setStatus ( HttpServletResponse.SC_NOT_FOUND );
response.setContentType ( "text/plain" );
response.getWriter ().write ( e.getMessage () );
return;
}
catch ( final IllegalArgumentException e )
{
response.setStatus ( HttpServletResponse.SC_BAD_GATEWAY );
response.setContentType ( "text/plain" );
response.getWriter ().write ( e.getMessage () );
return;
}
}
protected void handleMaven ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
{
if ( path.isEmpty () )
{
throw new IllegalArgumentException ( String.format ( "The 'maven' type needs an addition type (latest, prefixed, ...)" ) );
}
final String mavenType = path.pop ();
if ( path.isEmpty () )
{
throw new IllegalArgumentException ( String.format ( "The 'maven' type needs the channel id or name after the sub-type" ) );
}
final String channelIdOrName = path.pop ();
try
{
getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {
final IOConsumer<MavenVersionedArtifact> consumer = ( artifact ) -> {
streamArtifactEntry ( request, response, channel, artifact.getArtifact (), path );
};
switch ( mavenType )
{
case "latest":
handleMavenLatest ( channel::getArtifacts, channel.getId (), path, false, consumer );
break;
case "latest-SNAPSHOT":
handleMavenLatest ( channel::getArtifacts, channel.getId (), path, true, consumer );
break;
case "prefixed":
handleMavenPrefixed ( channel::getArtifacts, channel.getId (), path, consumer );
break;
case "perfect":
handleMavenPerfect ( channel::getArtifacts, channel.getId (), path, consumer );
break;
default:
handleNotFoundError ( response, String.format ( "Unknown maven sub-type: %s", mavenType ) );
break;
}
} );
}
catch ( final ChannelNotFoundException e )
{
throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
}
}
protected void handleNewest ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
{
handleWithFilter ( "newest", request, response, path, null );
}
protected void handleNewestZip ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
{
handleWithFilter ( "newestZip", request, response, path, UnzipServlet::isZip );
}
protected void handleWithFilter ( final String type, final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path, final Predicate<ArtifactInformation> filter ) throws IOException
{
requirePathPrefix ( path, 1, String.format ( "The '%1$s' method requires at least one parameter: channel. e.g. /unzip/%1$s/<channelIdOrName>/path/to/file", type ) );
final String channelIdOrName = path.pop ();
final ChannelService service = getService ( request );
try
{
service.accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {
List<ArtifactInformation> arts = new ArrayList<> ( channel.getArtifacts () );
if ( filter != null )
{
arts = arts.stream ().filter ( filter ).collect ( Collectors.toList () );
}
if ( arts.isEmpty () )
{
throw new IllegalStateException ( String.format ( "Unable to find artifacts in channel '%s' (%s)", channelIdOrName, channel.getId () ) );
}
Collections.sort ( arts, Comparator.comparing ( ArtifactInformation::getCreationInstant ) );
final ArtifactInformation artifact = arts.get ( 0 );
logger.debug ( "Streaming artifact {} for channel {}", artifact.getId (), channelIdOrName );
streamArtifactEntry ( request, response, channel, artifact, path );
} );
}
catch ( final ChannelNotFoundException e )
{
throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
}
}
protected static void processArtifacts ( final String sourceName, final List<MavenVersionedArtifact> arts, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
{
if ( arts.isEmpty () )
{
throw new IllegalStateException ( String.format ( "Unable to find artifacts in %s", sourceName ) );
}
Collections.sort ( arts ); // by version
final MavenVersionedArtifact artifact = arts.get ( arts.size () - 1 ); // get last
logger.debug ( "Streaming artifact {} for {}", artifact.getArtifact (), sourceName );
consumer.accept ( artifact );
}
protected static void handleMavenPrefixed ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
{
requirePathPrefix ( path, 3, "The 'maven' method requires at least one parameter: channel. e.g. /unzip/maven/prefixed/<channelIdOrName>/<group.id>/<artifact.id>/<version>/path/to/file" );
final String groupId = path.pop ();
final String artifactId = path.pop ();
final String versionString = path.pop ();
final String versionPrefix;
final int idx = versionString.toLowerCase ().indexOf ( 'x' );
if ( idx > 0 )
{
// the x marks the spot
versionPrefix = versionString.substring ( 0, idx - 1 );
}
else
{
versionPrefix = versionString;
}
final boolean snapshot = versionString.endsWith ( "-SNAPSHOT" );
final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, snapshot, ( a ) -> a.toString ().startsWith ( versionPrefix ) );
if ( arts.isEmpty () )
{
// no result,
throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s, version: %s, snapshots: %s", groupId, artifactId, versionPrefix, snapshot ) );
}
processArtifacts ( String.format ( "maven artifact %s/%s/%s in channel %s", groupId, artifactId, versionString, channelId.getId () ), arts, consumer );
}
protected static void handleMavenPerfect ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
{
requirePathPrefix ( path, 3, "The 'maven' method requires at least one parameter: channel. e.g. /unzip/maven/perfect/<channelIdOrName>/<group.id>/<artifact.id>/<version>/path/to/file" );
final String groupId = path.pop ();
final String artifactId = path.pop ();
final String versionString = path.pop ();
final ComparableVersion v = new ComparableVersion ( versionString );
final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, true, ( a ) -> a.compareTo ( v ) == 0 );
if ( arts.isEmpty () )
{
// no result,
throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s, version: %s", groupId, artifactId, v ) );
}
processArtifacts ( String.format ( "maven artifact %s/%s/%s in channel %s", groupId, artifactId, versionString, channelId.getId () ), arts, consumer );
}
protected static void handleMavenLatest ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final boolean snapshot, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
{
requirePathPrefix ( path, 2, "The 'maven' method requires at least two parameters: groupId, artifactId. e.g. /unzip/maven/latest(-SNAPSHOT)/<channelIdOrName>/<group.id>/<artifact.id>/path/to/file" );
final String groupId = path.pop ();
final String artifactId = path.pop ();
final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, snapshot, null );
if ( arts.isEmpty () )
{
// no result,
throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s", groupId, artifactId ) );
}
processArtifacts ( String.format ( "latest maven artifact %s/%s in channel %s", groupId, artifactId, channelId.getId () ), arts, consumer );
}
/**
* Get a list of all relevant maven artifacts
*
* @param artifactsSupplier
* the supplier of artifacts
* @param groupId
* the group id to filter for, must not be <code>null
* @param artifactId
* the artifact id to filter for, must not be <code>null
* @param snapshot
* whether to consider snapshot versions of not
* @param versionFilter
* an optional version filter
* @return a list of all matching artifacts wrapped in
* {@link MavenVersionedArtifact}, if there is a snapshot version
* present, then the snapshot version of used as version
*/
protected static List<MavenVersionedArtifact> getMavenArtifacts ( final String channelId, final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final String groupId, final String artifactId, final boolean snapshot, final Predicate<ComparableVersion> versionFilter )
{
final List<MavenVersionedArtifact> arts = new ArrayList<> ();
for ( final ArtifactInformation ai : artifactsSupplier.get () )
{
if ( !isZip ( ai ) )
{
// if is is anot a zip, then this is not for the unzip plugin
continue;
}
// fetch meta data
final String mvnGroupId = ai.getMetaData ().get ( MK_GROUP_ID );
final String mvnArtifactId = ai.getMetaData ().get ( MK_ARTIFACT_ID );
final String classifier = ai.getMetaData ().get ( MK_CLASSIFIER );
final String mvnVersion = ai.getMetaData ().get ( MK_VERSION );
final String mvnSnapshotVersion = ai.getMetaData ().get ( MK_SNAPSHOT_VERSION );
if ( mvnGroupId == null || mvnArtifactId == null || mvnVersion == null )
{
// no GAV information
continue;
}
if ( classifier != null && !classifier.isEmpty () )
{
// no classifiers right now
continue;
}
if ( !mvnGroupId.equals ( groupId ) || !mvnArtifactId.equals ( artifactId ) )
{
// wrong group or artifact id
continue;
}
if ( !snapshot && ( mvnSnapshotVersion != null || mvnVersion.endsWith ( "-SNAPSHOT" ) ) )
{
// we are not looking for snapshots
continue;
}
final ComparableVersion v = parseVersion ( mvnVersion );
final ComparableVersion sv = parseVersion ( mvnSnapshotVersion );
if ( v == null )
{
// unable to parse v
continue;
}
if ( versionFilter == null )
{
// no filter, add it
arts.add ( new MavenVersionedArtifact ( sv != null ? sv : v, channelId, ai ) );
}
else if ( versionFilter.test ( v ) )
{
// filter matched, add it
arts.add ( new MavenVersionedArtifact ( sv != null ? sv : v, channelId, ai ) );
}
else if ( sv != null && versionFilter.test ( sv ) )
{
// we have a snapshot version and it matched, add it
arts.add ( new MavenVersionedArtifact ( sv, channelId, ai ) );
}
}
return arts;
}
private static ComparableVersion parseVersion ( final String version )
{
if ( version == null )
{
return null;
}
try
{
return new ComparableVersion ( version );
}
catch ( final Exception e )
{
logger.debug ( "Version not parsable: " + version, e );
return null;
}
}
/**
* Check if an artifact is a ZIP file
* <p>
* An artifact is a ZIP file if at least one of th following tests is true:
* <ul>
* <li>Its lower case name ends with <code>.zip</code></li>
* <li>The meta data field <code>mvn:extension</code> is set to
* <code>zip</code>
* <li>The meta data field <code>mime:type</code> is set to
* <code>application/zip</code>
* </ul>
* </p>
*
* @param artifact
* the artifact to check
* @return <code>true</code> if the artifact is a ZIP file,
* <code>false</code> otherwise
*/
protected static boolean isZip ( final ArtifactInformation artifact )
{
if ( artifact.getName ().toLowerCase ().endsWith ( ".zip" ) )
{
return true;
}
final String mdExtension = artifact.getMetaData ().get ( MK_MVN_EXTENSION );
if ( mdExtension != null && mdExtension.equalsIgnoreCase ( "zip" ) )
{
return true;
}
final String mdMime = artifact.getMetaData ().get ( MK_MIME_TYPE );
if ( mdMime != null && mdMime.equalsIgnoreCase ( "application/zip" ) )
{
return true;
}
return false;
}
protected void handleNewestByName ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
{
requirePathPrefix ( path, 2, "The 'newestByName' method requires at least two parameters: channel and name. e.g. /unzip/newestByName/<channelIdOrName>/<artifactName>/path/to/file" );
final String channelIdOrName = path.pop ();
final String name = path.pop ();
try
{
getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {
final List<ArtifactInformation> arts = channel.getArtifacts ().stream ().filter ( ai -> ai.getName ().equals ( name ) ).collect ( toList () );
if ( arts.isEmpty () )
{
throw new IllegalStateException ( String.format ( "Unable to find artifact with name '%s' in channel '%s' (%s)", name, channelIdOrName, channel.getId () ) );
}
Collections.sort ( arts, Comparator.comparing ( ArtifactInformation::getCreationInstant ) );
final ArtifactInformation artifact = arts.get ( 0 );
logger.debug ( "Streaming artifact {} for name {} in channel {}", artifact.getId (), name, channelIdOrName );
streamArtifactEntry ( request, response, channel, artifact, path );
} );
}
catch ( final ChannelNotFoundException e )
{
throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
}
}
private static void requirePathPrefix ( final LinkedList<String> path, final int pathPrefixCount, final String message )
{
if ( path.size () < pathPrefixCount )
{
throw new IllegalArgumentException ( message );
}
}
protected void handleArtifact ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
{
requirePathPrefix ( path, 2, "The 'artifact' method requires at least two parameters: channelId and artifactId. e.g. /unzip/artifact/<channelIdOrName>/<artifactId>/path/to/file" );
final String channelIdOrName = path.pop ();
try
{
getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {
final String artifactId = path.pop ();
final Optional<ChannelArtifactInformation> artifact = channel.getArtifact ( artifactId );
if ( !artifact.isPresent () )
{
handleNotFoundError ( response, String.format ( "Artifact '%s' could not be found", artifactId ) );
return;
}
streamArtifactEntry ( request, response, channel, artifact.get (), path );
} );
}
catch ( final ChannelNotFoundException e )
{
throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
}
}
protected void streamArtifactEntry ( final HttpServletRequest request, final HttpServletResponse response, final ReadableChannel channel, final ArtifactInformation artifact, final List<String> path ) throws IOException
{
final String localPath = StringHelper.join ( path, "/" );
if ( localPath.isEmpty () )
{
DownloadHelper.streamArtifact ( response, artifact, Optional.empty (), true, channel, null );
return;
}
// TODO: implement cache
if ( !channel.getContext ().stream ( artifact.getId (), ( stream ) -> {
try ( final ZipInputStream zis = new ZipInputStream ( stream ) )
{
ZipEntry entry;
while ( ( entry = zis.getNextEntry () ) != null )
{
if ( entry.getName ().equals ( localPath ) )
{
final String type = this.fileTypeMap.getContentType ( entry.getName () );
response.setContentType ( type );
response.setContentLengthLong ( entry.getSize () );
response.setDateHeader ( "Last-Modified", artifact.getCreationTimestamp ().getTime () );
ByteStreams.copy ( zis, response.getOutputStream () );
break;
}
}
if ( entry == null || !entry.getName ().equals ( localPath ) )
{
handleNotFoundError ( response, String.format ( "File entry '%s' could not be found in artifact '%s'", localPath, artifact.getId () ) );
}
}
} ) )
{
handleNotFoundError ( response, String.format ( "Artifact %s could not be found", artifact.getId () ) );
}
}
protected void handleNotFound ( final String path, final HttpServletResponse response ) throws IOException
{
handleNotFoundError ( response, String.format ( "Resource '%s' cound not be found", path ) );
}
protected void handleNotFoundError ( final HttpServletResponse response, final String message ) throws IOException
{
response.setStatus ( HttpServletResponse.SC_NOT_FOUND );
response.setContentType ( "text/plain" );
response.getWriter ().write ( message );
}
}