001/**
002 * Copyright 2014 Tampere University of Technology, Pori Department
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package core.tut.pori.http.parameters;
017
018import java.util.HashMap;
019import java.util.Iterator;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023
024import org.apache.commons.lang3.StringUtils;
025import org.apache.log4j.Logger;
026
027import core.tut.pori.http.Definitions;
028
029
030/**
031 * The default parser for limit parameters, 
032 * the syntax being: ?limits=START_ITEM-END_ITEM with a possible open ended limit for END_ITEM, e.g. ?limits=START_ITEM in this case the END_ITEM is automatically assigned to START_ITEM+DEFAULT_MAX_ITEMS-1
033 * 
034 * It is also possible to provide limits for a specific type with ?limits=TYPE;START_ITEM-END_ITEM or for types with ?limits=TYPE;START_ITEM-END_ITEM,TYPE;START_ITEM-END_ITEM, 
035 * the type limits can be retrieved using the parameterized getters, if no limits were provided for the type, default limits will be returned
036 * 
037 * It is also possible to provide both the default limit and type specific limits: ?limits=START_ITEM-END_ITEM,TYPE;START_ITEM-END_ITEM
038 * 
039 * Providing only the - character as a limit should as ?limits=- will be assumed to mean max items = 0, i.e. return nothing, this can also be used with typed limits, e.g. ?limits=0-1,TYPE;- to return no result for a specific type.
040 * 
041 * The limits have no specific processing order, and can also be given in any order in the limits clause. The given order is generally internally preserved, though conceptually, no such order exists.
042 */
043public final class Limits extends HTTPParameter{
044  /** the default HTTP parameter name */
045  public static final String PARAMETER_DEFAULT_NAME = "limits";
046  /** The default maximum amount of items if no last item is specified */
047  public static final int DEFAULT_MAX_ITEMS = Integer.MAX_VALUE;
048  private static final Logger LOGGER = Logger.getLogger(Limits.class);
049  private static final char SEPARATOR_LIMITS = '-';
050  private Map<String, TypeLimits> _typeLimits = new HashMap<>();  // typeName - typeLimits map
051
052  /**
053   * Initialize limits
054   * 
055   * @param startItem if &lt; 0, 0 will be used
056   * @param endItem the last item index (inclusively), if &lt;= startItem, startItem+DEFAULT_MAX_ITEM-1 will be used
057   */
058  public Limits(int startItem, int endItem){
059    _typeLimits.put(null, new TypeLimits(startItem, endItem, null));
060  }
061
062  /**
063   * create default limits
064   */
065  public Limits(){
066    _typeLimits.put(null, new TypeLimits(0, DEFAULT_MAX_ITEMS, null));
067  }
068  
069  /**
070   * 
071   * @return the limits as a limits string (query parameter value, without the parameter name and equals sign), or null if no limits
072   */
073  public String toLimitString(){
074    if(hasValues()){
075      StringBuilder value = new StringBuilder();
076      for(Iterator<Entry<String, TypeLimits>> iter = _typeLimits.entrySet().iterator();;){
077        Entry<String, TypeLimits> e = iter.next();
078        String type = e.getKey();
079        if(type != null){
080          value.append(type);
081          value.append(Definitions.SEPARATOR_URI_QUERY_TYPE_VALUE);
082        }
083        TypeLimits limits = e.getValue();
084        value.append(limits.getStartItem());
085        value.append(SEPARATOR_LIMITS);
086        value.append(limits.getEndItem());
087        if(iter.hasNext()){
088          value.append(Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES);
089        }else{
090          break;
091        }
092      }
093      return value.toString();
094    }else{
095      return null;
096    }
097  }
098
099  /**
100   * 
101   * @param startItem
102   * @param endItem
103   * @param typeName setting to null will replace the default (global) limits
104   */
105  public void setTypeLimits(int startItem, int endItem, String typeName){
106    _typeLimits.put(typeName, new TypeLimits(startItem, endItem, typeName));
107  }
108
109  /**
110   * 
111   * @return start item index
112   */
113  public int getStartItem(){
114    return _typeLimits.get(null).getStartItem();
115  }
116
117  /**
118   * @return the endItem index
119   */
120  public int getEndItem() {
121    return _typeLimits.get(null).getEndItem();
122  }
123
124  /**
125   * 
126   * @return maximum number of items
127   */
128  public int getMaxItems(){
129    return _typeLimits.get(null).getMaxItems(); // add one to return at least one result
130  }
131
132  /**
133   * 
134   * @param typeName
135   * @return start item index for the given type or if the type does not exist, the general start item
136   */
137  public int getStartItem(String typeName){
138    if(_typeLimits == null){
139      return getStartItem();
140    }
141    TypeLimits l = _typeLimits.get(typeName);
142    if(l == null){
143      return getStartItem();
144    }else{
145      return l.getStartItem();
146    }
147  }
148
149  /**
150   * 
151   * @param typeName
152   * @return the end item index or if the type is not defined, the general end item
153   */
154  public int getEndItem(String typeName){
155    if(_typeLimits == null){
156      return getEndItem();
157    }
158    TypeLimits l = _typeLimits.get(typeName);
159    if(l == null){
160      return getEndItem();
161    }else{
162      return l.getEndItem();
163    }
164  }
165  
166  /**
167   * Note: if no limits for the given typeName is found, this will return the default limits.
168   * Whether default limits were returned or not can be checked by callint TypeLimits.getTypeName(),
169   * which will return null on default limits.
170   * 
171   * @param typeName
172   * @return type limits for the requested type
173   */
174  public TypeLimits getTypeLimits(String typeName){
175    TypeLimits tl = _typeLimits.get(typeName);
176    if(tl == null){
177      return _typeLimits.get(null); // return the default limits
178    }else{
179      return tl;
180    }
181  }
182
183  /**
184   * 
185   * @param typeName
186   * @return maximum number of items for the given type or maximum number of items in general if the given type is not defined
187   */
188  public int getMaxItems(String typeName){
189    if(_typeLimits == null){
190      return getMaxItems();
191    }
192    TypeLimits l = _typeLimits.get(typeName);
193    if(l == null){
194      LOGGER.debug("Using defaults: No limits found for type: "+typeName);
195      return getMaxItems();
196    }else{
197      return l.getMaxItems(); // add one to return at least one result
198    }
199  }
200
201  @Override
202  public void initialize(List<String> parameterValues) throws IllegalArgumentException {
203    for(Iterator<String> iter = parameterValues.iterator();iter.hasNext();){
204      initializeLimits(iter.next());
205    }
206  }
207
208  @Override
209  public void initialize(String parameterValue) throws IllegalArgumentException {
210    initializeLimits(parameterValue);
211  }
212
213  /**
214   * 
215   * @param valueString 0, 0- or 0-1
216   * @return limits parsed from the given string
217   * @throws IllegalArgumentException on bad value string
218   */
219  private TypeLimits parseLimitValues(String valueString) throws IllegalArgumentException{
220    if(StringUtils.isBlank(valueString)){
221      LOGGER.debug("Detected null or empty value for parameter: "+getParameterName());
222      return null;
223    }
224    String[] parts = StringUtils.split(valueString, SEPARATOR_LIMITS);
225    try{
226      if(parts == null || parts.length > 2){  // probably limits=0-1-2-3 or similar
227        throw new IllegalArgumentException("Invalid "+getParameterName()+": "+valueString);
228      }else if(parts.length < 1){ // limits=- or similar
229        return new TypeLimits(-1, -1, null);
230      }else if(parts.length == 2){  // 0-1
231        return new TypeLimits(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), null);
232      }else{  // 0 (or 0-)
233        int item = Integer.parseInt(parts[0]);
234        return new TypeLimits(item, item, null);
235      }
236    }catch(NumberFormatException ex){
237      LOGGER.debug(ex, ex);
238      throw new IllegalArgumentException("Invalid "+getParameterName()+": "+valueString);
239    }
240  }
241
242  /**
243   * 
244   * @param limitString
245   * @throws IllegalArgumentException on bad input
246   */
247  private void initializeLimits(String limitString) throws IllegalArgumentException{
248    if(StringUtils.isBlank(limitString)){
249      LOGGER.debug("Detected null or empty value for parameter: "+getParameterName());
250      return;
251    }
252    String[] parts = StringUtils.split(limitString,Definitions.SEPARATOR_URI_QUERY_TYPE_VALUE);
253    if(parts.length == 1){  // 0, 0- or 0-1
254      TypeLimits limits = parseLimitValues(parts[0]);
255      _typeLimits.put(null, limits);
256    }else if(parts.length == 2){  // TYPE;0, TYPE;0- OR TYPE;0-1
257      TypeLimits limits = parseLimitValues(parts[1]);
258      limits._typeName = parts[0];
259      _typeLimits.put(parts[0], limits);
260    }else{
261      throw new IllegalArgumentException("Invalid "+getParameterName()+": "+limitString);
262    }
263  }
264
265  /**
266   * always true
267   */
268  @Override
269  public boolean hasValues() {
270    return true;
271  }
272  
273  /**
274   * @return there is no single value available, this always returns null
275   */
276  @Override
277  public Object getValue() {
278    return null;
279  }
280
281  /**
282   * A type specific limit clause.
283   */
284  public class TypeLimits{
285    private int _startItem = -1;
286    private int _endItem = -1;
287    private String _typeName = null;
288    
289    /**
290     * Create new TypeLimits. The limit validity will be checked:
291     * <ul>
292     *  <li>If end item is smaller than start item, this will throw an exception</li>
293     *  <li>if start item is less than 0, it will be set to 0</li>
294     *  <li>if end item is INTEGER MAX, it will be set to INTEGER_MAX - 1. This is because the end item is included in the limits, which would cause interval START - END cause limit overflow (INTEGER MAX + 1)</li>
295     *  <li>if end item is &lt; 0, the maximum item count is assumed to be 0 and getMaxItems() will return 0 regardless of the given start item</li>
296     * </ul>
297     * @param startItem
298     * @param endItem
299     * @param typeName
300     * @throws IllegalArgumentException 
301     */
302    public TypeLimits(int startItem, int endItem, String typeName) throws IllegalArgumentException{
303      _typeName = typeName;
304      if(startItem < 0){
305        LOGGER.debug("Start item < 0, setting value to 0.");
306        _startItem = 0;
307      }else{
308        _startItem = startItem;
309      }
310      if(endItem == Integer.MAX_VALUE){
311        LOGGER.debug("End item = "+Integer.MAX_VALUE+" setting value to MAX-1.");
312        _endItem = endItem-1;
313      }else if(endItem < _startItem && endItem >= 0){
314        throw new IllegalArgumentException("End item < start item.");
315      }else{
316        _endItem = endItem;
317      }
318    }
319
320    /**
321     * @return the startItem
322     */
323    public int getStartItem() {
324      return _startItem;
325    }
326
327    /**
328     * @return the endItem
329     */
330    public int getEndItem() {
331      return _endItem;
332    }
333
334    /**
335     * @return the typeName
336     */
337    public String getTypeName() {
338      return _typeName;
339    }
340    
341    /**
342     * 
343     * @return max item count
344     */
345    public int getMaxItems() {
346      if(_endItem < 0){
347        return 0;
348      }
349      return _endItem-_startItem+1;
350    }
351  } // class TypeLimits
352}