1   package com.atlassian.user.impl.hibernate.search.query;
2   
3   import com.atlassian.user.Entity;
4   import com.atlassian.user.EntityException;
5   import com.atlassian.user.impl.RepositoryException;
6   import com.atlassian.user.impl.hibernate.DefaultHibernateExternalEntity;
7   import com.atlassian.user.impl.hibernate.DefaultHibernateGroup;
8   import com.atlassian.user.impl.hibernate.DefaultHibernateUser;
9   import com.atlassian.user.impl.hibernate.repository.HibernateRepository;
10  import com.atlassian.user.repository.RepositoryIdentifier;
11  import com.atlassian.user.search.DefaultSearchResult;
12  import com.atlassian.user.search.SearchResult;
13  import com.atlassian.user.search.page.DefaultPager;
14  import com.atlassian.user.search.query.*;
15  import net.sf.hibernate.Criteria;
16  import net.sf.hibernate.HibernateException;
17  import net.sf.hibernate.Session;
18  import net.sf.hibernate.expression.*;
19  import org.springframework.orm.hibernate.SessionFactoryUtils;
20  
21  import java.util.*;
22  
23  /**
24   * Handles {@link Query} objects on a {@link HibernateRepository}.
25   */
26  public final class HibernateEntityQueryParser implements EntityQueryParser
27  {
28      private final RepositoryIdentifier identifier;
29      private final HibernateRepository repository;
30      private final QueryValidator queryValidator = new QueryValidator();
31  
32      public HibernateEntityQueryParser(RepositoryIdentifier identifier, HibernateRepository repository)
33      {
34          this.identifier = identifier;
35          this.repository = repository;
36      }
37  
38      public SearchResult findUsers(Query query) throws EntityException
39      {
40          queryValidator.assertValid(query);
41          return parseQuery(query);
42      }
43  
44      public SearchResult findGroups(Query query) throws EntityException
45      {
46          queryValidator.assertValid(query);
47          return parseQuery(query);
48      }
49  
50      public SearchResult findUsers(Query query, QueryContext context) throws EntityException
51      {
52          if (!context.contains(identifier))
53              return null;
54  
55          queryValidator.assertValid(query);
56          return parseQuery(query);
57      }
58  
59      public SearchResult findGroups(Query query, QueryContext context) throws EntityException
60      {
61          if (!context.contains(identifier))
62              return null;
63  
64          queryValidator.assertValid(query);
65          return parseQuery(query);
66      }
67  
68      private MatchMode getMatchMode(String matchingRule)
69      {
70          if (matchingRule.equals(TermQuery.SUBSTRING_CONTAINS))
71          {
72              return MatchMode.ANYWHERE;
73          }
74          if (matchingRule.equals(TermQuery.SUBSTRING_ENDS_WITH))
75          {
76              return MatchMode.END;
77          }
78          if (matchingRule.equals(TermQuery.SUBSTRING_STARTS_WITH))
79          {
80              return MatchMode.START;
81          }
82          return MatchMode.EXACT;
83      }
84  
85      private String identifyProperty(TermQuery q)
86      {
87          if (q instanceof UserNameTermQuery)
88              return "name";
89          else if (q instanceof EmailTermQuery)
90              return "email";
91          else if (q instanceof FullNameTermQuery)
92              return "fullName";
93          else if (q instanceof GroupNameTermQuery)
94              return "name";
95          else if (q instanceof GroupsOfUserTwoTermQuery)
96              return "entity";
97          return null;
98      }
99  
100     /**
101      * Converts a {@link BooleanQuery} object into an appropriate Hibernate {@link Criteria}, performs the search, and
102      * returns a {@link SearchResult}.
103      */
104     private SearchResult parseQuery(Query query) throws EntityException
105     {
106         //1. establish the Object being queried.
107         Session session = SessionFactoryUtils.getSession(repository.getSessionFactory(), true);
108         Query definingQuery = (query instanceof BooleanQuery) ? identifyDefiningQuery((BooleanQuery) query) : query;
109 
110         List result;
111         try
112         {
113             Criteria baseCriteria = getBaseCriteria(definingQuery, session);
114             baseCriteria = identifyAndAddSearchCriteria(query, definingQuery, baseCriteria);
115             baseCriteria.addOrder(Order.asc("name")); // sort results explicitly by username rather than leaving this up to the mood of the database
116 
117             result = baseCriteria.list();
118         }
119         catch (HibernateException e)
120         {
121             throw new RepositoryException(e);
122         }
123 
124         return new DefaultSearchResult(new DefaultPager(result), identifier.getName());
125     }
126 
127     private Criteria identifyAndAddSearchCriteria(Query q, Query definingQuery, Criteria baseCriteria)
128         throws EntityQueryException, HibernateException
129     {
130         if (q instanceof BooleanQuery)
131             return addSearchCriteria((BooleanQuery) q, definingQuery, baseCriteria);
132         else
133         {
134             return addSearchCriteria((TermQuery) q, baseCriteria);
135         }
136     }
137 
138     private Criteria addSearchCriteria(BooleanQuery q, Query definingQuery, Criteria baseCriteria)
139         throws EntityQueryException, HibernateException
140     {
141         /**
142          * Membership queries are performed on associations - i.e. subCriteria, and need to be handled on a case by case basis.
143          */
144         if (definingQuery instanceof MembershipQuery)
145         {
146             return addMembershipSearchCriteria(q, baseCriteria);
147         }
148 
149         Junction junction = identifyAndOrJunction(q);
150         baseCriteria.add(junction);
151 
152         for (Query query : q.getQueries())
153         {
154             if (query instanceof BooleanQuery)
155             {
156                 addSearchCriteria((BooleanQuery) query, definingQuery, baseCriteria);
157             }
158             else if (query instanceof TermQuery)
159             {
160                 junction.add(getQueryExpression((TermQuery) query));
161             }
162             else
163                 throw new EntityQueryException("Unknown query type: [" + query.getClass().getName() + "]");
164         }
165 
166         return baseCriteria;
167     }
168 
169     private Junction identifyAndOrJunction(BooleanQuery q)
170     {
171         Junction junction;
172         if (q.isAND())
173             junction = Expression.conjunction();
174         else
175             junction = Expression.disjunction();
176 
177         return junction;
178     }
179 
180     private Criteria addMembershipSearchCriteria(BooleanQuery q, Criteria baseCriteria) throws HibernateException
181     {
182         if (q instanceof GroupsOfUserTwoTermQuery)
183         {
184             addGroupsOfUserSearchCriteria(q, baseCriteria);
185         }
186         else if (q instanceof GroupsOfExternalEntityTwoTermQuery)
187         {
188             addGroupsOfExternalEntitySearchCriteria(q, baseCriteria);
189         }
190 
191         return baseCriteria;
192     }
193 
194     private void addGroupsOfUserSearchCriteria(BooleanQuery q, Criteria baseCriteria) throws HibernateException
195     {
196         UserNameTermQuery userNameQuery = ((GroupsOfUserTwoTermQuery) q).getUserNameTermQuery();
197         GroupNameTermQuery groupNameQuery = ((GroupsOfUserTwoTermQuery) q).getGroupNameTermQuery();
198 
199         if (groupNameQuery.getTerm().equals(TermQuery.WILDCARD))
200         {
201             //do nothing, there is no specific group query and we defer to the username term below
202         }
203         else if (groupNameQuery.isMatchingSubstring())
204         {
205             baseCriteria.add(getLikeExpression("name", groupNameQuery, false));
206         }
207         else
208         {
209             baseCriteria.add(new EqExpression("name", groupNameQuery.getTerm(), false));
210         }
211 
212         if (userNameQuery.isMatchingSubstring())
213         {
214             baseCriteria.createCriteria("localMembers").add(getLikeExpression("name", userNameQuery, false));
215         }
216         else
217         {
218             baseCriteria.createCriteria("localMembers").add(new EqExpression("name", userNameQuery.getTerm(), false));
219         }
220     }
221 
222     private Criterion getLikeExpression(String entityAttribute, TermQuery termQuery, boolean caseInsensitive)
223     {
224         if (caseInsensitive)
225             return Expression.ilike(entityAttribute, termQuery.getTerm(), getMatchMode(termQuery.getMatchingRule()));
226         else
227             return Expression.like(entityAttribute, termQuery.getTerm(), getMatchMode(termQuery.getMatchingRule()));
228     }
229 
230     private void addGroupsOfExternalEntitySearchCriteria(BooleanQuery q, Criteria baseCriteria)
231         throws HibernateException
232     {
233         ExternalEntityNameTermQuery nameQuery =
234             ((GroupsOfExternalEntityTwoTermQuery) q).getExternalEntityNameTermQuery();
235         GroupNameTermQuery groupQuery = ((GroupsOfExternalEntityTwoTermQuery) q).getGroupNameTermQuery();
236 
237         if (groupQuery.getTerm().equals(TermQuery.WILDCARD))
238         {
239             //do nothing, there is no specific group query and we defer to the username term below
240         }
241         else if (groupQuery.isMatchingSubstring())
242         {
243             baseCriteria.add(getLikeExpression("name", groupQuery, false));
244         }
245         else
246         {
247             baseCriteria.add(new EqExpression("name", groupQuery.getTerm(), false));
248         }
249 
250         if (nameQuery.isMatchingSubstring())
251         {
252             baseCriteria.createCriteria("externalMembers").add(getLikeExpression("name", nameQuery, false));
253         }
254         else
255         {
256             baseCriteria.createCriteria("externalMembers").add(new EqExpression("name", nameQuery.getTerm(), false));
257         }
258     }
259 
260     private Criteria addSearchCriteria(TermQuery q, Criteria baseCriteria)
261     {
262         Criterion expression = getQueryExpression(q);
263         baseCriteria.add(expression);
264         return baseCriteria;
265     }
266 
267     private Criterion getQueryExpression(TermQuery termQuery)
268     {
269         String hqlField = identifyProperty(termQuery);
270 
271         if (termQuery.isMatchingSubstring())
272         {
273             return getLikeExpression(hqlField, termQuery, true);
274         }
275         else
276         {
277             return new EqExpression(hqlField, termQuery.getTerm(), true);
278         }
279     }
280 
281     private static final Map<Class<? extends Query>, Class<? extends Entity>> QUERY_TYPE_CLASSES = new LinkedHashMap<Class<? extends Query>, Class<? extends Entity>>();
282 
283     static {
284         QUERY_TYPE_CLASSES.put(UserQuery.class, DefaultHibernateUser.class);
285         QUERY_TYPE_CLASSES.put(GroupQuery.class, DefaultHibernateGroup.class);
286         QUERY_TYPE_CLASSES.put(UsersInGroupTwoTermQuery.class, DefaultHibernateUser.class);
287         QUERY_TYPE_CLASSES.put(GroupsOfUserTwoTermQuery.class, DefaultHibernateGroup.class);
288         QUERY_TYPE_CLASSES.put(GroupsOfExternalEntityTwoTermQuery.class, DefaultHibernateGroup.class);
289         QUERY_TYPE_CLASSES.put(ExternalEntitiesInGroupTwoTermQuery.class, DefaultHibernateExternalEntity.class);
290     }
291 
292     private Criteria getBaseCriteria(Query query, Session session)
293     {
294         for (Map.Entry<Class<? extends Query>, Class<? extends Entity>> entry : QUERY_TYPE_CLASSES.entrySet())
295         {
296             if (entry.getKey().isInstance(query))
297             {
298                 return session.createCriteria(entry.getValue());
299             }
300         }
301         return null;
302     }
303 
304     /**
305      * The 'defining query' lets us know which EntityClass we are querying.
306      */
307     private Query identifyDefiningQuery(BooleanQuery q)
308     {
309         if (q instanceof MembershipQuery)
310             return q;
311         for (Query query : q.getQueries())
312         {
313             if (query instanceof TermQuery)
314                 return query;
315             if (query instanceof BooleanQuery)
316                 return identifyDefiningQuery((BooleanQuery) query);
317         }
318         return null;
319     }
320 }