001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.objecttools;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.lang.reflect.*;
023import java.util.*;
024
025import org.apache.juneau.*;
026
027/**
028 * POJO model sorter.
029 *
030 * <p>
031 *    This class is designed to sort arrays and collections of maps or beans.
032 * </p>
033 *
034 * <h5 class='section'>Example:</h5>
035 * <p class='bjava'>
036 *    MyBean[] <jv>arrayOfBeans</jv> = ...;
037 *    ObjectSorter <jv>sorter</jv> = ObjectSorter.<jsm>create</jsm>();
038 *
039 *    <jc>// Returns a list of beans sorted accordingly.</jc>
040 *    List&lt;MyBean&gt; <jv>result</jv> = <jv>sorter</jv>.run(<jv>arrayOfBeans</jv>, <js>"foo,bar-"</js>);
041 * </p>
042 * <p>
043 *    The tool can be used against the following data types:
044 * </p>
045 * <ul>
046 *    <li>Arrays/collections of maps or beans.
047 * </ul>
048 * <p>
049 *    The arguments are a simple comma-delimited list of property names optionally suffixed with <js>'+'</js> and <js>'-'</js> to
050 *    denote ascending/descending order.
051 * </p>
052 *
053 * <h5 class='section'>See Also:</h5><ul>
054 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ObjectTools">Object Tools</a>
055
056 * </ul>
057 */
058@SuppressWarnings({ "unchecked", "rawtypes" })
059public class ObjectSorter implements ObjectTool<SortArgs> {
060   private static class SortEntry implements Comparable {
061      Object o;
062      ClassMeta<?> cm;
063      BeanSession bs;
064
065      Object sortVal;
066      boolean isDesc;
067
068      SortEntry(BeanSession bs, Object o) {
069         this.o = o;
070         this.bs = bs;
071         this.cm = bs.getClassMetaForObject(o);
072      }
073
074      @Override
075      public int compareTo(Object o) {
076         if (isDesc)
077            return cmp(((SortEntry)o).sortVal, this.sortVal);
078         return cmp(this.sortVal, ((SortEntry)o).sortVal);
079      }
080
081      void setSort(String sortCol, boolean isDesc) {
082         this.isDesc = isDesc;
083
084         if (cm == null)
085            sortVal = null;
086         else if (cm.isMap())
087            sortVal = ((Map)o).get(sortCol);
088         else if (cm.isBean())
089            sortVal = bs.toBeanMap(o).get(sortCol);
090         else
091            sortVal = null;
092      }
093   }
094
095   /**
096    * Default reusable searcher.
097    */
098   public static final ObjectSorter DEFAULT = new ObjectSorter();
099
100   /**
101    * Static creator.
102    *
103    * @return A new {@link ObjectSorter} object.
104    */
105   public static ObjectSorter create() {
106      return new ObjectSorter();
107   }
108
109   @Override /* Overridden from ObjectTool */
110   public Object run(BeanSession session, Object input, SortArgs args) {
111      if (input == null)
112         return null;
113
114      // If sort or view isn't empty, then we need to make sure that all entries in the
115      // list are maps.
116      var sort = args.getSort();
117
118      if (sort.isEmpty())
119         return input;
120
121      var type = session.getClassMetaForObject(input);
122
123      if (! type.isCollectionOrArray())
124         return input;
125
126      var l = (ArrayList<SortEntry>)null;
127
128      if (type.isArray()) {
129         var size = Array.getLength(input);
130         l = listOfSize(size);
131         for (var i = 0; i < size; i++)
132            l.add(new SortEntry(session, Array.get(input, i)));
133      } else /* isCollection() */ {
134         var c = (Collection)input;
135         l = listOfSize(c.size());
136         List<SortEntry> l2 = l;
137         c.forEach(x -> l2.add(new SortEntry(session, x)));
138      }
139
140      // We reverse the list and sort last to first.
141      var columns = toList(sort.keySet());
142      Collections.reverse(columns);
143
144      var l3 = l;
145      columns.forEach(c -> {
146         final boolean isDesc = sort.get(c);
147         l3.forEach(se -> se.setSort(c, isDesc));
148         Collections.sort(l3);
149      });
150
151      var l2 = listOfSize(l.size());
152      l.forEach(x -> l2.add(x.o));
153
154      return l2;
155   }
156
157   /**
158    * Convenience method for executing the sorter.
159    *
160    * @param <R> The return type.
161    * @param input The input.
162    * @param sortArgs The sort arguments.  See {@link SortArgs} for format.
163    * @return A list of maps/beans matching the
164    */
165   public <R> List<R> run(Object input, String sortArgs) {
166      var r = run(BeanContext.DEFAULT_SESSION, input, SortArgs.create(sortArgs));
167      if (r instanceof List r2)
168         return r2;
169      if (r instanceof Collection r2)
170         return toList(r2);
171      if (isArray(r))
172         return l((R[])r);
173      return null;
174   }
175}