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.Arrays;
020import java.util.Collection;
021import java.util.Date;
022import java.util.EnumSet;
023import java.util.Iterator;
024import java.util.LinkedList;
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 core.tut.pori.dao.SQLSelectBuilder.OrderDirection;
042import core.tut.pori.dao.SimpleSolrTemplate;
043import core.tut.pori.dao.SolrDAO;
044import core.tut.pori.dao.SolrQueryBuilder;
045import core.tut.pori.dao.filter.AbstractQueryFilter;
046import core.tut.pori.dao.filter.AndQueryFilter;
047import core.tut.pori.dao.filter.AndSubQueryFilter;
048import core.tut.pori.dao.filter.OrQueryFilter;
049import core.tut.pori.http.parameters.DataGroups;
050import core.tut.pori.http.parameters.Limits;
051import core.tut.pori.http.parameters.SortOptions;
052import core.tut.pori.users.UserIdentity;
053import core.tut.pori.utils.MediaUrlValidator.MediaType;
054
055/**
056 * This class can be used to add, remove and modify photo metadata, and also to associate the photos with media objects.
057 */
058public class PhotoDAO extends SolrDAO{
059  private static final String BEAN_ID_SOLR_SERVER = "solrServerPhotos";
060  private static final SortOptions DEFAULT_SORT_OPTIONS;
061  static{
062    DEFAULT_SORT_OPTIONS = new SortOptions();
063    DEFAULT_SORT_OPTIONS.addSortOption(new SortOptions.Option(SOLR_FIELD_ID, OrderDirection.ASCENDING, Definitions.ELEMENT_PHOTOLIST));
064  }
065  private static final String[] FIELDS_DATA_GROUP_DEFAULTS = new String[]{SOLR_FIELD_ID, Definitions.SOLR_FIELD_SERVICE_ID, Definitions.SOLR_FIELD_USER_ID};
066  private static final String FIELDS_DATA_GROUP_VISIBILITY = Definitions.SOLR_FIELD_VISIBILITY;
067  private static final String[] FIELDS_DATA_GROUP_BASIC = ArrayUtils.addAll(FIELDS_DATA_GROUP_DEFAULTS, FIELDS_DATA_GROUP_VISIBILITY, Definitions.SOLR_FIELD_CREDITS, Definitions.SOLR_FIELD_NAME, Definitions.SOLR_FIELD_DESCRIPTION);
068  private static final String[] FIELDS_GET_ACCESS_DETAILS = new String[]{Definitions.SOLR_FIELD_USER_ID, Definitions.SOLR_FIELD_VISIBILITY, SOLR_FIELD_ID};
069  private static final String[] FIELDS_SET_OWNERS = new String[]{SOLR_FIELD_ID, Definitions.SOLR_FIELD_USER_ID};
070  private static final String[] FIELDS_UPDATE = new String[]{Definitions.SOLR_FIELD_USER_ID, SOLR_FIELD_ID};
071  private static final Logger LOGGER = Logger.getLogger(PhotoDAO.class);
072  private static final EnumSet<MediaType> MEDIA_TYPES = EnumSet.of(MediaType.PHOTO);
073  @Autowired
074  private AssociationDAO _associationDAO = null;
075  @Autowired
076  private PhotoTaskDAO _photoTaskDAO = null;
077  @Autowired
078  private MediaObjectDAO _mediaObjectDAO = null;
079  
080  /**
081   * Inserts the objects and sets all media types to {@link core.tut.pori.utils.MediaUrlValidator.MediaType#PHOTO} for all objects with {@link core.tut.pori.utils.MediaUrlValidator.MediaType#UNKNOWN} or null media type-
082   * @param objects
083   * @return true on success
084   * @see service.tut.pori.contentanalysis.MediaObjectDAO#insert(MediaObjectList)
085   */
086  public boolean insert(MediaObjectList objects){
087    if(MediaObjectList.isEmpty(objects)){
088      LOGGER.debug("Empty list ignored.");
089      return true;
090    }
091    for(MediaObject object : objects.getMediaObjects()){
092      MediaType mediaType = object.getMediaType();
093      if(mediaType == null || MediaType.UNKNOWN.equals(mediaType)){
094        object.setMediaType(MediaType.PHOTO); //set all media object MediaType to PHOTO
095      }
096    }
097    return _mediaObjectDAO.insert(objects);
098  }
099  
100  /**
101   * Update the objects and sets all media types to {@link core.tut.pori.utils.MediaUrlValidator.MediaType#PHOTO} for all objects with {@link core.tut.pori.utils.MediaUrlValidator.MediaType#UNKNOWN} or null media type-
102   * @param objects
103   * @return true on success
104   * @see service.tut.pori.contentanalysis.MediaObjectDAO#update(MediaObjectList)
105   */
106  public boolean update(MediaObjectList objects){
107    if(MediaObjectList.isEmpty(objects)){
108      LOGGER.debug("Empty list ignored.");
109      return true;
110    }
111    for(MediaObject object : objects.getMediaObjects()){
112      MediaType mediaType = object.getMediaType();
113      if(mediaType == null || MediaType.UNKNOWN.equals(mediaType)){
114        object.setMediaType(MediaType.PHOTO); //set all media object MediaType to PHOTO
115      }
116    }
117    return _mediaObjectDAO.update(objects);
118  }
119  
120  /**
121   * 
122   * @param photo
123   * @return true on success
124   */
125  public boolean insert(Photo photo){
126    return insert(PhotoList.getPhotoList(Arrays.asList(photo), null));
127  }
128
129  /**
130   * 
131   * @param photos
132   * @return true on success
133   */
134  public boolean insert(PhotoList photos){
135    if(PhotoList.isEmpty(photos)){
136      LOGGER.debug("No photos given.");
137      return false;
138    }
139    LOGGER.debug("Adding photos...");
140    List<Photo> p = photos.getPhotos();
141    Date updated = new Date();
142    MediaObjectList combined = new MediaObjectList();
143    for(Photo photo : p){
144      if(photo.getUpdated() == null){
145        photo.setUpdated(updated);
146      }
147      
148      String guid = UUID.randomUUID().toString();
149      if(photo.getGUID() != null){
150        LOGGER.warn("Replacing GUID for photo with existing GUID: "+photo.getGUID()+", new GUID: "+guid);   
151      }
152      photo.setGUID(guid);
153
154      MediaObjectList objects = photo.getMediaObjects();
155      Visibility visibility = photo.getVisibility();
156      UserIdentity userId = photo.getOwnerUserId();
157      if(!MediaObjectList.isEmpty(objects)){
158        for(Iterator<MediaObject> vIter = objects.getMediaObjects().iterator(); vIter.hasNext();){
159          MediaObject object = vIter.next();
160          if(!UserIdentity.equals(userId, object.getOwnerUserId())){
161            LOGGER.warn("Invalid user identity for media object in photo, GUID: "+guid);
162            return false;
163          }
164          if(object.getVisibility() == null){
165            LOGGER.debug("Object missing visibility value, using photo's visibility.");
166            object.setVisibility(visibility);
167          }
168          combined.addMediaObject(object);
169        } // for
170      }
171    }
172    if(!PhotoList.isValid(photos)){ // check validity after ids have been generated
173      LOGGER.warn("Tried to add invalid photo list.");
174      return false;
175    }
176    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER); 
177    UpdateResponse response = template.addBeans(p);
178
179    if(response.getStatus() != SolrException.ErrorCode.UNKNOWN.code){
180      LOGGER.warn("Failed to add photos.");
181      return false;
182    }
183
184    if(insert(combined)){
185      associate(photos);
186    }else{
187      LOGGER.warn("Insert failed for combined photo list.");
188      return false;
189    }
190    return true;
191  }
192
193  /**
194   * Note: content added through ContentStorage MUST be removed through ContentStorage, removing the metadata directly using this method may cause undefined behavior.
195   * 
196   * @param guids
197   * @see service.tut.pori.contentstorage.ContentStorageCore
198   */
199  public void remove(Collection<String> guids){
200    if(guids == null || guids.isEmpty()){
201      LOGGER.debug("Ignored empty GUIDs list.");
202      return;
203    }
204    SimpleSolrTemplate template = getSolrTemplate(BEAN_ID_SOLR_SERVER);
205    if(template.deleteById(guids).getStatus() != SolrException.ErrorCode.UNKNOWN.code){
206      LOGGER.warn("Failed to delete by GUID.");
207    }else{
208      Map<String, Set<String>> guidVoidMap = _associationDAO.getAssociationsForGUIDs(guids);
209      if(guidVoidMap == null){
210        LOGGER.debug("No media objects for the GUID.");
211      }else{
212        for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
213          if(!_mediaObjectDAO.remove(e.getValue())){ // we do not need to de-associate, this will automatically cleanup the association table (by media object dao)
214            LOGGER.warn("Failed to remove media objects for GUID: "+e.getKey());
215          }
216        }
217      }
218      _photoTaskDAO.remove(guids);
219    }
220  }
221  
222  /**
223   * 
224   * @param dataGroups optional filter
225   * @param guids optional filter
226   * @param limits optional filter
227   * @param serviceTypes optional filter
228   * @param userIdFilter optional filter
229   * @return list of photos or null if none
230   */
231  public PhotoList getPhotos(DataGroups dataGroups, Collection<String> guids, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter){
232    return getPhotoList(dataGroups, guids, limits, serviceTypes, userIdFilter);
233  }
234
235  /**
236   * 
237   * @param dataGroups
238   * @param guids
239   * @param limits
240   * @param serviceTypes
241   * @param userIdFilter
242   * @return list of photos or null if none was found
243   */
244  private PhotoList getPhotoList(DataGroups dataGroups, Collection<String> guids, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter){
245    SolrQueryBuilder solr = new SolrQueryBuilder(null);
246    if(guids != null && !guids.isEmpty()){
247      LOGGER.debug("Adding guid filter...");
248      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
249    }
250    if(!ServiceType.isEmpty(serviceTypes)){
251      LOGGER.debug("Adding service type filter...");
252      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
253    }
254
255    if(!ArrayUtils.isEmpty(userIdFilter)){
256      LOGGER.debug("Adding user id filter...");
257      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
258    }
259
260    solr.setLimits(limits);
261    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
262    setDataGroups(dataGroups, solr);
263
264    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST));
265    List<Photo> photos = SimpleSolrTemplate.getList(response, Photo.class);
266    if(photos == null){
267      LOGGER.debug("No photos");
268      return null;
269    }
270
271    ResultInfo info = null;
272    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
273      LOGGER.debug("Resolving result info for the requested photos.");
274      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_PHOTOLIST), limits.getEndItem(Definitions.ELEMENT_PHOTOLIST), response.getResults().getNumFound());
275    }
276
277    PhotoList photoList = PhotoList.getPhotoList(photos, info);
278    Map<String, Set<String>> guidVoidMap = _associationDAO.getAssociationsForGUIDs(photoList.getGUIDs());
279    if(guidVoidMap == null){
280      LOGGER.debug("No objects for the photos.");
281    }else{
282      for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
283        MediaObjectList objects = _mediaObjectDAO.getMediaObjects(dataGroups, limits, MEDIA_TYPES, null, e.getValue(), null); // do NOT give serviceTypes as filter, we are searching photos with specific serviceTypes, not mediaObjects
284        if(MediaObjectList.isEmpty(objects)){
285          LOGGER.warn("Could not retrieve objects for GUID: "+e.getKey());
286        }else{
287          photoList.getPhoto(e.getKey()).addMediaObjects(objects);
288        }
289      }
290    }
291
292    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_STATUS, dataGroups)){
293      _photoTaskDAO.getMediaStatus(photoList.getPhotos());
294    }
295
296    return photoList;
297  }
298
299  /**
300   * 
301   * @param dataGroups
302   * @param solr
303   */
304  private void setDataGroups(DataGroups dataGroups, SolrQueryBuilder solr){
305    if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_ALL, dataGroups)){
306      LOGGER.debug("Data group "+DataGroups.DATA_GROUP_ALL+" found, will not set field list.");
307    }else if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_BASIC, dataGroups)){
308      solr.addFields(FIELDS_DATA_GROUP_BASIC);
309    }else{
310      boolean hasGroup = DataGroups.hasDataGroup(Definitions.DATA_GROUP_VISIBILITY, dataGroups);
311      if(hasGroup){
312        solr.addField(FIELDS_DATA_GROUP_VISIBILITY);
313      }
314
315      if(DataGroups.hasDataGroup(DataGroups.DATA_GROUP_DEFAULTS, dataGroups) || DataGroups.isEmpty(dataGroups)){  // if defaults are explicitly given or there are not datagroups
316        solr.addFields(FIELDS_DATA_GROUP_DEFAULTS);
317      }else if(!hasGroup){
318        LOGGER.debug("No valid data groups, using "+DataGroups.DATA_GROUP_DEFAULTS);
319        solr.addFields(FIELDS_DATA_GROUP_DEFAULTS);
320      }
321    }
322  }
323
324  /**
325   * 
326   * @param authenticatedUser optional filter
327   * @param dataGroups optional filter
328   * @param guids optional filter
329   * @param limits optional filter
330   * @param objects optional filter
331   * @param serviceTypes optional filter
332   * @param userIdFilter optional filter
333   * @return the results, or null if none.
334   * @throws IllegalArgumentException on bad search terms
335   */
336  public PhotoList search(UserIdentity authenticatedUser, DataGroups dataGroups, Collection<String> guids, Limits limits, MediaObjectList objects, EnumSet<ServiceType> serviceTypes, long[] userIdFilter) throws IllegalArgumentException{
337    SolrQueryBuilder solr = new SolrQueryBuilder();
338    setDataGroups(dataGroups, solr);
339    solr.setLimits(limits);
340
341    if(guids != null && !guids.isEmpty()){
342      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
343    }
344
345    Map<String, Set<String>> guidVoidMap = null;
346    if(!MediaObjectList.isEmpty(objects)){ // if media objects have been given as a search term, do a media object look-up first
347      List<String> mediaObjectIds = _mediaObjectDAO.getMediaObjectIds(authenticatedUser, dataGroups, null, null, userIdFilter, objects); // do NOT give serviceTypes as filter, we are searching photos with specific serviceTypes, not mediaObjects
348      if(mediaObjectIds == null){
349        LOGGER.debug("No objects found.");
350        return null;
351      }
352      
353      guidVoidMap = _associationDAO.getAssociationsForMediaObjectIds(mediaObjectIds);
354      if(guidVoidMap == null){
355        LOGGER.debug("No photos associated with the media object results.");
356        return null;
357      }
358      solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guidVoidMap.keySet()));
359    }
360
361    if(!ServiceType.isEmpty(serviceTypes)){
362      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_SERVICE_ID, ServiceType.toIdArray(serviceTypes)));
363    }
364
365    if(!ArrayUtils.isEmpty(userIdFilter)){
366      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_USER_ID, userIdFilter));
367    }
368
369    if(!UserIdentity.isValid(authenticatedUser)){
370      LOGGER.debug("Invalid authenticated user, limiting search to public content.");
371      solr.addCustomFilter(new AndQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt()));
372    }else{
373      solr.addCustomFilter(new AndSubQueryFilter(new AbstractQueryFilter[]{new OrQueryFilter(Definitions.SOLR_FIELD_USER_ID, authenticatedUser.getUserId()), new OrQueryFilter(Definitions.SOLR_FIELD_VISIBILITY, Visibility.PUBLIC.toInt())}));
374    }
375
376    solr.setSortOptions(DEFAULT_SORT_OPTIONS);
377    QueryResponse response = getSolrTemplate(BEAN_ID_SOLR_SERVER).query(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST));
378    List<Photo> photos = SimpleSolrTemplate.getList(response, Photo.class);
379    if(photos == null){
380      LOGGER.debug("No photos");
381      return null;
382    }
383
384    ResultInfo info = null;
385    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_RESULT_INFO, dataGroups)){
386      LOGGER.debug("Resolving result info for the requested photos.");
387      info = new ResultInfo(limits.getStartItem(Definitions.ELEMENT_PHOTOLIST), limits.getEndItem(Definitions.ELEMENT_PHOTOLIST), response.getResults().getNumFound());
388    }
389
390    PhotoList photoList = PhotoList.getPhotoList(photos, info);
391    if(guidVoidMap == null){  // resolve media object relations if not resolved already, depending on the data groups given we may not even need the media objects, but let's ignore it for now
392      guidVoidMap = _associationDAO.getAssociationsForGUIDs(photoList.getGUIDs());
393    }
394    
395    if(guidVoidMap == null){
396      LOGGER.debug("No photo-media object associations...");
397    }else{
398      LOGGER.debug("Retrieving media objects for the list of photos, if needed...");
399      for(Entry<String, Set<String>> e : guidVoidMap.entrySet()){
400        MediaObjectList photoObject = _mediaObjectDAO.getMediaObjects(dataGroups, limits, MEDIA_TYPES, null, e.getValue(), null); // do NOT give serviceTypes as filter, we are searching photos with specific serviceTypes, not mediaObjects
401        if(MediaObjectList.isEmpty(photoObject)){
402          LOGGER.debug("Could not retrieve objects for GUID: "+e.getKey());
403        }else{
404          Photo photo = photoList.getPhoto(e.getKey());
405          if(photo == null){
406            LOGGER.warn("Could not find photo, GUID: "+e.getKey());
407          }else{
408            photo.addMediaObjects(photoObject);
409          }
410        }
411      }
412    }
413
414    if(DataGroups.hasDataGroup(Definitions.DATA_GROUP_STATUS, dataGroups)){
415      _photoTaskDAO.getMediaStatus(photoList.getPhotos());
416    }
417
418    return photoList;
419  }
420
421  /**
422   * Sets the owner details (userId) to the given photos, requires that GUID has been set to the photo object
423   * 
424   * @param photos
425   * @return true on success, Note: the failed photos will have userId of null, thus, this method can also be used to check the existence of the given photos.
426   */
427  public boolean setOwners(PhotoList photos){
428    if(PhotoList.isEmpty(photos)){
429      LOGGER.debug("Ignored empty "+PhotoList.class.toString());
430      return true;
431    }
432
433    List<String> guids = photos.getGUIDs();
434    if(guids == null){
435      LOGGER.debug("No guids.");
436      return false;
437    }
438
439    SolrQueryBuilder solr = new SolrQueryBuilder();
440    solr.addFields(FIELDS_SET_OWNERS);
441    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
442    PhotoList found = PhotoList.getPhotoList(getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST), Photo.class),null);
443    if(PhotoList.isEmpty(found)){
444      LOGGER.debug("No photos found.");
445      for(Photo photo : photos.getPhotos()){  // null all user ids
446        photo.setOwnerUserId(null);
447      }
448    }else{
449      for(Photo photo : photos.getPhotos()){
450        Photo foundPhoto = found.getPhoto(photo.getGUID());
451        if(foundPhoto != null){
452          photo.setOwnerUserId(foundPhoto.getOwnerUserId());
453        }else{
454          photo.setOwnerUserId(null);
455        }
456      } // for
457    }
458    return true;
459  }
460
461  /**
462   * Note: media objects, if present on the photos, will be updated/inserted, old objects will never be removed 
463   * (i.e. this will NOT SET object, but only updates the object details). If you want to remove previously existing
464   * media objects, use {@link service.tut.pori.contentanalysis.MediaObjectDAO}.
465   * 
466   * update the photo details IF the given userId has the permission to do so
467   * @param authenticatedUser
468   * @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.
469   * @param photos
470   * @return true on success
471   */
472  private boolean updatePhotos(UserIdentity authenticatedUser, boolean onlyNewer, PhotoList photos){
473    if(!UserIdentity.isValid(authenticatedUser)){
474      LOGGER.warn("Cannot update for null user.");
475      return false;
476    }
477    if(PhotoList.isEmpty(photos)){
478      LOGGER.debug("Ignored empty photo list.");
479      return true;
480    }
481    if(!PhotoList.isValid(photos)){
482      LOGGER.warn("Tried update with invalid photo list.");
483      return false;
484    }
485
486    LOGGER.debug("Updating photos...");
487    List<String> guids = photos.getGUIDs();
488    SolrQueryBuilder solr = new SolrQueryBuilder();
489    solr.addFields(FIELDS_UPDATE);
490    if(onlyNewer){
491      solr.addField(Definitions.SOLR_FIELD_UPDATED);
492    }
493    List<Photo> photoList = photos.getPhotos(); // the original list of photos
494    List<Photo> checkPhotos = new LinkedList<>(photoList); // make copy to preserve the original, this is used to validate that no bad data is left over
495    List<Photo> update = new ArrayList<>(photoList.size()); // contains the photos that will actual be updated
496    Date updated = new Date();
497    SimpleSolrTemplate t = getSolrTemplate(BEAN_ID_SOLR_SERVER);
498    
499    solr.clearCustomFilters();
500    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
501    PhotoList references = PhotoList.getPhotoList(t.queryForList(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST), Photo.class), null);
502    if(PhotoList.isEmpty(references)){
503      LOGGER.warn("Could not find the photos requested for update.");
504      return false;
505    }
506    for(Iterator<Photo> iter = checkPhotos.iterator(); iter.hasNext();){
507      Photo check = iter.next();
508      String cguid = check.getGUID();
509      Photo reference = references.getPhoto(cguid);
510      if(reference != null){
511        UserIdentity rUserId = reference.getOwnerUserId();
512        if(UserIdentity.equals(rUserId, check.getOwnerUserId()) && UserIdentity.equals(rUserId, authenticatedUser)){ // check that the user is not trying to change the userId of the photo, and that he/she actually owns the photo
513          iter.remove(); // in any case, remove the photo
514          Date cUpdated = check.getUpdated();
515          if(onlyNewer){
516            if(cUpdated == null){
517              LOGGER.warn("Ignored photo without updated timestamp, guid: "+cguid);
518              continue;
519            }else if(cUpdated.before(reference.getUpdated())){
520              LOGGER.debug("Ignored photo with older timestamp, guid: "+cguid);
521              continue;
522            }
523          }else if(cUpdated == null){
524            check.setUpdated(updated);  // make sure there is an updated timestamp
525          }
526          update.add(check);
527        }else{ // reference photo was found, but user ids did not match
528          LOGGER.warn("Permission denied for user: "+authenticatedUser.getUserId());
529          return false;
530        }
531      } // do not break, in case there are duplicates in the list
532    } //for
533
534    if(!checkPhotos.isEmpty()){ // something was left over
535      LOGGER.warn("Tried to update non-existing photos, or access was denied for user, id: "+authenticatedUser.getUserId());
536      return false;
537    }
538
539    if(update.isEmpty()){
540      LOGGER.debug("Nothing to update.");
541      return true;
542    }
543
544    if(!insertOrUpdateMediaObjects(photos)){
545      LOGGER.warn("Failed to insert/update media objects.");
546      return false;
547    }
548
549    if(t.addBeans(update).getStatus() == SolrException.ErrorCode.UNKNOWN.code){
550      return true;
551    }else{
552      LOGGER.warn("Update failed.");
553      return false;
554    }
555  }
556
557  /**
558   * Update the photos only the given photos have newer timestamps than the ones in the database
559   * 
560   * Note: media objects, if present on the photos, will be updated/inserted, old objects will never be removed 
561   * (i.e. this will NOT SET object, but only updates the object details). If you want to remove previously existing
562   * media objects, use {@link service.tut.pori.contentanalysis.MediaObjectDAO}.
563   * 
564   * @param authenticatedUser
565   * @param photos
566   * @return true on success, note that <i>nothing updated</i> also equals to true if no other errors occur
567   */
568  public boolean updatePhotosIfNewer(UserIdentity authenticatedUser, PhotoList photos){
569    return updatePhotos(authenticatedUser, true, photos);
570  }
571
572  /**
573   * Update the photos only the given photos have newer timestamps than the ones in the database
574   * 
575   * Note: media objects, if present on the photos, will be updated/inserted, old objects will never be removed 
576   * (i.e. this will NOT SET object, but only updates the object details). If you want to remove previously existing
577   * media objects, use {@link service.tut.pori.contentanalysis.MediaObjectDAO}.
578   * 
579   * @param authenticatedUser
580   * @param photos
581   * @return true on success
582   */
583  public boolean updatePhotos(UserIdentity authenticatedUser, PhotoList photos) {
584    return updatePhotos(authenticatedUser, false, photos);
585  }
586
587  /**
588   * resolves ids for the given list of objects and performs insert for non-existing objects and update for others.
589   * 
590   * This is a helper method for update methods.
591   * 
592   * @param photos
593   * @return true on success
594   */
595  private boolean insertOrUpdateMediaObjects(PhotoList photos){
596    MediaObjectList combined = new MediaObjectList();
597    List<Photo> photoList = photos.getPhotos();
598    for(Photo photo : photoList){
599      MediaObjectList objects = photo.getMediaObjects();
600      if(!MediaObjectList.isEmpty(objects)){
601        combined.addMediaObjects(objects);
602      }
603    }
604
605    if(MediaObjectList.isEmpty(combined)){
606      LOGGER.debug("No objects to update or insert.");
607      return true;
608    }
609
610    _mediaObjectDAO.resolveObjectIds(combined); // resolve ids to find out what needs to be updated
611    MediaObjectList update = new MediaObjectList();
612    MediaObjectList insert = new MediaObjectList();
613    for(Photo photo : photoList){
614      MediaObjectList objects = photo.getMediaObjects();
615      if(!MediaObjectList.isEmpty(objects)){
616        UserIdentity userId = photo.getOwnerUserId();
617        for(MediaObject object : objects.getMediaObjects()){
618          Visibility visibility = photo.getVisibility();
619          UserIdentity oUserId = object.getOwnerUserId();
620          if(oUserId == null){
621            object.setOwnerUserId((oUserId = userId));
622          }
623          if(!UserIdentity.equals(userId, oUserId)){
624            LOGGER.warn("For update photo and media object must have the same user.");
625            return false;
626          }else if(StringUtils.isBlank(object.getMediaObjectId())){ // a new one
627            insert.addMediaObject(object);
628          }else{ // old one
629            update.addMediaObject(object);
630          } // else
631          if(object.getVisibility() == null){
632            LOGGER.debug("Object visibility missing, using photo's visibility.");
633            object.setVisibility(visibility);
634          }
635        } // for
636      } // if
637    }
638
639    boolean associate = true;
640    if(MediaObjectList.isEmpty(insert)){
641      LOGGER.debug("No new objects to insert.");
642      associate = false;
643    }else if(!insert(insert)){
644      LOGGER.warn("Failed to insert new objects.");
645      return false;
646    }
647
648    if(MediaObjectList.isEmpty(update)){
649      associate = (associate ? true : false);
650      LOGGER.debug("No objects to update.");
651    }else if(!_mediaObjectDAO.update(update)){
652      LOGGER.warn("Failed to update objects.");
653      return false;
654    }
655
656    if(associate){
657      LOGGER.debug("Associating...");
658      associate(photos);
659    }else{
660      LOGGER.debug("Skipping association: not needed.");
661    }
662
663    return true;
664  }
665  
666  /**
667   * 
668   * @param authenticatedUser
669   * @param guids
670   * @return access details for the photos, or null if none of the photo exist
671   */
672  public List<AccessDetails> getAccessDetails(UserIdentity authenticatedUser, Collection<String> guids){
673    SolrQueryBuilder solr = new SolrQueryBuilder();
674    solr.addFields(FIELDS_GET_ACCESS_DETAILS);
675    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guids));
676
677    List<Photo> photos = getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST), Photo.class);
678    if(photos == null){
679      LOGGER.debug("Guids do not exist.");
680      return null;
681    }
682    List<AccessDetails> details = new ArrayList<>(photos.size());
683    for(Photo photo : photos){
684      details.add(AccessDetails.getAccessDetails(authenticatedUser, photo));
685    }
686    return details;
687  }
688
689  /**
690   * 
691   * @param authenticatedUser
692   * @param guid
693   * @return access details for the photo, or null if the photo does not exist
694   */
695  public AccessDetails getAccessDetails(UserIdentity authenticatedUser, String guid) {
696    SolrQueryBuilder solr = new SolrQueryBuilder();
697    solr.addFields(FIELDS_GET_ACCESS_DETAILS);
698    solr.addCustomFilter(new AndQueryFilter(SOLR_FIELD_ID, guid));
699
700    List<Photo> photos = getSolrTemplate(BEAN_ID_SOLR_SERVER).queryForList(solr.toSolrQuery(Definitions.ELEMENT_PHOTOLIST), Photo.class);
701    if(photos == null){
702      LOGGER.debug("GUID does not exist: "+guid);
703      return null;
704    }
705    return AccessDetails.getAccessDetails(authenticatedUser, photos.iterator().next());
706  }
707
708  /**
709   * Note: this will NOT remove the media object, and it will only remove the association between the given GUID and media object id.
710   * Use {@link service.tut.pori.contentanalysis.MediaObjectDAO} if you want to remove the media objects.
711   * 
712   * @param guid if null, the given media object will be de-associated from all GUIDs
713   * @param mediaObjectId if null, all media objects for the given GUID will be de-associated
714   */
715  public void deassociate(String guid, String mediaObjectId) {
716    _associationDAO.deassociate(guid, mediaObjectId);
717  }
718  
719  /**
720   * create photo-media object associations from the given photo list
721   * 
722   * @param photos
723   */
724  public void associate(PhotoList photos){
725    _associationDAO.associate(photos.getPhotos());
726  }
727}