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 service.tut.pori.contentanalysis;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Date;
021import java.util.EnumSet;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.Set;
029import java.util.UUID;
030
031import org.apache.commons.lang3.ArrayUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.log4j.Logger;
034import org.apache.solr.client.solrj.response.QueryResponse;
035import org.apache.solr.client.solrj.response.UpdateResponse;
036import org.apache.solr.common.SolrException;
037import org.springframework.beans.factory.annotation.Autowired;
038
039import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
040import service.tut.pori.contentanalysis.CAContentCore.Visibility;
041import service.tut.pori.contentanalysis.MediaObject.ConfirmationStatus;
042import service.tut.pori.contentanalysis.MediaObject.MediaObjectType;
043import core.tut.pori.dao.SimpleSolrTemplate;
044import core.tut.pori.dao.SolrDAO;
045import core.tut.pori.dao.SolrQueryBuilder;
046import core.tut.pori.dao.SQLSelectBuilder.OrderDirection;
047import core.tut.pori.dao.filter.AbstractQueryFilter;
048import core.tut.pori.dao.filter.AndQueryFilter;
049import core.tut.pori.dao.filter.AndSubQueryFilter;
050import core.tut.pori.dao.filter.OrQueryFilter;
051import core.tut.pori.dao.filter.OrSubQueryFilter;
052import core.tut.pori.dao.filter.RangeQueryFilter;
053import core.tut.pori.http.parameters.DataGroups;
054import core.tut.pori.http.parameters.Limits;
055import core.tut.pori.http.parameters.QueryParameter;
056import core.tut.pori.http.parameters.SortOptions;
057import core.tut.pori.http.parameters.SortOptions.Option;
058import core.tut.pori.users.UserIdentity;
059import core.tut.pori.utils.MediaUrlValidator.MediaType;
060
061
062/**
063 * The DAO for storing and retrieving media objects.
064 * 
065 * This class can also be used to retrieve media object suggestions. Note that if you wish to associate media objects with photos you must use PhotoDAO, not this class.
066 *
067 */
068public class MediaObjectDAO extends SolrDAO{
069  private static final String BEAN_ID_SOLR_SERVER = "solrServerMediaObjects";
070  private static final int[] DEFAULT_CONFIRMATION_STATUS_LIST = ConfirmationStatus.toIntArray(EnumSet.of(ConfirmationStatus.CANDIDATE, ConfirmationStatus.USER_CONFIRMED));
071  private static final SortOptions DEFAULT_SORT_OPTIONS;
072  static{
073    DEFAULT_SORT_OPTIONS = new SortOptions();
074    DEFAULT_SORT_OPTIONS.addSortOption(new SortOptions.Option(SOLR_FIELD_ID, OrderDirection.ASCENDING, Definitions.ELEMENT_MEDIA_OBJECTLIST));
075  }
076  private static final Visibility DEFAULT_VISIBILITY = Visibility.PRIVATE;
077  private static final String[] FIELDS_DATA_GROUP_DEFAULTS = new String[]{
078    Definitions.SOLR_FIELD_BACKEND_ID, Definitions.SOLR_FIELD_CONFIDENCE, SOLR_FIELD_ID, Definitions.SOLR_FIELD_NAME, Definitions.SOLR_FIELD_CREATOR_OBJECT_ID, Definitions.SOLR_FIELD_RANK, Definitions.SOLR_FIELD_UPDATED, Definitions.SOLR_FIELD_VALUE, // members
079    Definitions.SOLR_FIELD_USER_ID, // user identity
080    Definitions.SOLR_FIELD_SERVICE_ID, // service type
081    Definitions.SOLR_FIELD_MEDIA_OBJECT_TYPE, // object type
082    Definitions.SOLR_FIELD_MEDIA_TYPE, // media type
083    Definitions.SOLR_FIELD_VISUAL_SHAPE_VALUE, // for visual shape
084    Definitions.SOLR_FIELD_VISUAL_SHAPE_TYPE,  // for visual shape
085    Definitions.SOLR_FIELD_STATUS // confirmation status
086    };
087  private static final String[] FIELDS_DATA_GROUP_TIMECODES = new String[]{Definitions.SOLR_FIELD_TIMECODES};
088  private static final String[] FIELDS_RESOLVE_OBJECT_IDS = new String[]{SOLR_FIELD_ID, Definitions.SOLR_FIELD_BACKEND_ID, Definitions.SOLR_FIELD_USER_ID, Definitions.SOLR_FIELD_CREATOR_OBJECT_ID};
089  private static final String[] FIELDS_UPDATE = new String[]{Definitions.SOLR_FIELD_USER_ID, SOLR_FIELD_ID, Definitions.SOLR_FIELD_BACKEND_ID, Definitions.SOLR_FIELD_CREATOR_OBJECT_ID};
090  private static final Logger LOGGER = Logger.getLogger(MediaObjectDAO.class);
091  @Autowired
092  private KeywordsDAO _keywordsDAO = null;
093  @Autowired
094  private PhotoDAO _photoDAO = null;
095  
096  /**
097   * @param objects
098   * @return true on success
099   */
100  public boolean insert(MediaObjectList objects){
101    if(MediaObjectList.isEmpty(objects)){
102      LOGGER.debug("Ignored empty object list.");
103      return true;
104    }
105    Date updated = new Date();
106    List<MediaObject> v = objects.getMediaObjects();
107    for(Iterator<MediaObject> vIter = v.iterator(); vIter.hasNext();){  // make sure all objects has updated timestamps
108      MediaObject o = vIter.next();
109      if(o.getUpdated() == null){
110        o.setUpdated(updated);
111      }
112      if(o.getMediaObjectId() != null){
113        LOGGER.warn("Replacing Media object Id exist for media object with existing id, id: "+o.getMediaObjectId());
114      }
115      String mediaObjectId = UUID.randomUUID().toString();
116      o.setMediaObjectId(mediaObjectId);
117      if(o.getObjectId() == null){
118        LOGGER.debug("No objectId given, using media object id.");
119        o.setObjectId(mediaObjectId);
120      }
121      if(o.getOwnerUserId() == null){
122        LOGGER.debug("Adding media object without owner.");
123      }
124      if(o.getVisibility() == null){
125        LOGGER.debug("No visibility value given, using default: "+DEFAULT_VISIBILITY.name());
126        o.setVisibility(DEFAULT_VISIBILITY);
127      }
128    }
129    if(!MediaObjectList.isValid(objects)){  // check validity after ids have been generated
130      LOGGER.warn("Tried to add invalid object list.");
131      return false;
132    }
133    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER); 
134    UpdateResponse response = template.addBeans(v); 
135    if(response.getStatus() == SolrException.ErrorCode.UNKNOWN.code){
136      return true;
137    }else{
138      LOGGER.warn("Failed to add media objects.");
139      return false;
140    }
141  }
142  
143  /**
144   * helper methods for resolving object ids (backendId, mediaObjectId, objectId, userId)
145   * 
146   * @param backendIdObjectIdMap
147   * @param mediaObjectIds
148   * @param objects
149   */
150  private void resolveObjectIds(Map<Integer, HashSet<String>> backendIdObjectIdMap, Set<String> mediaObjectIds, List<MediaObject> objects){
151    boolean noMediaObjectIds = mediaObjectIds.isEmpty();
152    boolean noObjectIds = backendIdObjectIdMap.isEmpty();
153    
154    if(noMediaObjectIds && noObjectIds){
155      LOGGER.debug("No media object ids or object ids.");
156      return;
157    }
158    
159    SolrQueryBuilder query = new SolrQueryBuilder();
160    query.addFields(FIELDS_RESOLVE_OBJECT_IDS);
161    if(!noMediaObjectIds){
162      query.addCustomFilter(new OrQueryFilter(SOLR_FIELD_ID, mediaObjectIds));
163    }
164    
165    if(!noObjectIds){
166      for(Entry<Integer, HashSet<String>> e : backendIdObjectIdMap.entrySet()){
167        query.addCustomFilter(new OrSubQueryFilter(new AbstractQueryFilter[]{new AndQueryFilter(Definitions.SOLR_FIELD_BACKEND_ID, e.getKey()), new AndQueryFilter(Definitions.SOLR_FIELD_CREATOR_OBJECT_ID, e.getValue())}));
168      }
169    }
170    
171    List<MediaObject> results = getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(query.toSolrQuery(Definitions.ELEMENT_MEDIA_OBJECTLIST), MediaObject.class);
172    if(results == null){
173      LOGGER.debug("No results.");
174      return;
175    }
176    
177    for(MediaObject object : objects){
178      for(MediaObject result : results){
179        if(result.getMediaObjectId().equals(object.getMediaObjectId())){
180          UserIdentity oUserId = object.getOwnerUserId();
181          UserIdentity rUserId = result.getOwnerUserId();
182          if(oUserId != null && !UserIdentity.equals(oUserId, rUserId)){
183            LOGGER.warn("Replacing conflicting userId.");
184          }
185          object.setOwnerUserId(rUserId);
186          
187          String oObjectId = object.getObjectId();
188          String rObjectId = result.getObjectId();
189          if(oObjectId != null && !oObjectId.equals(rObjectId )){
190            LOGGER.warn("Replacing conflicting objectId.");
191          }
192          object.setObjectId(rObjectId);
193          
194          Integer oBackendId = object.getBackendId();
195          Integer rBackendId = result.getBackendId();
196          if(oBackendId != null && !oBackendId.equals(rBackendId)){
197            LOGGER.warn("Replacing conflicting backendId.");
198          }
199          object.setBackendId(rBackendId);
200        }else if(result.getObjectId().equals(object.getObjectId())){
201          Integer rBackendId = result.getBackendId();
202          Integer oBackendId = object.getBackendId();
203          if((rBackendId == null && oBackendId == null) || (rBackendId != null && rBackendId.equals(oBackendId))){
204            UserIdentity oUserId = object.getOwnerUserId();
205            UserIdentity rUserId = result.getOwnerUserId();
206            if(oUserId != null && !UserIdentity.equals(oUserId, rUserId)){
207              LOGGER.warn("Replacing conflicting userId.");
208            }
209            object.setOwnerUserId(rUserId);
210            object.setMediaObjectId(result.getMediaObjectId());
211          } // if it is a match
212        } // else ignore non-matching media object, this can be later matched or new object without a match
213      } // for results
214    } // for objects
215  }
216  
217  /**
218   * Sets all missing ids for the given media objects if ids are found. 
219   * Objects without valid mediaObjectId or backendId+objectId pair are ignored.
220   * 
221   * @param mediaObjects
222   */
223  public void resolveObjectIds(MediaObjectList mediaObjects){
224    if(MediaObjectList.isEmpty(mediaObjects)){
225      LOGGER.debug("Empty object list.");
226      return;
227    }
228  
229    HashSet<String> mediaObjectIds = new HashSet<>();
230    HashMap<Integer, HashSet<String>> backendIdObjectIdMap = new HashMap<>();
231    List<MediaObject> objects = mediaObjects.getMediaObjects();
232    for(Iterator<MediaObject> vIter = objects.iterator(); vIter.hasNext();){ // get media object ids, backend ids and object ids
233      MediaObject vo = vIter.next();
234      String mediaObjectId = vo.getMediaObjectId();
235      if(!StringUtils.isBlank(mediaObjectId)){
236        mediaObjectIds.add(mediaObjectId);
237      }else{
238        String objectId = vo.getObjectId();
239        if(!StringUtils.isBlank(objectId)){
240          Integer backendId = vo.getBackendId();
241          HashSet<String> objectIds = backendIdObjectIdMap.get(backendId);
242          if(objectIds == null){
243            objectIds = new HashSet<>();
244            backendIdObjectIdMap.put(backendId, objectIds);
245          }
246          objectIds.add(objectId);
247        }else{
248          LOGGER.debug("Ignored media object without objectId and mediaObjectId.");
249        }
250      }
251    }
252    resolveObjectIds(backendIdObjectIdMap, mediaObjectIds, objects);
253  }
254  
255  /**
256   * 
257   * @param objects
258   * @return true on success. Note that <i>nothing updated</i> equals to success if no other errors are present.
259   */
260  public boolean updateIfNewer(MediaObjectList objects){
261    return update(objects, true);
262  }
263  
264  /**
265   * 
266   * @param objects
267   * @return true on success
268   */
269  public boolean update(MediaObjectList objects){
270    return update(objects, false);
271  }
272
273  /**
274   * The objects must have a valid id, either objectId (provided by back-end or client) + backendId (Note: null is a valid backendId if object created by the user) or mediaObjectId (provided by system)
275   * 
276   * @param objects
277   * @param onlyNewer if true, only the objects which are newer will be added to the database. If the passed object does not have updated set, it will be ignored if onlyNewer is true.
278   * @return true on success
279   */
280  private boolean update(MediaObjectList objects, boolean onlyNewer){
281    if(MediaObjectList.isEmpty(objects)){
282      LOGGER.debug("Ignored empty object list.");
283      return true;
284    }
285    if(!MediaObjectList.isValid(objects)){
286      LOGGER.warn("Tried update with invalid object list.");
287      return false;
288    }
289    
290    List<String> voids = objects.getMediaObjectIds();
291    if(voids == null){
292      LOGGER.warn("Could not get media object ids.");
293      return false;
294    }
295    
296    SolrQueryBuilder solr = new SolrQueryBuilder();
297    solr.addFields(FIELDS_UPDATE);
298    if(onlyNewer){
299      solr.addField(Definitions.SOLR_FIELD_UPDATED);
300    }
301    SimpleSolrTemplate t = getSolrTemplate(BEAN_ID_SOLR_SERVER);
302    List<MediaObject> refList = new ArrayList<>();
303
304    solr.clearCustomFilters();
305    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, voids));
306    refList = t.queryForList(solr.toSolrQuery(Definitions.ELEMENT_MEDIA_OBJECTLIST), MediaObject.class);
307    if(refList.isEmpty()){
308      LOGGER.warn("Tried to update non-existent objects.");
309      return false;
310    }
311    
312    MediaObjectList references = MediaObjectList.getMediaObjectList(refList, null);
313    
314    List<MediaObject> mediaObjects = objects.getMediaObjects();
315    List<MediaObject> update = new ArrayList<>(mediaObjects.size());
316    Date updated = new Date();
317    for(MediaObject object : mediaObjects){ // check for that userId, backendId or objectId is not being changed
318      String mediaObjectId = object.getMediaObjectId();
319      MediaObject reference = references.getMediaObject(mediaObjectId);
320      if(reference == null){
321        LOGGER.warn("Tried to update non-existent object, id: "+mediaObjectId);
322        return false;
323      }else if(!reference.getObjectId().equals(object.getObjectId())){
324        LOGGER.warn("Object creator id mismatch for object, id: "+mediaObjectId);
325        return false;
326      }else if(!UserIdentity.equals(reference.getOwnerUserId(), object.getOwnerUserId())){
327        LOGGER.warn("Object user identity mismatch for object, id: "+mediaObjectId);
328        return false;
329      }else{
330        Integer rBackendId = reference.getBackendId();
331        Integer oBackendId = object.getBackendId();
332        if(rBackendId != null){
333          if(!rBackendId.equals(oBackendId)){
334            LOGGER.warn("Object backend id mismatch for object, id: "+mediaObjectId);
335            return false;
336          }
337        }else if(oBackendId != null){ // both are not null
338          LOGGER.warn("Object backend id mismatch for object, id: "+mediaObjectId);
339          return false;
340        }
341      }
342      Date oUpdated = object.getUpdated();
343      if(onlyNewer){
344        if(oUpdated == null){
345          LOGGER.warn("Ignored object without updated timestamp, id: "+mediaObjectId);
346          continue;
347        }else if(oUpdated.before(reference.getUpdated())){
348          LOGGER.debug("Ignored object with older timestamp, id: "+mediaObjectId);
349          continue;
350        }
351      }else if(oUpdated == null){
352        object.setUpdated(updated); // make sure there is a timestamp
353      }
354      if(object.getVisibility() == null){
355        LOGGER.debug("Object is missing visibility, using default: "+DEFAULT_VISIBILITY.name());
356        object.setVisibility(DEFAULT_VISIBILITY);
357      }
358      update.add(object);
359    }
360    
361    if(t.addBeans(update).getStatus() == SolrException.ErrorCode.UNKNOWN.code){
362      return true;
363    }else{
364      LOGGER.warn("Failed to update media objects.");
365      return false;
366    }
367  }
368
369  
370  
371  /**
372   * This will also automatically remove any associations between the given media objects and their photos.
373   * 
374   * @param mediaobjectIds
375   * @return true on success
376   */
377  public boolean remove(Collection<String> mediaobjectIds){
378    if(mediaobjectIds == null || mediaobjectIds.isEmpty()){
379      LOGGER.debug("Ignored empty media object id list.");
380      return true;
381    }
382    
383    for(String mediaObjectId : mediaobjectIds){ // remove associations
384      _photoDAO.deassociate(null, mediaObjectId);
385    }
386    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER);
387    if(template.deleteById(mediaobjectIds).getStatus() == SolrException.ErrorCode.UNKNOWN.code){
388      return true;
389    }else{
390      LOGGER.warn("Failed to remove media objects.");
391      return false;
392    }
393  }
394  
395  /**
396   * 
397   * @param dataGroups
398   * @param limits
399   * @param mediaTypes target media types for the retrieval
400   * @param serviceTypes
401   * @param mediaObjectIds
402   * @param userIdFilter
403   * @return list of media objects or null if none was found
404   * @throws IllegalArgumentException on bad query terms
405   */
406  public MediaObjectList getMediaObjects(DataGroups dataGroups, Limits limits, EnumSet<MediaType> mediaTypes, EnumSet<ServiceType> serviceTypes, Collection<String> mediaObjectIds, long[] userIdFilter) throws IllegalArgumentException {
407    if(mediaTypes == null || mediaTypes.isEmpty()){
408      throw new IllegalArgumentException("Invalid MediaType "+MediaType.class.toString()+" given.");
409    }
410    return getMediaObjectList(dataGroups, limits, mediaTypes, serviceTypes, mediaObjectIds, userIdFilter);
411  }
412
413  /**
414   * helper method for retrieving the media object list
415   * 
416   * @param dataGroups
417   * @param limits
418   * @param mediaTypes target media types, not null, nor empty
419   * @param serviceTypes
420   * @param mediaObjectIds
421   * @param userIdFilter
422   * @return list of media objects or null if none was found
423   */
424  private MediaObjectList getMediaObjectList(DataGroups dataGroups, Limits limits, EnumSet<MediaType> mediaTypes, EnumSet<ServiceType> serviceTypes, Collection<String> mediaObjectIds, long[] userIdFilter) {
425    SolrQueryBuilder solr = new SolrQueryBuilder();
426    if(!processDataGroups(dataGroups, solr)){
427      LOGGER.debug("Process data groups did not result viable combination for search, returning null...");
428      return null;
429    }
430    
431    solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_MEDIA_TYPE, MediaType.toInt(mediaTypes)));
432    
433    if(!ServiceType.isEmpty(serviceTypes)){
434      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
435    }
436    
437    if(mediaObjectIds != null && !mediaObjectIds.isEmpty()){
438      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, mediaObjectIds));
439    }
440    
441    if(userIdFilter != null){
442      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
443    }
444    
445    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
446    solr.setLimits(limits);
447    
448    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_MEDIA_OBJECTLIST));
449    List<MediaObject> mediaObjects = SimpleSolrTemplate.getList(response, MediaObject.class);
450    if(mediaObjects == null){
451      LOGGER.debug("No results.");
452      return null;
453    }
454    
455    ResultInfo info = null;
456    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
457      LOGGER.debug("Resolving result info for the requested objects.");
458      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), limits.getEndItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), response.getResults().getNumFound());
459    }
460    
461    MediaObjectList voList = MediaObjectList.getMediaObjectList(mediaObjects, info);
462    _keywordsDAO.assignFriendlyKeywords(voList);
463    return voList;
464  }
465  
466  /**
467   * 
468   * @param authenticatedUser
469   * @param dataGroups
470   * @param limits
471   * @param serviceTypes
472   * @param userIdFilter
473   * @param mediaObjectTerms list of terms to use for search. Note: if the objects have mediaObjectIds set, these will be directly used as filter
474   * @return list of ids or null if none
475   */
476  public List<String> getMediaObjectIds(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter, MediaObjectList mediaObjectTerms) {
477    SolrQueryBuilder solr = new SolrQueryBuilder();
478    if(!processDataGroups(dataGroups, solr)){
479      LOGGER.debug("Process data groups did not result viable combination for search, returning null...");
480      return null;
481    }
482    
483    if(!UserIdentity.isValid(authenticatedUser)){
484      LOGGER.debug("Invalid authenticated user, limiting search to public content.");
485      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt()));
486    }else{
487      solr.addCustomFilter(new AndSubQueryFilter(new AbstractQueryFilter[]{new OrQueryFilter(Definitions.SOLR_FIELD_USER_ID, authenticatedUser.getUserId()), new OrQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt())}));
488    }
489    
490    if(!ServiceType.isEmpty(serviceTypes)){
491      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
492    }
493    
494    if(!ArrayUtils.isEmpty(userIdFilter)){
495      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
496    }
497    
498    processMediaObjects(solr, mediaObjectTerms);
499    
500    solr.setLimits(limits);
501    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
502    solr.addField(SOLR_FIELD_ID);
503    
504    return SimpleSolrTemplate.getObjects(getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_MEDIA_OBJECTLIST)), SOLR_FIELD_ID, String.class);
505  }
506  
507  /**
508   * 
509   * @param solr
510   * @param mediaObjectTerms
511   * @throws IllegalArgumentException on too high term count
512   */
513  private void processMediaObjects(SolrQueryBuilder solr, MediaObjectList mediaObjectTerms) throws IllegalArgumentException {
514    if(MediaObjectList.isEmpty(mediaObjectTerms)){
515      LOGGER.debug("No media objects.");
516      return;
517    }
518    LOGGER.debug("Creating media object filters...");
519    
520    List<MediaObject> terms = mediaObjectTerms.getMediaObjects();
521    List<String> mediaObjectIds = new ArrayList<>();
522    List<AbstractQueryFilter> filters = new ArrayList<>();
523    for(MediaObject term : terms){
524      String mediaObjectId = term.getMediaObjectId();
525      if(!StringUtils.isBlank(mediaObjectId)){
526        LOGGER.debug("media object id was given, using as filter, id: "+mediaObjectId);
527        mediaObjectIds.add(mediaObjectId);
528      }else{
529        String value = term.getValue();
530        if(StringUtils.isBlank(value)){
531          LOGGER.debug("No value for visual object, attempting to use name.");
532          value = term.getName();
533          if(StringUtils.isBlank(value)){
534            LOGGER.warn("Ignording media object search term without value or name.");
535            continue;
536          }
537        }
538        
539        OrSubQueryFilter subFilter = new OrSubQueryFilter();
540        subFilter.addFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VALUE_SEARCH, value));
541        
542        MediaObjectType type = term.getMediaObjectType();
543        if(type != null){
544          subFilter.addFilter(new AndQueryFilter(Definitions.SOLR_FIELD_MEDIA_OBJECT_TYPE, type.toInt()));
545        }
546        
547        Visibility visibility = term.getVisibility();
548        if(visibility != null){
549          subFilter.addFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, visibility.toInt()));
550        }
551        
552        ServiceType serviceType = term.getServiceType();
553        if(serviceType != null){
554          subFilter.addFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, serviceType.getServiceId()));
555        }
556        
557        ConfirmationStatus status = term.getConfirmationStatus();
558        if(status != null){
559          subFilter.addFilter(new AndQueryFilter(Definitions.SOLR_FIELD_STATUS, status.toInt()));
560        }
561        
562        Integer temp = term.getRank();
563        if(temp != null){
564          subFilter.addFilter(new RangeQueryFilter(Definitions.SOLR_FIELD_RANK, temp, null));
565        }
566        
567        temp = term.getBackendId();
568        if(temp != null){
569          subFilter.addFilter(new RangeQueryFilter(Definitions.SOLR_FIELD_BACKEND_ID, temp, null));
570        }
571        
572        Double confidence = term.getConfidence();
573        if(confidence != null){
574          subFilter.addFilter(new RangeQueryFilter(Definitions.SOLR_FIELD_CONFIDENCE, confidence, null));
575        }
576        
577        filters.add(subFilter);
578      }
579    } // for
580    
581    if(!filters.isEmpty()){
582      solr.addCustomFilter(new AndSubQueryFilter(filters.toArray(new AbstractQueryFilter[filters.size()])));
583    }
584
585    if(!mediaObjectIds.isEmpty()){
586      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, mediaObjectIds));
587    }
588  }
589  
590  /**
591   * 
592   * @param solr
593   * @param sortOptions 
594   */
595  private void setOrderBy(SolrQueryBuilder solr, SortOptions sortOptions){
596    if(sortOptions == null || !sortOptions.hasValues()){
597      solr.setSortOptions(DEFAULT_SORT_OPTIONS);
598      return;
599    }
600    
601    Set<Option> so = sortOptions.getSortOptions(Definitions.ELEMENT_MEDIA_OBJECTLIST);
602    if(so == null){
603      return;
604    }
605    
606    for(Iterator<Option> iter = so.iterator();iter.hasNext();){
607      Option o = iter.next();
608      String elementName = o.getElementName();
609      if(Definitions.ELEMENT_CONFIDENCE.equals(elementName)){
610        solr.addSortOption(new Option(Definitions.SOLR_FIELD_CONFIDENCE, o.getOrderDirection(), Definitions.ELEMENT_MEDIA_OBJECTLIST));
611      }else if(Definitions.ELEMENT_RANK.equals(elementName)){
612        solr.addSortOption(new Option(Definitions.SOLR_FIELD_RANK, o.getOrderDirection(), Definitions.ELEMENT_MEDIA_OBJECTLIST));
613      }else if(Definitions.ELEMENT_VALUE.equals(elementName)){
614        solr.addSortOption(new Option(Definitions.SOLR_FIELD_VALUE, o.getOrderDirection(), Definitions.ELEMENT_MEDIA_OBJECTLIST));
615      }else{
616        LOGGER.debug("Ignored unknown sort element: "+elementName);
617      }
618    }
619  }
620
621  /**
622   * Note that because of solr limitations, the media object count cannot exceed MAX_FILTER_COUNT.
623   * 
624   * @param authenticatedUser
625   * @param dataGroups
626   * @param limits
627   * @param mediaTypes list of target media types for the search
628   * @param serviceTypes
629   * @param sortOptions
630   * @param userIdFilter
631   * @param mediaObjectTerms list of terms to use for search. Note: if the objects have mediaObjectIds set, these will be directly used as filter
632   * @return list of media objects or null if none was found
633   * @throws IllegalArgumentException on bad values
634   */
635  public MediaObjectList search(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, EnumSet<MediaType> mediaTypes, EnumSet<ServiceType> serviceTypes, SortOptions sortOptions, long[] userIdFilter, MediaObjectList mediaObjectTerms) throws IllegalArgumentException {
636    if(mediaTypes == null || mediaTypes.isEmpty()){
637      throw new IllegalArgumentException("Invalid "+MediaType.class.toString()+" given.");
638    }
639    
640    SolrQueryBuilder solr = new SolrQueryBuilder();
641    
642    if(!processDataGroups(dataGroups, solr)){
643      LOGGER.debug("Process data groups did not result viable combination for search, returning null...");
644      return null;
645    }
646    
647    solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_MEDIA_TYPE, MediaType.toInt(mediaTypes)));
648    
649    processMediaObjects(solr, mediaObjectTerms); // throws IllegalArgumentException if term count exceeds maximum
650    
651    if(!UserIdentity.isValid(authenticatedUser)){
652      LOGGER.debug("Invalid authenticated user, limiting search to public content.");
653      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt()));
654    }else{
655      solr.addCustomFilter(new AndSubQueryFilter(new AbstractQueryFilter[]{new OrQueryFilter(Definitions.SOLR_FIELD_USER_ID, authenticatedUser.getUserId()), new OrQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt())}));
656    }
657    
658    if(!ServiceType.isEmpty(serviceTypes)){
659      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
660    }
661    
662    if(!ArrayUtils.isEmpty(userIdFilter)){
663      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
664    }
665    
666    solr.setLimits(limits);
667    setOrderBy(solr, sortOptions);
668    
669    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_MEDIA_OBJECTLIST));
670    List<? extends MediaObject> mediaObjects = SimpleSolrTemplate.getList(response, MediaObject.class);
671    if(mediaObjects == null){
672      LOGGER.debug("No results.");
673      return null;
674    }
675    
676    ResultInfo info = null;
677    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
678      LOGGER.debug("Resolving result info for the requested objects.");
679      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), limits.getEndItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), response.getResults().getNumFound());
680    }
681    
682    MediaObjectList voList = MediaObjectList.getMediaObjectList(mediaObjects, info);
683    _keywordsDAO.assignFriendlyKeywords(voList);
684    return voList;
685  }
686  
687  /**
688   * helper method for processing the data groups and setting filters for the query builder.
689   * 
690   * @param dataGroups For applicable values see {@link service.tut.pori.contentanalysis.MediaObject.MediaObjectType#fromDataGroups(DataGroups)}.
691   * @param solr
692   * @return true if the data groups can returned results.
693   */
694  private boolean processDataGroups(DataGroups dataGroups, SolrQueryBuilder solr){
695    if(!DataGroups.hasDataGroup(DataGroups.DATA_GROUP_ALL, dataGroups, Definitions.ELEMENT_MEDIA_OBJECTLIST)){ // do not care about filters if ALL is present
696      boolean onlyBasic = DataGroups.hasDataGroup(DataGroups.DATA_GROUP_BASIC, dataGroups, Definitions.ELEMENT_MEDIA_OBJECTLIST);
697      
698      EnumSet<ConfirmationStatus> statuses = ConfirmationStatus.fromDataGroups(dataGroups);
699      if(statuses == null){
700        LOGGER.debug("No "+ConfirmationStatus.class.toString()+" filter, using defaults.");
701        solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_STATUS, DEFAULT_CONFIRMATION_STATUS_LIST));
702      }else{
703        onlyBasic = false;
704        solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_STATUS, ConfirmationStatus.toIntArray(statuses)));
705      }
706    
707      EnumSet<MediaObjectType> types = MediaObjectType.fromDataGroups(dataGroups);
708      if(types != null){
709        onlyBasic = false;
710        solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_MEDIA_OBJECT_TYPE, MediaObjectType.toIntArray(types)));
711      }
712      
713      if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_TIMECODES, dataGroups, Definitions.ELEMENT_MEDIA_OBJECTLIST)){
714        onlyBasic = false;
715        solr.addFields(FIELDS_DATA_GROUP_TIMECODES);
716      }
717      
718      if(onlyBasic){
719        LOGGER.debug("Data group "+DataGroups.DATA_GROUP_BASIC+" given without other media object related data groups, no results can be found.");
720        return false;
721      }
722      
723      solr.addFields(FIELDS_DATA_GROUP_DEFAULTS);
724    }
725    return true;
726  }
727  
728  /**
729   * Suggestion/Autocomplete from Solr
730   * @param authenticatedUser 
731   * @param dataGroups filters based on MediaObjectType. For applicable values see {@link service.tut.pori.contentanalysis.MediaObject.MediaObjectType#fromDataGroups(DataGroups)}.
732   * @param limits 
733   * @param query the term to be searched for.
734   * @return response
735   */
736  public QueryResponse getSuggestions(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, String query) {
737    SolrQueryBuilder solr = new SolrQueryBuilder();
738    
739    //set visibility restrictions
740    if(!UserIdentity.isValid(authenticatedUser)){
741      LOGGER.debug("Invalid authenticated user, limiting search to public content.");
742      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt()));
743    }else{
744      solr.addCustomFilter(new AndSubQueryFilter(new AbstractQueryFilter[]{new OrQueryFilter(Definitions.SOLR_FIELD_USER_ID, authenticatedUser.getUserId()), new OrQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt())}));
745    }
746    
747    processDataGroups(dataGroups, solr); //sets the query filter for MediaObjectType
748    solr.setQueryParameter(new QueryParameter(query));
749    solr.setLimits(limits);
750    
751    return getSolrTemplate(BEAN_ID_SOLR_SERVER).query(SolrQueryBuilder.setRequestHandler(solr.toSolrQuery(), SolrQueryBuilder.RequestHandlerType.SUGGEST));
752  }
753}