/* * Copyright (C) 2015 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * * Akvo FLOW is free software: you can redistribute it and modify it under the terms of * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, * either version 3 of the License or any later version. * * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License included below for more details. * * The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>. */ package org.akvo.flow.events; import static com.gallatinsystems.common.util.MemCacheUtils.initCache; import static org.akvo.flow.events.EventUtils.getEventAndActionType; import static org.akvo.flow.events.EventUtils.newContext; import static org.akvo.flow.events.EventUtils.newEntity; import static org.akvo.flow.events.EventUtils.newEvent; import static org.akvo.flow.events.EventUtils.newSource; import static org.akvo.flow.events.EventUtils.populateEntityProperties; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.jsr107cache.Cache; import org.akvo.flow.events.EventUtils.Action; import org.akvo.flow.events.EventUtils.EventTypes; import org.akvo.flow.events.EventUtils.Key; import org.akvo.flow.events.EventUtils.Prop; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import com.gallatinsystems.common.Constants; import com.gallatinsystems.common.util.PropertyUtil; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PostDelete; import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PutContext; import com.google.appengine.api.datastore.Text; import com.google.appengine.api.utils.SystemProperty; public class EventLogger { private static Logger logger = Logger.getLogger(EventLogger.class.getName()); private static final long MIN_TIME_DIFF = 60000; // 60 seconds private void sendNotification() { try { String urlPath = PropertyUtil.getProperty(Prop.EVENT_NOTIFICATION); if (urlPath == null || urlPath.trim().length() == 0) { logger.log(Level.SEVERE, "Event notification URL not present in appengine-web.xml"); return; } URL url = new URL(urlPath.trim()); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setDoOutput(true); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); Map<String, String> messageMap = new HashMap<String, String>(); String appId = SystemProperty.applicationId.get(); messageMap.put(Key.APP_ID, appId); messageMap.put(Key.URL, appId + ".appspot.com"); ObjectMapper m = new ObjectMapper(); OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); m.writeValue(writer, messageMap); writer.close(); if (connection.getResponseCode() != HttpURLConnection.HTTP_NO_CONTENT) { logger.log(Level.SEVERE, "Unified log notification failed with status code: " + connection.getResponseCode()); } } catch (MalformedURLException e) { logger.log(Level.SEVERE, "Unified log notification failed with malformed URL exception", e); } catch (IOException e) { logger.log(Level.SEVERE, "Unified log notification failed with IO exception", e); } catch (Exception e) { logger.log(Level.SEVERE, "Unified log notification failed with error", e); } } /* * Notify the log that a new event is ready to be downloaded. */ private void notifyLog() { Cache cache = initCache(60 * 60); // 1 hour if (cache == null) { // cache is not accessible, so we will notify anyway logger.log(Level.WARNING, "cache not accessible, but still sending notification to unified log"); sendNotification(); return; } if (cache.containsKey(Action.UNIFIED_LOG_NOTIFIED)) { // check if the time the last notification was send is less than one minute ago Date cacheDate = (Date) cache.get(Action.UNIFIED_LOG_NOTIFIED); Date nowDate = new Date(); Long deltaMils = nowDate.getTime() - cacheDate.getTime(); if (deltaMils < MIN_TIME_DIFF) { // it is too soon, so don't send the notification return; } } // if we are here, either the key is not in the cache, or it is too old // in both cases, we send the notification and add a fresh value to the cache sendNotification(); cache.put(Action.UNIFIED_LOG_NOTIFIED, new Date()); } private void storeEvent(Map<String, Object> event, Date timestamp) { try { DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); ObjectMapper m = new ObjectMapper(); StringWriter w = new StringWriter(); m.writeValue(w, event); Entity entity = new Entity("EventQueue"); entity.setProperty("createdDateTime", timestamp); entity.setProperty("lastUpdateDateTime", timestamp); String payload = w.toString(); if (payload.length() > Constants.MAX_LENGTH) { entity.setProperty("payloadText", new Text(payload)); } else { entity.setProperty("payload", payload); } datastore.put(entity); notifyLog(); } catch (Exception e) { logger.log(Level.SEVERE, "could not store " + event.get("eventType") + " event. Error: " + e.toString(), e); } } @PostPut(kinds = { "SurveyGroup", "Survey", "QuestionGroup", "Question", "SurveyInstance", "QuestionAnswerStore", "SurveyedLocale", "DeviceFiles" }) void logPut(PutContext context) { try { if (!"true".equals(PropertyUtil.getProperty(Prop.ENABLE_CHANGE_EVENTS))) { return; } Entity current = context.getCurrentElement(); // determine type of event and type of action EventTypes types = getEventAndActionType(current.getKey().getKind()); // determine if this entity was created or updated Date lastUpdateDatetime = (Date) current.getProperty(Prop.LAST_UPDATE_DATE_TIME); Date createdDateTime = (Date) current.getProperty(Prop.CREATED_DATE_TIME); String actionType = createdDateTime.equals(lastUpdateDatetime) ? Action.CREATED : Action.UPDATED; // create event source // get the authentication information. This seems to contain the userId, but // according to the documentation, should hold the 'password' final Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); Map<String, Object> eventSource = newSource(authentication.getPrincipal()); Date timestamp = (Date) context.getCurrentElement().getProperty( Prop.LAST_UPDATE_DATE_TIME); // create event context map Map<String, Object> eventContext = newContext(timestamp, eventSource); // create event entity Map<String, Object> eventEntity = newEntity(types.type, context .getCurrentElement().getKey().getId()); populateEntityProperties(types.type, context.getCurrentElement(), eventEntity); // create event Map<String, Object> event = newEvent(SystemProperty.applicationId.get(), types.action + actionType, eventEntity, eventContext); // store it storeEvent(event, timestamp); } catch (Exception e) { logger.log(Level.SEVERE, "Could not handle datastore put event: " + e.getMessage(), e); } } @PostDelete(kinds = { "SurveyGroup", "Survey", "QuestionGroup", "Question", "SurveyInstance", "QuestionAnswerStore", "SurveyedLocale", "DeviceFiles" }) void logDelete(DeleteContext context) { try { if (!"true".equals(PropertyUtil.getProperty(Prop.ENABLE_CHANGE_EVENTS))) { return; } // determine type of event and type of action EventTypes types = getEventAndActionType(context.getCurrentElement().getKind()); // create event source // get the authentication information. This seems to contain the userId, but // according to the documentation, should hold the 'password' final Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); Object principal = authentication.getPrincipal(); Map<String, Object> eventSource = newSource(principal); // create event context map // we create our own timestamp here, as we don't have one in the context Date timestamp = new Date(); Map<String, Object> eventContext = newContext(timestamp, eventSource); // create event entity Map<String, Object> eventEntity = newEntity(types.type, context .getCurrentElement().getId()); // create event Map<String, Object> event = newEvent(SystemProperty.applicationId.get(), types.action + Action.DELETED, eventEntity, eventContext); // store it storeEvent(event, timestamp); } catch (Exception e) { logger.log(Level.SEVERE, "Could not handle datastore delete event: " + e.getMessage(), e); } } }