001/**
002 * Copyright 2015 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.EnumSet;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026
027import org.apache.log4j.Logger;
028import org.springframework.beans.factory.annotation.Autowired;
029import org.springframework.jdbc.core.JdbcTemplate;
030
031import service.tut.pori.contentanalysis.AsyncTask.TaskStatus;
032import core.tut.pori.dao.SQLSelectBuilder;
033import core.tut.pori.dao.SQLSelectBuilder.OrderDirection;
034import core.tut.pori.dao.clause.AndClause;
035import core.tut.pori.dao.clause.SQLClause.SQLType;
036import core.tut.pori.http.parameters.DataGroups;
037import core.tut.pori.http.parameters.Limits;
038import core.tut.pori.utils.MediaUrlValidator.MediaType;
039
040/**
041 * Abstract base class for tasks that use media content.
042 * 
043 */
044public abstract class MediaTaskDAO extends TaskDAO {
045  private static final Logger LOGGER = Logger.getLogger(MediaTaskDAO.class);
046  /* tables */
047  private static final String TABLE_TASK_GUIDS = DATABASE +".ca_tasks_guids";
048  private static final String TABLE_TASK_GUIDS_STATUS = DATABASE +".ca_tasks_guids_status";
049  private static final String TABLE_TASK_MEDIA_OBJECTS = DATABASE +".ca_tasks_media_objects";
050  /* columns */
051  private static final String COLUMN_GUID_TYPE = "type";
052  /* sql scripts */
053  private static final String[] SQL_GET_MEDIA_STATUS_SELECT_COLUMNS = {Definitions.COLUMN_BACKEND_ID, COLUMN_GUID, Definitions.COLUMN_STATUS, COLUMN_MESSAGE};
054
055  private static final int[] SQL_DELETE_GUID_SQL_TYPES = {SQLType.STRING.toInt()};
056  private static final String SQL_DELETE_TASK_GUIDS = "DELETE FROM "+TABLE_TASK_GUIDS+" WHERE "+COLUMN_GUID+"=?";
057  private static final String SQL_DELETE_TASK_GUIDS_STATUS = "DELETE FROM "+TABLE_TASK_GUIDS_STATUS+" WHERE "+COLUMN_GUID+"=?";
058  private static final String SQL_DELETE_TASK_MEDIA_OBJECTS = "DELETE FROM "+TABLE_TASK_MEDIA_OBJECTS+" WHERE "+COLUMN_GUID+"=?";
059
060  private static final String SQL_INSERT_TASK_GUID = "INSERT INTO "+TABLE_TASK_GUIDS+" ("+COLUMN_TASK_ID+", "+COLUMN_GUID+", "+COLUMN_GUID_TYPE+", "+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW())";
061  private static final int[] SQL_INSERT_TASK_GUID_TYPES = {SQLType.LONG.toInt(), SQLType.STRING.toInt(), SQLType.INTEGER.toInt()};
062
063  private static final String SQL_INSERT_TASK_MEDIA_OBJECTS = "INSERT INTO "+TABLE_TASK_MEDIA_OBJECTS+" ("+COLUMN_TASK_ID+", "+COLUMN_GUID+", "+Definitions.COLUMN_MEDIA_OBJECT_ID+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW())";
064  private static final int[] SQL_INSERT_TASK_MEDIA_OBJECTS_TYPES = {SQLType.LONG.toInt(), SQLType.STRING.toInt(), SQLType.STRING.toInt()};
065
066  private static final String SQL_UPDATE_MEDIA_STATUS = "INSERT INTO "+TABLE_TASK_GUIDS_STATUS+" ("+Definitions.COLUMN_BACKEND_ID+", "+COLUMN_GUID+", "+COLUMN_TASK_ID+", "+Definitions.COLUMN_STATUS+", "+COLUMN_MESSAGE+", "+COLUMN_ROW_CREATED+") VALUES (?,?,?,?,?, NOW()) ON DUPLICATE KEY UPDATE "+COLUMN_TASK_ID+"=VALUES("+COLUMN_TASK_ID+"), "+Definitions.COLUMN_STATUS+"=VALUES("+Definitions.COLUMN_STATUS+"), "+COLUMN_MESSAGE+"=VALUES("+COLUMN_MESSAGE+"), "+COLUMN_ROW_UPDATED+"=NOW()";
067  private static final int[] SQL_UPDATE_MEDIA_STATUS_TYPES = {SQLType.INTEGER.toInt(), SQLType.STRING.toInt(), SQLType.LONG.toInt(), SQLType.INTEGER.toInt(), SQLType.STRING.toInt()};
068
069  @Autowired
070  private BackendDAO _backendDAO = null;
071  @Autowired
072  private MediaObjectDAO _mediaObjectDAO = null;
073  
074  /**
075   * the type of GUID in tasks GUIDs table
076   */
077  protected enum GUIDType{
078    /** GUID describes a basic media item */
079    MEDIA(0),
080    /** GUID describes a reference media item */
081    REFERENCE_MEDIA(1),
082    /** GUID describes media item similar with a reference item */
083    SIMILAR_MEDIA(2),
084    /** GUID describes media item dissimilar with a reference item */
085    DISSIMILAR_MEDIA(3),
086    /** GUID describes a deleted media item */
087    DELETED_MEDIA(4);
088
089    private int _value;
090
091    /**
092     * 
093     * @param value
094     */
095    private GUIDType(int value){
096      _value = value;
097    }
098
099    /**
100     * 
101     * @return type as a string
102     */
103    public int toInt(){
104      return _value;
105    }
106
107    /**
108     * 
109     * @param value
110     * @return value converted to type
111     * @throws IllegalArgumentException on bad value
112     */
113    public static GUIDType fromInt(int value) throws IllegalArgumentException {
114      for(GUIDType t : GUIDType.values()){
115        if(t._value == value){
116          return t;
117        }
118      }
119      throw new IllegalArgumentException("Bad "+GUIDType.class.toString()+" : "+value);
120    }
121  } // enum GUIDType
122
123  /**
124   * 
125   * @param limits 
126   * @param taskId
127   * @param type
128   * @return list of GUIDs for the given task, which are of the given type or null if none was found
129   */
130  protected List<String> getTaskGUIDs(Limits limits, Long taskId, GUIDType type) {
131    SQLSelectBuilder sql = new SQLSelectBuilder(TABLE_TASK_GUIDS);
132    sql.addSelectColumn(COLUMN_GUID);
133    sql.setLimits(limits);
134    sql.addWhereClause(new AndClause(COLUMN_TASK_ID, taskId, SQLType.LONG));
135    sql.addWhereClause(new AndClause(COLUMN_GUID_TYPE, type.toInt(), SQLType.INTEGER));
136    
137    List<String> guids = getJdbcTemplate().queryForList(sql.toSQLString(), sql.getValues(), sql.getValueTypes(), String.class);
138    if(guids.isEmpty()){
139      LOGGER.warn("No types found for task, id: "+taskId);
140      return null;
141    }
142    return guids;
143  } 
144  
145  /**
146   * This will also set photo statuses, if any are present. Note that even through status elements can appear in any photo list,
147   * creating two different lists with identical GUIDs, and conflicting status lists may create undefined behavior.
148   * 
149   * @param media
150   * @param taskId
151   * @param type 
152   */
153  protected <T extends Media> void insertTaskGUIDs(Collection<T> media, Long taskId, GUIDType type){
154    if(media == null || media.isEmpty()){
155      LOGGER.debug("No media for task, id: "+taskId);
156    }else{
157      JdbcTemplate t = getJdbcTemplate();
158      Object[] values = new Object[]{taskId,null,type.toInt()};
159      for(T m : media){
160        values[1] = m.getGUID();
161        t.update(SQL_INSERT_TASK_GUID, values, SQL_INSERT_TASK_GUID_TYPES);
162      }
163      updateMediaStatus(media, taskId);
164    }
165  }
166
167  /**
168   * 
169   * @param guid
170   * @param taskId
171   * @param mediaObjects list of media objects to add
172   */
173  protected void insertTaskMediaObjects(String guid, Long taskId, MediaObjectList mediaObjects){
174    if(MediaObjectList.isEmpty(mediaObjects)){
175      LOGGER.debug("Ignored empty media object list for GUID: "+guid);
176      return;
177    }
178    if(guid == null){
179      LOGGER.debug("Adding task media object without GUID.");
180    }
181    Object[] ob = {taskId, guid, null};
182    for(MediaObject vo : mediaObjects.getMediaObjects()){
183      ob[2] = vo.getMediaObjectId();
184      getJdbcTemplate().update(SQL_INSERT_TASK_MEDIA_OBJECTS, ob, SQL_INSERT_TASK_MEDIA_OBJECTS_TYPES);
185    }
186  }
187
188  /**
189   * 
190   * @param media list of media items
191   * @param taskId optional taskId. This should be (if given) the id of the most recent task.
192   */
193  public <T extends Media> void updateMediaStatus(Collection<T> media, Long taskId){
194    if(media == null || media.isEmpty()){
195      LOGGER.debug("No media given.");
196      return;
197    }
198
199    JdbcTemplate t = getJdbcTemplate();
200    Object[] values = new Object[]{null,null,taskId,null,null};
201    for(Media m : media){
202      values[1] = m.getGUID();
203      BackendStatusList statuses = m.getBackendStatus();
204      if(BackendStatusList.isEmpty(statuses)){
205        LOGGER.debug("No statuses for photo, guid: "+values[1]);
206      }else{
207        for(Iterator<BackendStatus> sIter = statuses.getBackendStatuses().iterator(); sIter.hasNext();){
208          BackendStatus s = sIter.next();
209          values[0] = s.getBackendId();
210          values[3] = s.getStatus().toInt();
211          values[4] = s.getMessage();
212          t.update(SQL_UPDATE_MEDIA_STATUS, values, SQL_UPDATE_MEDIA_STATUS_TYPES);
213        } // for
214      }
215    } // for
216  }
217
218  /**
219   * Removes the list of GUIDs from all tasks
220   * 
221   * @param guids
222   */
223  public void remove(Collection<String> guids){
224    if(guids == null || guids.isEmpty()){
225      LOGGER.debug("Ignored empty guids list.");
226      return;
227    }
228    JdbcTemplate t = getJdbcTemplate();
229    Object[] ob = new Object[1];
230    for(Iterator<String> iter = guids.iterator(); iter.hasNext();){
231      ob[0] = iter.next();
232      t.update(SQL_DELETE_TASK_GUIDS, ob, SQL_DELETE_GUID_SQL_TYPES);
233      t.update(SQL_DELETE_TASK_GUIDS_STATUS, ob, SQL_DELETE_GUID_SQL_TYPES);
234      t.update(SQL_DELETE_TASK_MEDIA_OBJECTS, ob, SQL_DELETE_GUID_SQL_TYPES);
235    }
236  }
237
238  @Override
239  public abstract AbstractTaskDetails getTask(Integer backendId, DataGroups dataGroups, Limits limits, Long taskId) throws IllegalArgumentException;
240
241  /**
242   * 
243   * @param dataGroups
244   * @param limits
245   * @param media
246   * @param mediaTypes media object media types to retrieve
247   * @param taskId
248   */
249  protected <T extends Media> void setMediaObjects(DataGroups dataGroups, Limits limits, Collection<T> media, EnumSet<MediaType> mediaTypes, Long taskId){
250    Map<String, List<String>> GUIDMediaObjectIdMap = getMediaObjectIds(limits, taskId);
251    if(GUIDMediaObjectIdMap == null){
252      return;
253    }
254
255    for(T m : media){
256      List<String> voids = GUIDMediaObjectIdMap.get(m.getGUID());
257      if(voids != null){
258        m.setMediaObjects(_mediaObjectDAO.getMediaObjects(dataGroups, null, mediaTypes, null, voids, null));
259      }
260    }
261  }
262
263  /**
264   * 
265   * @param limits
266   * @param taskId
267   * @return GUID-void map of media object ids or null if none were found for the given task
268   */
269  protected Map<String, List<String>> getMediaObjectIds(Limits limits, Long taskId){
270    SQLSelectBuilder sql = new SQLSelectBuilder(TABLE_TASK_MEDIA_OBJECTS);
271    sql.addSelectColumns(new String[]{COLUMN_GUID, Definitions.COLUMN_MEDIA_OBJECT_ID});
272    sql.addOrderBy(Definitions.COLUMN_MEDIA_OBJECT_ID, OrderDirection.ASCENDING);
273    sql.setLimits(limits);
274    sql.addWhereClause(new AndClause(COLUMN_TASK_ID, taskId, SQLType.LONG));
275    
276    List<Map<String,Object>> rows = getJdbcTemplate().queryForList(sql.toSQLString(Definitions.ELEMENT_MEDIA_OBJECTLIST), sql.getValues(), sql.getValueTypes());
277    if(rows.isEmpty()){
278      LOGGER.debug("No media objects for task, id: "+taskId);
279      return null;
280    }
281    HashMap<String, List<String>> map = new HashMap<>(rows.size());
282    for(Map<String, Object> row : rows){
283      String guid = (String) row.get(COLUMN_GUID);
284      List<String> voids = map.get(guid);
285      if(voids == null){
286        map.put(guid, (voids = new ArrayList<>()));
287      }
288      voids.add((String) row.get(Definitions.COLUMN_MEDIA_OBJECT_ID));
289    }
290    return map;
291  }
292
293  /**
294   * retrieves the media status information for the list of media-items if available
295   * 
296   * @param mediaList the list to which the status information is to be set. If previous information exists, it is overridden.
297   * @return the passed list
298   */
299  public <T extends Media> Collection<T> getMediaStatus(Collection<T> mediaList){
300    if(mediaList == null || mediaList.isEmpty()){
301      LOGGER.debug("Empty media list.");
302    }else{
303      SQLSelectBuilder sql = new SQLSelectBuilder(TABLE_TASK_GUIDS_STATUS);
304      
305      List<String> guids = new ArrayList<>(mediaList.size());
306      for (Media media : mediaList) {
307        guids.add(media.getGUID());
308      }     
309      sql.addSelectColumns(SQL_GET_MEDIA_STATUS_SELECT_COLUMNS);
310      sql.addWhereClause(new AndClause(COLUMN_GUID, guids.toArray(), SQLType.STRING));
311      List<Map<String,Object>> rows = getJdbcTemplate().queryForList(sql.toSQLString(Definitions.ELEMENT_BACKEND_STATUS_LIST), sql.getValues(), sql.getValueTypes());
312      if(rows.isEmpty()){
313        LOGGER.debug("No status for the given media items.");
314        return mediaList;
315      }
316
317      for(Iterator<Map<String, Object>> rIter = rows.iterator(); rIter.hasNext();){
318        Map<String,Object> row = rIter.next();
319        BackendStatus status = extractBackendStatus(row);
320        if(status == null){
321          LOGGER.debug("Ignored status for non-existent backend.");
322          continue;
323        }
324        String guid = (String) row.get(COLUMN_GUID);
325        for(Iterator<? extends Media> pIter = mediaList.iterator(); pIter.hasNext();){
326          Media p = pIter.next();
327          if(guid.equals(p.getGUID())){
328            p.addackendStatus(status);
329          }
330        } // for mediaList
331      } // for rows
332    } // else
333    return mediaList;
334  }
335  
336  /**
337   * extract new BackendStatus from the given row
338   * 
339   * @param row
340   * @return status extracted from the given row map
341   */
342  private BackendStatus extractBackendStatus(Map<String, Object> row){
343    BackendStatus s = new BackendStatus();
344    for(Entry<String, Object> e : row.entrySet()){
345      switch (e.getKey()) {
346        case Definitions.COLUMN_BACKEND_ID:
347          AnalysisBackend end = _backendDAO.getBackend((Integer)e.getValue());
348          if(end == null){
349            LOGGER.warn("Detected non-existent backend.");
350            return null;
351          }
352          s.setBackend(end);
353          break;
354        case Definitions.COLUMN_STATUS:
355          s.setStatus(TaskStatus.fromInt((Integer)e.getValue()));
356          break;
357        case COLUMN_MESSAGE:
358          s.setMessage((String) e.getValue());
359          break;
360        case COLUMN_GUID:   // valid column, but not handled by extractor
361        case COLUMN_TASK_ID:  // valid column, but not handled by extractor
362          break;
363        default:
364          throw new IllegalArgumentException("Unhandeled column: "+e.getKey());
365      }
366    } // for
367    return s;
368  }
369
370  /**
371   * @return the backendDAO
372   */
373  protected BackendDAO getBackendDAO() {
374    return _backendDAO;
375  }
376
377  /**
378   * @return the mediaObjectDAO
379   */
380  protected MediaObjectDAO getMediaObjectDAO() {
381    return _mediaObjectDAO;
382  }
383}