001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2017 Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.types;
020
021import java.text.ParseException;
022import java.util.HashMap;
023import java.util.Map;
024
025public class MediaRange extends MediaBase implements Comparable<MediaRange> {
026
027        public static final MediaRange ALL_MEDIA = new MediaRange("*", "*");
028
029        /**
030         * Create a new object with the given type and subtype.
031         * 
032         * @param type the top-level type
033         * @param subtype the subtype
034         */
035        public MediaRange(String type, String subtype) {
036                super(new MediaTypePair(type, subtype));
037        }
038        
039        /**
040         * Create a new object with the given type, subtype and parameters.
041         * 
042         * @param type the top-level type
043         * @param subtype the subtype
044         * @param parameters the parameters
045         */
046        public MediaRange(String type, String subtype, 
047                        Map<String, String> parameters) {
048                super(new MediaTypePair(type, subtype), parameters);
049        }
050
051        /**
052         * Create a new object with the given type and parameters.
053         * 
054         * @param type the type
055         * @param parameters the parameters
056         */
057        public MediaRange(MediaTypePair type, Map<String, String> parameters) {
058                super(type, parameters);
059        }
060
061        /* (non-Javadoc)
062         * @see java.lang.Comparable#compareTo(java.lang.Object)
063         */
064        @Override
065        public int compareTo(MediaRange other) {
066                float myQuality = 1;
067                String param = parameter("q");
068                if (param != null) {
069                        myQuality = Float.parseFloat(param);
070                }
071                float otherQuality = 1;
072                param = other.parameter("q");
073                if (param != null) {
074                        otherQuality = Float.parseFloat(param);
075                }
076                if (myQuality != otherQuality) {
077                        return -(int)Math.signum(myQuality - otherQuality);
078                }
079                
080                // Same or no quality, look for wildcards
081                if (!subtype().equals("*") && other.subtype().equals("*")) {
082                        return -1;
083                }
084                if (subtype().equals("*") && !other.subtype().equals("*")) {
085                        return 1;
086                }
087                if (!topLevelType().equals("*") 
088                                && other.topLevelType().equals("*")) {
089                        return -1;
090                }
091                if (topLevelType().equals("*")
092                                && !other.topLevelType().equals("*")) {
093                        return 1;
094                }
095                
096                // No wildcards or same type, look for number of parameters
097                return countParameters(other)- countParameters(this);
098        }
099
100        private static int countParameters(ParameterizedValue<?> value) {
101                return (int)value.parameters().keySet().stream()
102                                .filter(k -> !"q".equals(k)).count();
103        }
104        
105        /**
106         * Checks if the given media type falls within this range.
107         * 
108         * @param type the type to check
109         * @return the result
110         */
111        public boolean matches(MediaType type) {
112                if (!("*".equals(topLevelType()) 
113                                || topLevelType().equals(type.topLevelType()))) {
114                        // Top level is neither * nor match
115                        return false;
116                }
117                if (!("*".equals(subtype())
118                                || subtype().equals(type.subtype()))) {
119                        // Subtype is neither * nor match
120                        return false;
121                }
122                for (Map.Entry<String,String> e: parameters().entrySet()) {
123                        if ("q".equals(e.getKey())) {
124                                continue;
125                        }
126                        if (!type.parameters().containsKey(e.getKey())) {
127                                return false;
128                        }
129                        if (!type.parameter(e.getKey()).equals(e.getValue())) {
130                                return false;
131                        }
132                }
133                return true;
134        }
135        
136        /**
137         * Creates a new builder for a media type.
138         * 
139         * @return the builder
140         */
141        @SuppressWarnings("unchecked")
142        public static Builder builder() {
143                return new Builder();
144        }
145        
146        /**
147         * A builder for the (immutable) parameterized type.
148         */
149        public static class Builder 
150                extends ParameterizedValue.Builder<MediaRange, MediaTypePair> {
151
152                private Builder() {
153                        super(new MediaRange("*", "*", new HashMap<>()));
154                }
155                
156                /**
157                 * Sets the media range.
158                 * 
159                 * @param topLevelType the top level type
160                 * @param subtype the subtype
161                 * @return the builder for easy chaining
162                 */
163                public Builder setType(String topLevelType, String subtype) {
164                        setValue(new MediaTypePair(topLevelType, subtype));
165                        return this;
166                }
167        }
168        
169        public static class MediaRangeConverter
170                extends ParamValueConverterBase<MediaRange, MediaTypePair> {
171
172                public MediaRangeConverter() {
173                        super(new MediaTypePairConverter() {
174                                
175                                /**
176                                 * Work around buggy clients, notably `HttpUrlConnection` (see
177                                 * [JDK-8163921](http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8163921))
178                                 */
179                                @Override
180                                public MediaTypePair fromFieldValue(String text)
181                                        throws ParseException {
182                                        if ("*".equals(text)) {
183                                                return MediaTypePair.ALL_MEDIA;
184                                        }
185                                        return super.fromFieldValue(text);
186                                }
187                        },
188                        Converters.UNQUOTE_ONLY, MediaRange::new);
189                }
190        }
191
192}