001/**
002 * Copyright 2016 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.apilta.sensors;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.EnumSet;
021import java.util.List;
022import java.util.Set;
023
024import org.apache.log4j.Logger;
025import org.springframework.context.ApplicationListener;
026
027import service.tut.pori.apilta.files.FilesCore;
028import service.tut.pori.apilta.files.datatypes.FileDetails;
029import service.tut.pori.apilta.sensors.datatypes.DataPoint;
030import service.tut.pori.apilta.sensors.datatypes.Measurement;
031import service.tut.pori.apilta.sensors.datatypes.MeasurementList;
032import service.tut.pori.apilta.sensors.datatypes.SensorTask;
033import service.tut.pori.backends.BackendDAO;
034import service.tut.pori.backends.BackendsCore;
035import service.tut.pori.backends.datatypes.BackendEvent;
036import service.tut.pori.backends.datatypes.BackendUserIdentity.UserPermission;
037import service.tut.pori.tasks.TaskDAO;
038import service.tut.pori.tasks.TasksCore;
039import service.tut.pori.tasks.datatypes.TaskBackend;
040import service.tut.pori.tasks.datatypes.TaskBackend.Status;
041import service.tut.pori.tasks.datatypes.TaskPermissions;
042import service.tut.pori.users.UserDAO;
043import core.tut.pori.context.ServiceInitializer;
044import core.tut.pori.http.parameters.DataGroups;
045import core.tut.pori.http.parameters.DateIntervalParameter.Interval;
046import core.tut.pori.http.parameters.Limits;
047import core.tut.pori.users.UserGroup.Permission;
048import core.tut.pori.users.UserIdentity;
049
050/**
051 * the core methods for sensor service
052 * 
053 */
054public final class SensorsCore {
055  private static final EnumSet<UserPermission> ENUMSET_AUTH_BACKENDS = EnumSet.of(UserPermission.AUTH_BACKENDS);
056  private static final Logger LOGGER = Logger.getLogger(SensorsCore.class);
057  
058  /**
059   * 
060   */
061  private SensorsCore() {
062    // nothing needed
063  }
064
065  /**
066   * 
067   * @param authenticatedUser
068   * @param task
069   * @throws IllegalArgumentException on invalid task
070   */
071  public static void taskFinished(UserIdentity authenticatedUser, SensorTask task) throws IllegalArgumentException {
072    List<String> taskIds = task.getTaskIds();
073    if(taskIds == null || taskIds.isEmpty()){
074      throw new IllegalArgumentException("Invalid task: task identifier missing.");
075    }
076    
077    if(!SensorTask.isValid(task) || task.getConditions() != null){ // it is enough to check that either conditions or output is not present as the validity is checked through isValid()
078      throw new IllegalArgumentException("Invalid task.");
079    }
080    
081    List<TaskBackend> backends = task.getBackends();
082    
083    ArrayList<String> validTaskIds = new ArrayList<>(taskIds.size());
084    SensorTaskDAO taskDAO = task.getTaskDao();
085    for(String taskId : taskIds){ // check that the user has permissions to modify the tasks
086      TaskPermissions permissions = taskDAO.getTaskPermissions(true, taskId, authenticatedUser);
087      if(!permissions.isTaskExists()){
088        LOGGER.warn("Ignored non-existing task, id: "+taskId+" for user, id: "+authenticatedUser.getUserId());  // simply ignore tasks that do not exist
089      }else if(!permissions.hasPermissions(backends, ENUMSET_AUTH_BACKENDS)){ // check that the user can really authenticate as every one of the given back ends
090        throw new IllegalArgumentException("Invalid back end identifiers.");
091      }else{
092        validTaskIds.add(taskId);
093      }
094    }
095    
096    if(validTaskIds.isEmpty()){
097      throw new IllegalArgumentException("Invalid task: no valid task identifiers.");
098    }
099    
100    for(TaskBackend backend : backends) { // check that the user has permissions to provide data for the reported back ends
101      Long backendId = backend.getBackendId();
102      for(String taskId : validTaskIds){
103        if(!taskDAO.statusUpdated(backend, taskId)){ // update the status information for the back end
104          throw new IllegalArgumentException("Failed to update status for back end, id: "+backendId+", for task, id: "+taskId); // something is wrong if this fails, to do no allow to proceed
105        }
106      }
107    }
108    
109    SensorsDAO sensorsDAO = ServiceInitializer.getDAOHandler().getDAO(SensorsDAO.class);
110    List<Measurement> measurements = task.getMeasurements().getMeasurements();
111    for(Measurement measurement : measurements){ // check that the file GUIDs (if present) has been associated with the given back ends (i.e. the back end has previously uploaded the file)
112      Long backendId = measurement.getBackendId();
113      for(DataPoint dp : measurement.getDataPoints()){ 
114        if(Definitions.DATA_POINT_KEY_FILE_GUID.equals(dp.getKey()) && !sensorsDAO.backendHasGUID(backendId, dp.getValue())){
115          throw new IllegalArgumentException("File GUID: "+dp.getValue()+" is not associated with back end, id: "+backendId);
116        } // if
117      } // for
118    }
119    
120    sensorsDAO.addMeasurements(measurements, validTaskIds); // finally, add the measurements for all valid identifiers
121  }
122
123  /**
124   * Status information for back ends (if given) is ignored and set to {@link service.tut.pori.tasks.datatypes.TaskBackend.Status#NOT_STARTED}
125   * 
126   * Task identifier, if given is ignored and a new identifier is generated.
127   * 
128   * @param authenticatedUser
129   * @param task
130   * @return identifier for the created task or null on failure (permission denied)
131   * @throws IllegalArgumentException on invalid task
132   */
133  public static String createTask(UserIdentity authenticatedUser, SensorTask task) throws IllegalArgumentException {
134    List<TaskBackend> backends = task.getBackends();
135    if(backends == null){
136      throw new IllegalArgumentException("Invalid task: no back ends.");
137    }
138    BackendDAO backendDAO = ServiceInitializer.getDAOHandler().getDAO(BackendDAO.class);
139    for(TaskBackend backend : backends){ // reset all status information to not started before task validation
140      Long backendId = backend.getBackendId();
141      Set<UserPermission> permissions = backendDAO.getBackendPermissions(backendId, authenticatedUser);
142      if(permissions == null || !permissions.contains(UserPermission.TASKS)){
143        LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" is not allowed to create tasks for back end, id: "+backendId);
144        return null;
145      }
146      backend.setStatus(Status.NOT_STARTED);
147    }
148    
149    List<String> taskIds = task.getTaskIds();
150    if(taskIds != null && !taskIds.isEmpty()){
151      LOGGER.debug("Ignoring task ids for new task.");
152    }
153    task.setTaskIds(null);
154    
155    if(!SensorTask.isValid(task) || task.getConditions() == null){ // it is enough to check that either conditions or output exists as the validity is checked through isValid()
156      throw new IllegalArgumentException("Invalid task.");
157    }
158    
159    UserIdentity target = task.getUserId();
160    if(UserIdentity.isValid(target)){
161      if(!ServiceInitializer.getDAOHandler().getDAO(UserDAO.class).hasPermission(authenticatedUser, target, Permission.MODIFY_USERS)){
162        LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" attempted to modify user, id: "+target.getUserId()+" without permission "+Permission.MODIFY_USERS.name());
163        return null;
164      }
165    }else{
166      LOGGER.debug("No user id for the task, defaulting to the authenticated user.");
167      task.setUserId(authenticatedUser);
168    }
169    
170    Set<String> taskTypes = task.getTaskTypes();
171    if(taskTypes == null || taskTypes.isEmpty()){
172      throw new IllegalArgumentException("Invalid task: no task type.");
173    }
174    
175    return TasksCore.scheduleTask(task);
176  }
177  
178  /**
179   * 
180   * Task identifier, if given is ignored and a new identifier is generated.
181   * 
182   * @param authenticatedUser
183   * @param task
184   * @return identifier for the created task or null on failure (permission denied)
185   * @throws IllegalArgumentException on invalid task
186   */
187  public static String modifyTask(UserIdentity authenticatedUser, SensorTask task) throws IllegalArgumentException {
188    List<String> taskIds = task.getTaskIds();
189    if(taskIds == null || taskIds.size() != 1){
190      throw new IllegalArgumentException("Invalid task: the task must have exactly one task identifier.");
191    }
192    
193    String taskId = taskIds.iterator().next();
194    TaskPermissions taskPermissions = task.getTaskDao().getTaskPermissions(false, taskId, authenticatedUser);
195    if(!taskPermissions.isTaskExists()){
196      throw new IllegalArgumentException("Invalid task: the task does not exist, id: "+taskId);
197    }else if(!taskPermissions.isTaskOwner()){
198      LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" is not allowed to modify task, id: "+taskId);
199      return null;
200    }
201    
202    List<TaskBackend> backends = task.getBackends();
203    if(backends == null){
204      throw new IllegalArgumentException("Invalid task: no back ends.");
205    }
206    
207    BackendDAO backendDAO = ServiceInitializer.getDAOHandler().getDAO(BackendDAO.class);
208    for(TaskBackend backend : backends){ // reset all status information to not started before task validation
209      Long backendId = backend.getBackendId();
210      Set<UserPermission> permissions = backendDAO.getBackendPermissions(backendId, authenticatedUser);
211      if(permissions == null || !permissions.contains(UserPermission.TASKS)){
212        LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" is not allowed to use tasks for back end, id: "+backendId);
213        return null;
214      }
215    }
216    
217    if(!SensorTask.isValid(task) || task.getConditions() == null){ // it is enough to check that either conditions or output exists as the validity is checked through isValid()
218      throw new IllegalArgumentException("Invalid task.");
219    }
220    
221    UserIdentity target = task.getUserId();
222    if(!UserIdentity.isValid(target)){
223      throw new IllegalArgumentException("Invalid task: task must have a user.");
224    }
225    
226    if(!ServiceInitializer.getDAOHandler().getDAO(UserDAO.class).hasPermission(authenticatedUser, target, Permission.MODIFY_USERS)){
227      LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" attempted to modify user, id: "+target.getUserId()+" without permission "+Permission.MODIFY_USERS.name());
228      return null;
229    }
230    
231    Set<String> taskTypes = task.getTaskTypes();
232    if(taskTypes == null || taskTypes.isEmpty()){
233      throw new IllegalArgumentException("Invalid task: no task type.");
234    }
235    
236    return TasksCore.scheduleTask(task);
237  }
238  
239  /**
240   * 
241   * @param authenticatedUser
242   * @param backendId 
243   * @param file
244   * @return details for the created file or null on failure (permission denied)
245   * @throws IllegalArgumentException on bad data
246   */
247  public static FileDetails createFile(UserIdentity authenticatedUser, Long backendId, InputStream file) throws IllegalArgumentException {
248    Set<UserPermission> permissions = ServiceInitializer.getDAOHandler().getDAO(BackendDAO.class).getBackendPermissions(backendId, authenticatedUser);
249    if(permissions == null || !permissions.contains(UserPermission.AUTH_BACKENDS)){
250      LOGGER.warn("User, id: "+authenticatedUser.getUserId()+" does not have permission "+UserPermission.AUTH_BACKENDS.name());
251      return null;
252    }
253    
254    FileDetails details = FilesCore.createFile(file);
255    if(!FileDetails.isValid(details)){
256      throw new IllegalArgumentException("Failed to create file from the given data.");
257    }
258    
259    String guid = details.getGUID();
260    LOGGER.debug("Associating file, guid: "+guid+" with back end, id: "+backendId);
261    ServiceInitializer.getDAOHandler().getDAO(SensorsDAO.class).addFile(backendId, guid);
262    
263    return details;
264  }
265  
266  /**
267   * 
268   * @param authenticatedUser
269   * @param backendIdFilter
270   * @param createdFilter
271   * @param dataGroups valid data groups are: {@value DataGroups#DATA_GROUP_ALL}, {@value DataGroups#DATA_GROUP_BASIC} (only the measurement details are given without data points, this is the default), {@value Definitions#DATA_GROUP_DATA_POINTS} (data points included in the response)
272   * @param limits
273   * @param measurementIdFilter
274   * @param taskIds
275   * @return list of measurements or null if none
276   * @throws IllegalArgumentException on invalid parameters
277   */
278  public static MeasurementList getMeasurements(UserIdentity authenticatedUser, long[] backendIdFilter, Set<Interval> createdFilter, DataGroups dataGroups, Limits limits, List<String> measurementIdFilter, List<String> taskIds) throws IllegalArgumentException {
279    TaskDAO taskDAO = ServiceInitializer.getDAOHandler().getDAO(TaskDAO.class);
280    for(String taskId : taskIds){
281      TaskPermissions permissions = taskDAO.getTaskPermissions(false, taskId, authenticatedUser);
282      if(!permissions.canAccessData()){
283        throw new IllegalArgumentException("Task, id: "+taskId+" was not found or permission was denied.");
284      }
285    }
286    
287    MeasurementList measurementList =  ServiceInitializer.getDAOHandler().getDAO(SensorsDAO.class).getMeasurements(backendIdFilter, createdFilter, dataGroups, limits, measurementIdFilter, taskIds);
288    resolveFileUrls(measurementList);
289    return measurementList;
290  }
291  
292  /**
293   * resolve proper url's for all applicable data points in the measurement list
294   * 
295   * @param measurementList
296   */
297  private static void resolveFileUrls(MeasurementList measurementList) {
298    if(MeasurementList.isEmpty(measurementList)){
299      LOGGER.debug("Empty measurement list.");
300    }else{
301      LOGGER.debug("Resolving data point values: "+Definitions.DATA_POINT_KEY_FILE_GUID+" to "+Definitions.DATA_POINT_KEY_FILE_DETAILS_URL);
302      
303      for(Measurement m : measurementList.getMeasurements()){
304        List<DataPoint> dataPoints = m.getDataPoints();
305        if(dataPoints == null){
306          LOGGER.debug("No data points for measurement, id: "+m.getMeasurementId());
307        }else{
308          for(DataPoint dp : m.getDataPoints()){
309            if(Definitions.DATA_POINT_KEY_FILE_GUID.equals(dp.getKey())){
310              dp.setKey(Definitions.DATA_POINT_KEY_FILE_DETAILS_URL);
311              dp.setValue(FilesCore.generateTemporaryUrl(dp.getValue()));
312            } // if guid key
313          } // for data points
314        } // else has data points
315      } // for measurements
316    } // else
317  }
318  
319  /**
320   * Event listener for back end related events.
321   * 
322   * Automatically instantiated by Spring as a bean.
323   */
324  @SuppressWarnings("unused")
325  private static class BackendListener implements ApplicationListener<BackendEvent>{
326
327    @Override
328    public void onApplicationEvent(BackendEvent event) {
329      service.tut.pori.backends.datatypes.BackendEvent.EventType type = event.getType();
330      if(type == service.tut.pori.backends.datatypes.BackendEvent.EventType.BACKEND_REMOVED && event.getSource().equals(BackendsCore.class)){
331        Long backendId = event.getBackendId();
332        LOGGER.debug("Detected event of type "+type.name()+", removing all data generated by back end, id: "+backendId);
333        
334        SensorsDAO dao = ServiceInitializer.getDAOHandler().getDAO(SensorsDAO.class);
335        dao.deleteMeasurements(backendId);
336        
337        List<String> guids = dao.getFileGUIDs(backendId);
338        if(guids != null){
339          LOGGER.debug("Removing files associated with back end, id: "+backendId);
340          dao.deleteFiles(backendId);
341          for(String guid : guids){
342            FilesCore.removeFile(guid);
343          }
344        } // if
345        
346        LOGGER.debug("Back end data removed, back end id: "+backendId);
347      }
348    }
349  } // class BackendListener
350}