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.LinkedHashMap;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.commons.lang3.StringUtils;
027import org.apache.log4j.Logger;
028
029import core.tut.pori.dao.SQLSelectBuilder.OrderDirection;
030import core.tut.pori.http.Definitions;
031
032
033/**
034 * <p>The default parser for sort options.</p>
035 * 
036 * <p>The basic syntax is: ?PARAMETER=OrderDirection, e.g. ?sort=&lt;</p>
037 * 
038 * <p>It is also possible to give type specific order clauses, e.g. ?sort=TYPE1;&lt;,TYPE2;&gt;</p>
039 * 
040 * <p>Also, you can provide specific element name, e.g. ?sort=&lt;ELEMENT_NAME or ?sort=TYPE1;&gt;ELEMENT_NAME</p>
041 * 
042 * <p>These to variations can be combined, e.g. ?sort=&lt;,TYPE;&gt; to provide different sort option for a specific type</p>
043 * 
044 * <p>Note: the parses has no way of knowing if the given TYPE or ELEMENT_NAME value is valid, you should check the values yourself</p>
045 *
046 * <p>Any number of sort clauses (SortOption) can be given, and they will be processed in-order by the default SQL and SOLR query builders.</p>
047 */
048public final class SortOptions extends HTTPParameter{
049  /** String for ascending sort */
050  public static final String ORDER_DIRECTION_ASCENDING = "<";
051  /** String for descending sort */
052  public static final String ORDER_DIRECTION_DESCENDING = ">";
053  /** Recommended name for the sort parameter */
054  public static final String PARAMETER_DEFAULT_NAME = "sort_by";
055  private static final Logger LOGGER = Logger.getLogger(SortOptions.class);
056  private static final int ORDER_DIRECTION_ASCENDING_LENGTH = ORDER_DIRECTION_ASCENDING.length();
057  private static final int ORDER_DIRECTION_DESCENDING_LENGTH = ORDER_DIRECTION_DESCENDING.length();
058  private Map<String, Set<Option>> _typeOrderDirections = null; // type-name, sort option map
059  
060  /**
061   * 
062   * @param options
063   * @param typeName
064   * @return the sort options or null if none
065   */
066  public static Set<Option> getSortOptions(SortOptions options, String typeName){
067    if(options == null){
068      return null;
069    }else{
070      return options.getSortOptions(typeName);
071    }
072  }
073  
074  /**
075   * @param typeName
076   * @return set of sort options, the iteration order of the map is predictable, i.e. the values are iterated in the order given by the user. Or, null if none provided by the user.
077   */
078  public Set<Option> getSortOptions(String typeName){
079    if(_typeOrderDirections == null){
080      return null;
081    }else{
082      Set<Option> o = _typeOrderDirections.get(typeName);
083      if(o == null){
084        return getDefaultSortOptions();
085      }else{
086        return o;
087      }
088    }
089  }
090  
091  /**
092   * 
093   * @param option
094   */
095  public void addSortOption(Option option){
096    if(option == null){
097      LOGGER.debug("Ignored null option.");
098      return;
099    }
100    Set<Option> options = null;
101    String typename = option.getTypeName();
102    if(_typeOrderDirections == null){
103      _typeOrderDirections = new HashMap<>();
104      _typeOrderDirections.put(typename, (options = new LinkedHashSet<>()));
105    }else{
106      options = _typeOrderDirections.get(typename);
107      if(options == null){
108        _typeOrderDirections.put(typename, (options = new LinkedHashSet<>()));
109      }
110    }
111    options.add(option);
112  }
113  
114  /**
115   * 
116   * @return non-typed sort options, or null if not given by the user
117   */
118  public Set<Option> getDefaultSortOptions(){
119    return (_typeOrderDirections == null ? null : _typeOrderDirections.get(null));
120  }
121
122  @Override
123  public void initialize(List<String> parameterValues) throws IllegalArgumentException {
124    for(Iterator<String> iter = parameterValues.iterator();iter.hasNext();){
125      initializeSort(iter.next());
126    }
127  }
128
129  @Override
130  public void initialize(String parameterValue) throws IllegalArgumentException {
131    if(StringUtils.isBlank(parameterValue)){
132      _typeOrderDirections = null;
133      return;
134    }
135    initializeSort(parameterValue);
136  }
137  
138  /**
139   * 
140   * @param valueString
141   * @throws IllegalArgumentException on bad valueString
142   */
143  private void initializeSort(String valueString) throws IllegalArgumentException{
144    if(StringUtils.isBlank(valueString)){
145      LOGGER.debug("Detected null or empty value for parameter: "+getParameterName());
146      return;
147    }
148    String[] parts = StringUtils.split(valueString, Definitions.SEPARATOR_URI_QUERY_TYPE_VALUE);
149    Option option = null;
150    if(parts.length > 2){ // bad input
151      throw new IllegalArgumentException("Invalid value "+valueString+" for paramater "+getParameterName());
152    }else if(parts.length == 2){  //TYPE;ELEMENT, TYPE;<ELEMENT, TYPE;>ELEMENT, TYPE;> or TYPE;<
153      option = parseOption(parts[1]);
154      option._typeName = parts[0];
155    }else{  // <, >, <ELEMENT, >ELEMENT or ELEMENT
156      option = parseOption(parts[0]);
157    }
158    
159    Set<Option> set = null;
160    if(_typeOrderDirections == null){
161      _typeOrderDirections = new LinkedHashMap<>();
162    }else{
163      set = _typeOrderDirections.get(option._typeName);
164    }
165    
166    if(set == null){
167      set = new LinkedHashSet<>();
168      _typeOrderDirections.put(option._typeName, set);
169    }
170    
171    set.add(option);
172  }
173  
174  /**
175   * helper method for parsin the sort string
176   * 
177   * @param sortString ELEMENT, <ELEMENT, >ELEMENT, > or <
178   * @return the parsed option
179   * @throws IllegalArgumentException on bad input
180   */
181  private Option parseOption(String sortString) throws IllegalArgumentException{
182    if(StringUtils.isBlank(sortString)){
183      throw new IllegalArgumentException("Invalid value "+sortString+" for paramater "+getParameterName());
184    }
185    
186    OrderDirection od = null;
187    String sortElementName = null;
188    if(sortString.startsWith(ORDER_DIRECTION_ASCENDING)){
189      od = OrderDirection.ASCENDING;
190      if(sortString.length() > ORDER_DIRECTION_ASCENDING_LENGTH){ // <ELEMENT_NAME
191        sortElementName = sortString.substring(ORDER_DIRECTION_ASCENDING_LENGTH);
192      } // only <
193    }else if(sortString.startsWith(ORDER_DIRECTION_DESCENDING)){
194      od = OrderDirection.DESCENDING;
195      if(sortString.length() > ORDER_DIRECTION_DESCENDING_LENGTH){  //> ELEMENT_NAME
196        sortElementName = sortString.substring(ORDER_DIRECTION_DESCENDING_LENGTH);
197      } // only >
198    }else{  // must be the element name
199      sortElementName = sortString;
200    }
201    return new Option(sortElementName,od, null);
202  }
203
204  /**
205   * Note: getValue() may return null even though this method returns null, if user has provided a specific
206   * sorting option, which is NOT a default option
207   * 
208   * @return true if user has provided values, but not if only default value is given
209   */
210  @Override
211  public boolean hasValues() {
212    return (_typeOrderDirections != null);
213  }
214
215  /**
216   * @return the general sort order
217   */
218  @Override
219  public Set<Option> getValue() {
220    return getDefaultSortOptions();
221  }
222  
223  /**
224   * A single sort option.
225   */
226  public static class Option{
227    private String _elementName = null;
228    private OrderDirection _orderDirection = OrderDirection.DESCENDING;
229    private String _typeName = null;
230    
231    /**
232     * 
233     * @param elementName
234     * @param direction
235     * @param typeName
236     */
237    public Option(String elementName, OrderDirection direction, String typeName){
238      _elementName = elementName;
239      _typeName = typeName;
240      if(direction == null){
241        LOGGER.debug("null "+OrderDirection.class.toString()+", using default: "+_orderDirection.name());
242      }else{
243        _orderDirection = direction;
244      }
245    }
246
247    /**
248     * @return the elementName
249     */
250    public String getElementName() {
251      return _elementName;
252    }
253
254    /**
255     * @return the orderDirection
256     */
257    public OrderDirection getOrderDirection() {
258      return _orderDirection;
259    }
260
261    /**
262     * @return the typeName
263     */
264    public String getTypeName() {
265      return _typeName;
266    }
267
268    @Override
269    public int hashCode() {
270      final int prime = 31;
271      int result = 1;
272      result = prime * result + ((_elementName == null) ? 0 : _elementName.hashCode());
273      result = prime * result + ((_orderDirection == null) ? 0 : _orderDirection.hashCode());
274      result = prime * result + ((_typeName == null) ? 0 : _typeName.hashCode());
275      return result;
276    }
277
278    @Override
279    public boolean equals(Object obj) {
280      if (this == obj)
281        return true;
282      if (obj == null)
283        return false;
284      if (getClass() != obj.getClass())
285        return false;
286      Option other = (Option) obj;
287      if (_elementName == null) {
288        if (other._elementName != null)
289          return false;
290      } else if (!_elementName.equals(other._elementName))
291        return false;
292      if (_orderDirection != other._orderDirection)
293        return false;
294      if (_typeName == null) {
295        if (other._typeName != null)
296          return false;
297      } else if (!_typeName.equals(other._typeName))
298        return false;
299      return true;
300    }
301  } //  class Option
302}