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.reference;
017
018import java.io.IOException;
019import java.util.EnumSet;
020import java.util.Iterator;
021import java.util.List;
022
023import org.apache.commons.lang3.ArrayUtils;
024import org.apache.commons.lang3.StringUtils;
025import org.apache.http.client.methods.HttpPost;
026import org.apache.http.entity.StringEntity;
027import org.apache.http.impl.client.BasicResponseHandler;
028import org.apache.http.impl.client.CloseableHttpClient;
029import org.apache.http.impl.client.HttpClients;
030import org.apache.log4j.Logger;
031
032import service.tut.pori.contentanalysis.AnalysisBackend;
033import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
034import service.tut.pori.contentanalysis.AsyncTask.TaskType;
035import service.tut.pori.contentanalysis.BackendStatus;
036import service.tut.pori.contentanalysis.BackendStatusList;
037import service.tut.pori.contentanalysis.CAContentCore.ServiceType;
038import service.tut.pori.contentanalysis.CAContentCore.Visibility;
039import service.tut.pori.contentanalysis.Definitions;
040import service.tut.pori.contentanalysis.DeletedPhotoList;
041import service.tut.pori.contentanalysis.DissimilarPhotoList;
042import service.tut.pori.contentanalysis.MediaObject;
043import service.tut.pori.contentanalysis.MediaObjectList;
044import service.tut.pori.contentanalysis.Photo;
045import service.tut.pori.contentanalysis.PhotoFeedbackList;
046import service.tut.pori.contentanalysis.PhotoList;
047import service.tut.pori.contentanalysis.PhotoParameters;
048import service.tut.pori.contentanalysis.PhotoParameters.AnalysisType;
049import service.tut.pori.contentanalysis.PhotoTaskDetails;
050import service.tut.pori.contentanalysis.PhotoTaskResponse;
051import service.tut.pori.contentanalysis.ReferencePhotoList;
052import service.tut.pori.contentanalysis.ResultInfo;
053import service.tut.pori.contentanalysis.SimilarPhotoList;
054import service.tut.pori.contentanalysis.VisualShape;
055import core.tut.pori.context.ServiceInitializer;
056import core.tut.pori.http.RedirectResponse;
057import core.tut.pori.http.Response;
058import core.tut.pori.http.parameters.DataGroups;
059import core.tut.pori.http.parameters.Limits;
060import core.tut.pori.users.UserIdentity;
061import core.tut.pori.utils.XMLFormatter;
062
063/**
064 * The reference implementations for Content Analysis Service.
065 *
066 */
067public final class CAReferenceCore {
068  private static final CAXMLObjectCreator CREATOR = new CAXMLObjectCreator(null);
069  private static final Limits DEFAULT_LIMITS = new Limits(0, 0);  // default number of limits for references
070  private static final DataGroups DATAGROUPS_ALL = new DataGroups(DataGroups.DATA_GROUP_ALL);
071  private static final DataGroups DATAGROUPS_BACKEND_RESPONSE = new DataGroups(CAXMLObjectCreator.DATA_GROUP_BACKEND_RESPONSE); // data groups for add task callback
072  private static final String EXAMPLE_URI = "http://otula.pori.tut.fi/d2i/leijona_album_art.jpg";
073  private static final Logger LOGGER = Logger.getLogger(CAReferenceCore.class);
074  
075  /**
076   * 
077   */
078  private CAReferenceCore(){
079    // nothing needed
080  }
081
082  /**
083   * This performs a trivial check for the task contents, checking for the presence of a few key values.
084   * The validity of the actual task contents will not checked.
085   * 
086   * @param response
087   */
088  public static void taskFinished(PhotoTaskResponse response) {
089    Integer tBackendId = response.getBackendId();
090    if(tBackendId == null){
091      throw new IllegalArgumentException("Invalid backendId: "+tBackendId);
092    }
093    Long tTaskId = response.getTaskId();
094    if(tTaskId == null){
095      throw new IllegalArgumentException("Invalid taskId: "+tTaskId);
096    }
097
098    TaskStatus status = response.getStatus();
099    if(status == null){
100      throw new IllegalArgumentException("TaskStatus is invalid or missing.");
101    }
102
103    TaskType type = response.getTaskType();
104    if(type == null){
105      throw new IllegalArgumentException("TaskType is invalid or missing.");
106    }
107
108    try{
109      switch(type){
110        case ANALYSIS:
111          PhotoList pl = response.getPhotoList();
112          if(!PhotoList.isEmpty(pl)){
113            if(!PhotoList.isValid(pl)){
114              LOGGER.warn("Invalid "+Definitions.ELEMENT_PHOTOLIST);
115            }
116            for(Photo p : response.getPhotoList().getPhotos()){
117              MediaObjectList vObjects = p.getMediaObjects();
118              if(MediaObjectList.isEmpty(vObjects)){
119                LOGGER.info("No media objects for photo, GUID: "+p.getGUID());
120              }else if(!MediaObjectList.isValid(vObjects)){
121                throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_MEDIA_OBJECTLIST);
122              }
123            }
124          }
125          break;
126        case BACKEND_FEEDBACK: // should not have any content, so accept anything
127          break;
128        case FEEDBACK:
129          if(!PhotoList.isValid(response.getPhotoList())){
130            throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_PHOTOLIST);
131          }
132          break;
133        case SEARCH:
134        default:      
135          throw new IllegalArgumentException("Tasks of type: "+type.name()+" are not supported by this validator.");
136      }
137    }catch(ClassCastException ex){
138      LOGGER.debug(ex, ex);
139      throw new IllegalArgumentException("Task content data was not of the expected type.");
140    }
141  }
142
143  /**
144   * This performs a trivial check for the task contents, checking for the presence of a few key values.
145   * The validity of the actual task contents will not be checked.
146   * 
147   * @param taskDetails
148   */
149  public static void addTask(PhotoTaskDetails taskDetails) {
150    Integer tBackendId = taskDetails.getBackendId();
151    if(tBackendId == null){
152      throw new IllegalArgumentException("Invalid backendId: "+tBackendId);
153    }
154    Long tTaskId = taskDetails.getTaskId();
155    if(tTaskId == null){
156      throw new IllegalArgumentException("Invalid taskId: "+tTaskId);
157    }
158
159    String uri = taskDetails.getCallbackUri();
160    if(StringUtils.isBlank(uri)){
161      throw new IllegalArgumentException("Invalid callbackUri: "+uri);
162    }
163
164    TaskType type = taskDetails.getTaskType();
165    if(type == null){
166      throw new IllegalArgumentException("TaskType is invalid or missing.");
167    }
168
169    switch(type){
170      case BACKEND_FEEDBACK:
171        if(!PhotoList.isValid(taskDetails.getPhotoList())){
172          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_PHOTOLIST);
173        }
174        addTaskAsyncCallback(taskDetails, null);
175        break;
176      case ANALYSIS:
177        PhotoList photoList = taskDetails.getPhotoList();
178        if(!PhotoList.isValid(photoList)){
179          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_PHOTOLIST);
180        }
181        if(taskDetails.getSimilarPhotoList() != null || taskDetails.getDissimilarPhotoList() != null || taskDetails.getDeletedPhotoList() != null){
182          throw new IllegalArgumentException(Definitions.ELEMENT_SIMILAR_PHOTOLIST+", "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST+" or "+Definitions.ELEMENT_DELETED_PHOTOLIST+" cannot appear in tasks of type: "+TaskType.ANALYSIS.name()+" or "+TaskType.SEARCH.name());
183        }
184        PhotoParameters ap = taskDetails.getTaskParameters();
185        for(Photo photo : photoList.getPhotos()){
186          MediaObjectList mediaObjects = CREATOR.createMediaObjectList((ap == null ? null : ap.getAnalysisTypes()), DATAGROUPS_BACKEND_RESPONSE, DEFAULT_LIMITS, null);
187          for(service.tut.pori.contentanalysis.MediaObject o : mediaObjects.getMediaObjects()){
188            o.setOwnerUserId(null);
189            o.setBackendId(tBackendId);
190            o.setMediaObjectId(null);
191            o.setServiceType(null);
192          }
193          photo.addackendStatus(new BackendStatus(new AnalysisBackend(tBackendId), TaskStatus.COMPLETED));
194          photo.addMediaObjects(mediaObjects);
195        }
196        addTaskAsyncCallback(taskDetails, photoList);
197        break;
198      case FEEDBACK:
199        if(taskDetails.getReferencePhotoList() != null){
200          if(taskDetails.getSimilarPhotoList() == null && taskDetails.getDissimilarPhotoList() == null){
201            throw new IllegalArgumentException(Definitions.ELEMENT_REFERENCE_PHOTOLIST+" requires at least one of "+Definitions.ELEMENT_SIMILAR_PHOTOLIST+" or "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST);
202          }
203          if(taskDetails.getDeletedPhotoList() != null){
204            throw new IllegalArgumentException(Definitions.ELEMENT_DELETED_PHOTOLIST+" cannot appear together with "+Definitions.ELEMENT_REFERENCE_PHOTOLIST);
205          }
206          if(taskDetails.getPhotoList() != null){
207            throw new IllegalArgumentException(Definitions.ELEMENT_PHOTOLIST+" cannot appear together with "+Definitions.ELEMENT_REFERENCE_PHOTOLIST);
208          }
209        }else if(taskDetails.getPhotoList() != null){
210          if(taskDetails.getSimilarPhotoList() != null || taskDetails.getDissimilarPhotoList() != null || taskDetails.getDeletedPhotoList() != null){
211            throw new IllegalArgumentException(Definitions.ELEMENT_PHOTOLIST+" cannot appear together with "+Definitions.ELEMENT_DELETED_PHOTOLIST+", "+Definitions.ELEMENT_SIMILAR_PHOTOLIST+" or "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST);
212          }
213        }else if(taskDetails.getDeletedPhotoList() != null){
214          if(taskDetails.getSimilarPhotoList() != null || taskDetails.getDissimilarPhotoList() != null){
215            throw new IllegalArgumentException(Definitions.ELEMENT_DELETED_PHOTOLIST+" cannot appear together with "+Definitions.ELEMENT_SIMILAR_PHOTOLIST+" or "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST);
216          }
217        }else{
218          throw new IllegalArgumentException("At least one of "+Definitions.ELEMENT_PHOTOLIST+", "+ Definitions.ELEMENT_REFERENCE_PHOTOLIST+", "+Definitions.ELEMENT_DELETED_PHOTOLIST+", "+Definitions.ELEMENT_SIMILAR_PHOTOLIST+" or "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST+" must be present");
219        }
220
221        if(taskDetails.getPhotoList() != null && !PhotoList.isValid(taskDetails.getPhotoList())){
222          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_PHOTOLIST);
223        }
224
225        if(taskDetails.getSimilarPhotoList() != null && !SimilarPhotoList.isValid(taskDetails.getSimilarPhotoList())){
226          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_SIMILAR_PHOTOLIST);
227        }
228
229        if(taskDetails.getDissimilarPhotoList() != null && !DissimilarPhotoList.isValid(taskDetails.getDissimilarPhotoList())){
230          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_DISSIMILAR_PHOTOLIST);
231        }
232
233        if(taskDetails.getDeletedPhotoList() != null && !DeletedPhotoList.isValid(taskDetails.getDeletedPhotoList())){
234          throw new IllegalArgumentException("Invalid "+Definitions.ELEMENT_DELETED_PHOTOLIST);
235        }
236        addTaskAsyncCallback(taskDetails, null);
237        break;
238      case SEARCH:
239        LOGGER.warn("Accepting task of type "+TaskType.SEARCH.name()+" without validation.");
240        break;
241      default:
242        throw new IllegalArgumentException("Tasks of type: "+type.name()+" are not supported by this validator.");
243    }
244  }
245
246  /**
247   * Call asynchronously the callback given in the details, returning an example task response
248   * 
249   * @param details
250   * @param photoList
251   * @see service.tut.pori.contentanalysis.PhotoTaskResponse
252   */
253  public static void addTaskAsyncCallback(PhotoTaskDetails details, PhotoList photoList) {
254    HttpPost post = new HttpPost(details.getCallbackUri());
255    PhotoTaskResponse r = new PhotoTaskResponse();
256    r.setBackendId(details.getBackendId());
257    r.setTaskId(details.getTaskId());
258    r.setStatus(TaskStatus.COMPLETED);
259    r.setTaskType(details.getTaskType());
260    r.setPhotoList(photoList);
261    post.setEntity(new StringEntity((new XMLFormatter()).toString(r), core.tut.pori.http.Definitions.ENCODING_UTF8));
262    executeAsyncCallback(post);
263  }
264
265  /**
266   * 
267   * @param post
268   */
269  public static void executeAsyncCallback(final HttpPost post){
270    ServiceInitializer.getExecutorHandler().getExecutor().execute(
271      new Runnable() {
272        @Override
273        public void run() {
274          try (CloseableHttpClient client = HttpClients.createDefault()) {
275            LOGGER.debug("Waiting "+service.tut.pori.contentanalysis.reference.Definitions.ASYNC_CALLBACK_DELAY/1000+" seconds...");
276            Thread.sleep(service.tut.pori.contentanalysis.reference.Definitions.ASYNC_CALLBACK_DELAY);
277  
278            LOGGER.debug("Calling uri: "+post.getURI().toString());
279            LOGGER.debug("Server responded: "+client.execute(post, new BasicResponseHandler()));
280          } catch (IOException | InterruptedException ex) {
281            LOGGER.error(ex, ex);
282          }
283        }
284    });
285  }
286
287  /**
288   * 
289   * @param backendId
290   * @param taskId
291   * @param dataGroups
292   * @param limits
293   * @return an example response for the given values
294   */
295  public static Response queryTaskDetails(Integer backendId, Long taskId, DataGroups dataGroups, Limits limits) {
296    if(limits.getMaxItems(Definitions.ELEMENT_PHOTOLIST) >= Limits.DEFAULT_MAX_ITEMS){
297      LOGGER.debug("Reseting limits for "+Definitions.ELEMENT_PHOTOLIST+": max items was "+Limits.DEFAULT_MAX_ITEMS);
298      limits.setTypeLimits(DEFAULT_LIMITS.getStartItem(Definitions.ELEMENT_PHOTOLIST), DEFAULT_LIMITS.getEndItem(Definitions.ELEMENT_PHOTOLIST), Definitions.ELEMENT_PHOTOLIST); // startItem makes no difference for random
299    }
300
301    if(limits.getMaxItems(Definitions.ELEMENT_MEDIA_OBJECTLIST) >= Limits.DEFAULT_MAX_ITEMS){
302      LOGGER.debug("Reseting limits for "+Definitions.ELEMENT_MEDIA_OBJECTLIST+": max items was "+Limits.DEFAULT_MAX_ITEMS);
303      limits.setTypeLimits(DEFAULT_LIMITS.getStartItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), DEFAULT_LIMITS.getEndItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), Definitions.ELEMENT_MEDIA_OBJECTLIST); // startItem makes no difference for random
304    }
305    limits.setTypeLimits(-1, -1, null); // disable all other photo lists
306    return new Response(CREATOR.createPhotoTaskDetails(backendId, dataGroups, limits, taskId, TaskType.ANALYSIS));
307  }
308
309  /**
310   * 
311   * @param taskId
312   * @param dataGroups
313   * @param limits
314   * @return an example response for the given values
315   */
316  public static Response queryTaskStatus(Long taskId, DataGroups dataGroups, Limits limits) {
317    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
318      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
319      limits = DEFAULT_LIMITS; // startItem makes no difference for random
320    }
321    return new Response(CREATOR.createTaskResponse(null, dataGroups, limits, taskId, TaskType.ANALYSIS));
322  }
323
324  /**
325   * back-end API variant
326   * @param analysisTypes 
327   * @param serviceTypes
328   * @param url
329   * @param userIds
330   * @param dataGroups
331   * @param limits
332   * @return an example response for the given values
333   */
334  public static Response searchSimilarByContent(EnumSet<AnalysisType> analysisTypes, String url, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIds) {
335    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
336      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
337      limits = DEFAULT_LIMITS; // startItem makes no difference for random
338    }
339    return new Response(CREATOR.createSearchResults(null, dataGroups, limits, serviceTypes, userIds, null));
340  }
341
342  /**
343   * Back-end API variation of search by GUID
344   * 
345   * @param analysisTypes not used
346   * @param serviceTypes
347   * @param guid
348   * @param userIds
349   * @param dataGroups
350   * @param limits
351   * @return an example response for the given values
352   */
353  public static Response searchSimilarById(EnumSet<AnalysisType> analysisTypes, String guid, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIds) {
354    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
355      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
356      limits = DEFAULT_LIMITS; // startItem makes no difference for random
357    }
358    return new Response(CREATOR.createSearchResults(guid, dataGroups, limits, serviceTypes, userIds, null));
359  }
360
361  /**
362   * 
363   * @param authenticatedUser
364   * @param keywords
365   * @param dataGroups
366   * @param limits
367   * @param serviceTypes
368   * @param userIdFilters
369   * @return an example response for the given values
370   */
371  public static Response searchByKeyword(UserIdentity authenticatedUser, List<String> keywords, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
372    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
373    if(limits.getMaxItems() >= Limits.DEFAULT_MAX_ITEMS){
374      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
375      limits = DEFAULT_LIMITS; // startItem makes no difference for random
376    }
377    return new Response(CREATOR.createSearchResults(null, dataGroups, limits, serviceTypes, userIdFilters, MediaObjectList.getMediaObjectListFromKeywords(keywords)));
378  }
379
380  /**
381   * Client API variation of search by GUID
382   * 
383   * @param authenticatedUser
384   * @param analysisTypes
385   * @param guid
386   * @param dataGroups
387   * @param limits
388   * @param serviceTypes
389   * @param userIdFilters
390   * @return an example response for the given values
391   */
392  public static Response searchSimilarById(UserIdentity authenticatedUser, EnumSet<AnalysisType> analysisTypes, String guid, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
393    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
394    return searchSimilarById(analysisTypes, guid, dataGroups, limits, serviceTypes, userIdFilters); // we can directly call the back-end API reference implementation
395  }
396
397  /**
398   * 
399   * @param authenticatedUser
400   * @param serviceId
401   * @param guid
402   * @return an example response for the given values
403   */
404  public static RedirectResponse generateTargetUrl(UserIdentity authenticatedUser, ServiceType serviceId, String guid) {
405    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
406    return new RedirectResponse(EXAMPLE_URI);
407  }
408
409  /**
410   * 
411   * @param authenticatedUser
412   * @param objects
413   * @param dataGroups
414   * @param limits
415   * @param serviceTypes
416   * @param userIdFilters
417   * @return an example response for the given values
418   */
419  public static Response similarPhotosByObject(UserIdentity authenticatedUser, MediaObjectList objects, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
420    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
421    if(limits.getMaxItems(Definitions.ELEMENT_PHOTOLIST) >= Limits.DEFAULT_MAX_ITEMS){
422      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
423      limits = DEFAULT_LIMITS; // startItem makes no difference for random
424    }
425    return new Response(CREATOR.createSearchResults(null, dataGroups, limits, serviceTypes, userIdFilters, objects));
426  }
427
428  /**
429   * 
430   * @param photoList
431   * @param authenticatedUser
432   */
433  public static void updatePhotos(UserIdentity authenticatedUser, PhotoList photoList) {
434    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
435    if(!PhotoList.isValid(photoList)){
436      throw new IllegalArgumentException("Received empty or invalid photoList.");
437    }
438  }
439
440  /**
441   * client API variant
442   * 
443   * @param url
444   * @param serviceTypes
445   * @param authenticatedUser
446   * @param analysisTypes 
447   * @param userIdFilters
448   * @param dataGroups
449   * @param limits
450   * @return an example response for the given values
451   */
452  public static Response searchByContent(UserIdentity authenticatedUser, EnumSet<AnalysisType> analysisTypes, String url, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilters) {
453    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
454    if(limits.getMaxItems(Definitions.ELEMENT_PHOTOLIST) >= Limits.DEFAULT_MAX_ITEMS){
455      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
456      limits = DEFAULT_LIMITS; // startItem makes no difference for random
457    }
458    return searchSimilarByContent(analysisTypes, url, dataGroups, limits, serviceTypes, userIdFilters); // we can use the back-end variant
459  }
460
461  /**
462   * 
463   * @param authenticatedUser
464   * @param guids
465   * @param dataGroups
466   * @param limits
467   * @param serviceTypes
468   * @param userIdFilter 
469   * @return an example response for the given values
470   */
471  public static Response getPhotos(UserIdentity authenticatedUser, List<String> guids, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, long[] userIdFilter) {
472    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
473    if(limits.getMaxItems(Definitions.ELEMENT_PHOTOLIST) >= Limits.DEFAULT_MAX_ITEMS){
474      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
475      limits = DEFAULT_LIMITS; // startItem makes no difference for random
476    }
477    int userIdCount = (ArrayUtils.isEmpty(userIdFilter) ? 0 : userIdFilter.length);
478    PhotoList list = CREATOR.createPhotoList(null, dataGroups, limits, serviceTypes, null);
479    if(list != null && guids != null && !guids.isEmpty()){
480      for(Iterator<Photo> iter = list.getPhotos().iterator();iter.hasNext();){  // remove all extra GUIDs, we could also modify the limit parameter, but for this testing method, the implementation does not matter
481        Photo photo = iter.next();
482        if(guids.isEmpty()){  // we have used up all given GUIDs
483          iter.remove();
484        }else{
485          photo.setGUID(guids.remove(0));
486          photo.setVisibility(Visibility.PUBLIC); // there could also be private photos for the authenticated user, but to make sure the results are valid, return only PUBLIC photos
487        }
488        if(userIdCount > 1){
489          photo.setOwnerUserId(new UserIdentity(userIdFilter[CREATOR.getRandom().nextInt(userIdCount)]));
490        }
491      } // for
492    }
493    return new Response(list);
494  }
495
496  /**
497   * 
498   * @param authenticatedUser
499   * @param feedbackList
500   */
501  public static void similarityFeedback(UserIdentity authenticatedUser, PhotoFeedbackList feedbackList) {
502    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
503    if(!PhotoFeedbackList.isValid(feedbackList)){
504      throw new IllegalArgumentException("Received empty or invalid feedbackList.");
505    }
506  }
507
508  /**
509   * 
510   * @param authenticatedUser
511   * @param dataGroups
512   * @param limits
513   * @param serviceTypes
514   * @param mediaObjectIds 
515   * @return an example response for the given values
516   */
517  public static Response getMediaObjects(UserIdentity authenticatedUser, DataGroups dataGroups, Limits limits, EnumSet<ServiceType> serviceTypes, List<String> mediaObjectIds) {
518    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
519
520    if(limits.getMaxItems(Definitions.ELEMENT_MEDIA_OBJECTLIST) >= Limits.DEFAULT_MAX_ITEMS){
521      LOGGER.debug("Reseting limits for "+Definitions.ELEMENT_MEDIA_OBJECTLIST+": max items was "+Limits.DEFAULT_MAX_ITEMS);
522      limits.setTypeLimits(DEFAULT_LIMITS.getStartItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), DEFAULT_LIMITS.getEndItem(Definitions.ELEMENT_MEDIA_OBJECTLIST), Definitions.ELEMENT_MEDIA_OBJECTLIST); // startItem makes no difference for random
523    }
524
525    MediaObjectList mediaObjects = CREATOR.createMediaObjectList(null, dataGroups, limits, serviceTypes);
526    if(!MediaObjectList.isEmpty(mediaObjects) && mediaObjectIds != null && !mediaObjectIds.isEmpty()){
527      Iterator<String> voidIter = mediaObjectIds.iterator();
528      for(Iterator<MediaObject> voIter = mediaObjects.getMediaObjects().iterator(); voIter.hasNext();){
529        MediaObject vo = voIter.next();
530        if(voidIter.hasNext()){
531          vo.setMediaObjectId(voidIter.next());
532        }else{
533          voIter.remove(); // we have used all available media object ids
534        }
535      } // for
536    }
537    return new Response(mediaObjects);
538  }
539
540  /**
541   * 
542   * @param authenticatedUser
543   * @param guids
544   */
545  public static void deletePhotos(UserIdentity authenticatedUser, List<String> guids) {
546    LOGGER.info((authenticatedUser == null ? "No logged in user." : "Ignoring the logged in user, id: "+authenticatedUser.getUserId()));  // only notify of the logged in status
547  }
548
549  /**
550   * 
551   * @param limits
552   * @return generated feedback list
553   */
554  public static PhotoFeedbackList generateFeedbackList(Limits limits) {
555    return CREATOR.createFeedbackList(limits);
556  }
557
558  /**
559   * 
560   * @param dataGroups has only effect for photo list, if null or empty, data groups {@value core.tut.pori.http.parameters.DataGroups#DATA_GROUP_ALL}
561   * @param limits
562   * @param cls 
563   * @return generated photo list
564   * @throws IllegalArgumentException 
565   */
566  public static PhotoList generatePhotoList(DataGroups dataGroups, Limits limits, Class<? extends PhotoList> cls) throws IllegalArgumentException {
567    if(PhotoList.class == cls){
568      return CREATOR.createPhotoList(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), limits, null, null);
569    }else if(DeletedPhotoList.class == cls){
570      return CREATOR.createDeletedPhotoList(limits);
571    }else if(DissimilarPhotoList.class == cls){
572      return CREATOR.createDissimilarPhotoList(limits);
573    }else if(SimilarPhotoList.class == cls){
574      return CREATOR.createSimilarPhotoList(limits);
575    }else if(ReferencePhotoList.class == cls){
576      return CREATOR.createReferencePhotoList(limits);
577    }else{
578      throw new IllegalArgumentException("Unsupported class : "+cls);
579    }
580  }
581
582  /**
583   * 
584   * @param dataGroups has only effect for photo list, if null or empty, data groups {@value core.tut.pori.http.parameters.DataGroups#DATA_GROUP_ALL}
585   * @param limits
586   * @return generated media object list
587   */
588  public static MediaObjectList generateMediaObjectList(DataGroups dataGroups, Limits limits) {
589    return CREATOR.createMediaObjectList(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), limits, null);
590  }
591
592  /**
593   * 
594   * @param limits
595   * @return generated task response
596   */
597  public static PhotoTaskResponse generateTaskResponse(Limits limits) {
598    return CREATOR.createTaskResponse(null, DATAGROUPS_BACKEND_RESPONSE, limits, null, TaskType.ANALYSIS);
599  }
600
601  /**
602   * 
603   * @param dataGroups has only effect for photo list, if null or empty, data groups {@value core.tut.pori.http.parameters.DataGroups#DATA_GROUP_ALL}
604   * @param limits
605   * @param taskType 
606   * @return generated task details
607   * @throws IllegalArgumentException 
608   */
609  public static PhotoTaskDetails generatePhotoTaskDetails(DataGroups dataGroups, Limits limits, TaskType taskType) throws IllegalArgumentException {
610    switch(taskType){
611      case ANALYSIS:
612      case BACKEND_FEEDBACK:
613      case FEEDBACK:
614        break;
615      default:
616        throw new IllegalArgumentException("Unsupported task type: "+taskType.name());
617    }
618    limits.setTypeLimits(-1, -1, Definitions.ELEMENT_BACKEND_STATUS_LIST); // do not add back-end status list
619    return CREATOR.createPhotoTaskDetails((DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), limits, taskType);
620  }
621
622  /**
623   * 
624   * @param limits
625   * @return generated back-end status list
626   */
627  public static BackendStatusList generateBackendStatusList(Limits limits) {
628    int count = limits.getMaxItems();
629    if(count >= Limits.DEFAULT_MAX_ITEMS){
630      LOGGER.debug("Reseting limits: Default max items was "+Limits.DEFAULT_MAX_ITEMS);
631      count = DEFAULT_LIMITS.getMaxItems(Definitions.ELEMENT_BACKEND_STATUS_LIST); // startItem makes no difference for random
632    }
633    return CREATOR.createBackendStatusContainer(count);
634  }
635
636  /**
637   * 
638   * @param dataGroups has only effect for photo list, if null or empty, data groups {@value core.tut.pori.http.parameters.DataGroups#DATA_GROUP_ALL}
639   * @return generated photo
640   */
641  public static Photo generatePhoto(DataGroups dataGroups) {
642    return CREATOR.createPhoto(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), DEFAULT_LIMITS, null, null);
643  }
644
645  /**
646   * 
647   * @return generated back-end status
648   */
649  public static BackendStatus generateBackendStatus() {
650    return CREATOR.createBackendStatus(CREATOR.createBackendId());
651  }
652
653  /**
654   * 
655   * @return generated result info
656   */
657  public static ResultInfo generateResultInfo() {
658    return CREATOR.createResultInfo();
659  }
660
661  /**
662   * 
663   * @param dataGroups has only effect for photo list, if null or empty, data groups {@value core.tut.pori.http.parameters.DataGroups#DATA_GROUP_ALL}
664   * @return generated media object
665   */
666  public static MediaObject generateMediaObject(DataGroups dataGroups) {
667    return CREATOR.createMediaObject(null, (DataGroups.isEmpty(dataGroups) ? DATAGROUPS_ALL : dataGroups), null);
668  }
669
670  /**
671   * 
672   * @return generated visual shape
673   */
674  public static VisualShape generateVisualShape() {
675    return CREATOR.createVisualShape();
676  }
677
678  /**
679   * 
680   * @return generated analysis back-end
681   */
682  public static AnalysisBackend generateAnalysisBackend() {
683    return CREATOR.createAnalysisBackend();
684  }
685
686  /**
687   * 
688   * @return randomly generated analysis parameters
689   */
690  public static PhotoParameters generateAnalysisParameters() {
691    return CREATOR.createAnalysisParameters();
692  }
693}