/*
* Copyright (c) 2014 the original author or authors
*
* Licensed 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 io.werval.runtime.mime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
import io.werval.api.mime.MediaRange;
import io.werval.util.Couple;
import io.werval.util.Strings;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
import static java.util.Collections.unmodifiableList;
import static io.werval.api.mime.MimeTypes.WILDCARD_MIMETYPE;
import static io.werval.util.Strings.EMPTY;
/**
* Media Range Instance.
*/
public final class MediaRangeInstance
implements MediaRange
{
private static final MediaRange WILDCARD = new MediaRangeInstance( "*", "*", 1D, emptyList() );
private static final Comparator<MediaRange> COMPARATOR = (range1, range2) ->
{
if( range1.qValue() == range2.qValue() )
{
return Integer.compare( range2.acceptExtensions().size(), range1.acceptExtensions().size() );
}
return Double.compare( range2.qValue(), range1.qValue() );
};
public static List<MediaRange> parseList( String pattern )
{
if( Strings.isEmpty( pattern ) )
{
return singletonList( WILDCARD );
}
List<MediaRange> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer( pattern, "," );
while( tokenizer.hasMoreTokens() )
{
result.add( parseSingle( tokenizer.nextToken().trim() ) );
}
sort( result, COMPARATOR );
return unmodifiableList( result );
}
public static MediaRange parseSingle( String pattern )
{
if( Strings.isEmpty( pattern ) )
{
return WILDCARD;
}
final String type;
final String subtype;
final StringTokenizer tokenizer;
if( pattern.contains( "/" ) )
{
StringTokenizer tokens = new StringTokenizer( pattern, "/" );
type = tokens.nextToken().trim();
String subtypeAndParams = tokens.nextToken();
tokenizer = new StringTokenizer( subtypeAndParams, ";" );
subtype = tokenizer.nextToken().trim();
}
else
{
if( !pattern.startsWith( "*" ) )
{
throw new IllegalArgumentException( "Invalid MediaRange pattern: " + pattern );
}
type = "*";
subtype = "*";
tokenizer = new StringTokenizer( pattern, ";" );
tokenizer.nextToken();
}
double qValue = 1D;
final List<Couple<String, String>> acceptExtensions = new ArrayList<>();
while( tokenizer.hasMoreTokens() )
{
String paramToken = tokenizer.nextToken();
if( paramToken.contains( "=" ) )
{
StringTokenizer paramTokenizer = new StringTokenizer( paramToken, "=" );
String key = paramTokenizer.nextToken().trim();
if( "q".equalsIgnoreCase( key ) )
{
qValue = Double.valueOf( paramTokenizer.nextToken().trim() );
}
else
{
acceptExtensions.add( Couple.of( key, paramTokenizer.nextToken().trim() ) );
}
}
else
{
acceptExtensions.add( Couple.of( paramToken.trim(), EMPTY ) );
}
}
return new MediaRangeInstance( type, subtype, qValue, acceptExtensions );
}
public static boolean accepts( List<MediaRange> ranges, String mimeType )
{
if( ranges.isEmpty() )
{
return true;
}
return ranges.stream().anyMatch( range -> range.accepts( mimeType ) );
}
public static String preferred( List<MediaRange> ranges, String... mimeTypes )
{
if( ranges.isEmpty() && ( mimeTypes == null || mimeTypes.length == 0 ) )
{
// No Accept header, no candidate mimetype, return */*
return WILDCARD_MIMETYPE;
}
if( ranges.isEmpty() )
{
// No Accept header, return first candidate mimetype
return mimeTypes[0];
}
if( mimeTypes == null || mimeTypes.length == 0 )
{
// No candidate mimetype, return preferred from Accept header
return ranges.get( 0 ).mimetype();
}
// Find best candidate mimetype among Accept header
int score = Integer.MAX_VALUE;
String preferred = null;
for( String mimeType : mimeTypes )
{
for( int idx = 0; idx < ranges.size(); idx++ )
{
if( ranges.get( idx ).accepts( mimeType ) )
{
if( idx < score )
{
score = idx;
preferred = ranges.get( idx ).mimetype();
break;
}
}
}
}
// If none found, return preferred from Accept header
return preferred == null ? ranges.get( 0 ).mimetype() : preferred;
}
private final String type;
private final String subtype;
private final double qValue;
private final List<Couple<String, String>> acceptExtensions;
private MediaRangeInstance(
String type, String subtype,
double qValue, List<Couple<String, String>> acceptExtensions
)
{
this.type = type;
this.subtype = subtype;
this.qValue = qValue;
this.acceptExtensions = acceptExtensions;
}
@Override
public String type()
{
return type;
}
@Override
public String subtype()
{
return subtype;
}
@Override
public String mimetype()
{
return type + "/" + subtype;
}
@Override
public double qValue()
{
return qValue;
}
@Override
public List<Couple<String, String>> acceptExtensions()
{
return unmodifiableList( acceptExtensions );
}
@Override
public boolean accepts( String mimeType )
{
return ( type + "/" + subtype ).equalsIgnoreCase( mimeType )
|| ( "*".equals( subtype ) && type.equalsIgnoreCase( mimeType.substring( mimeType.indexOf( "/" ) ) ) )
|| ( "*".equals( type ) && "*".equals( subtype ) );
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( type ).append( "/" ).append( subtype );
if( qValue != 1 )
{
sb.append( ";q=" ).append( qValue );
}
for( Couple<String, String> acceptExt : acceptExtensions )
{
sb.append( ";" ).append( acceptExt.left() );
if( acceptExt.hasRight() )
{
sb.append( "=" ).append( acceptExt.right() );
}
}
return sb.toString();
}
}