/*
* Copyright 2012 Nodeable Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.streamreduce.core.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;
import com.streamreduce.Constants;
import com.streamreduce.ProviderIdConstants;
import com.streamreduce.connections.ConnectionProvider;
import com.streamreduce.connections.ConnectionProviderFactory;
import com.streamreduce.core.dao.DAODatasourceType;
import com.streamreduce.core.dao.EventDAO;
import com.streamreduce.core.dao.GenericCollectionDAO;
import com.streamreduce.core.event.EventId;
import com.streamreduce.core.model.Account;
import com.streamreduce.core.model.Connection;
import com.streamreduce.core.model.Event;
import com.streamreduce.core.model.InventoryItem;
import com.streamreduce.core.model.ObjectWithId;
import com.streamreduce.core.model.SobaObject;
import com.streamreduce.core.model.User;
import com.streamreduce.core.model.messages.SobaMessage;
import com.streamreduce.core.service.exception.AccountNotFoundException;
import com.streamreduce.core.service.exception.UserNotFoundException;
import net.sf.json.JSONObject;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.authc.AuthenticationException;
import org.bson.types.ObjectId;
import org.jclouds.domain.LocationScope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Implementation of {@link EventService}.
*/
@Service("eventService")
public class EventServiceImpl extends AbstractService implements EventService {
@Autowired
private EventDAO eventDAO;
@Autowired
private ConnectionProviderFactory connectionProviderFactory;
@Autowired
private GenericCollectionDAO genericCollectionDAO;
@Autowired
private SecurityService securityService;
@Autowired
private UserService userService;
/**
* {@inheritDoc}
*/
@Override
public <T extends ObjectWithId> Event createEvent(EventId eventId, T target, Map<String, Object> extraMetadata) {
Account account = null;
User user = null;
try {
user = securityService.getCurrentUser();
account = user.getAccount();
} catch (AuthenticationException ae) {
// We will not persist any READ_* events when a user is not logged in
if (eventId == EventId.READ) {
logger.debug("Anonymous read events are not persisted (" + target + "): " + eventId);
return null;
}
} catch (UnavailableSecurityManagerException e) {
logger.warn("Unable to derive user from SecurityService. A User will be derived from the target (" +
target + ") if possible. If not, no event will be persisted", e);
} catch (Exception nfi) {
logger.warn("Unknown exception type in EventService", nfi);
}
if (extraMetadata == null) {
extraMetadata = new HashMap<>();
}
// TODO: Figure out a way to make these automatically-generated metadata keys constants somewhere
// Extend the context with T information
if (target != null) {
// Fill out ObjectWithId information
extraMetadata.put("targetId", target.getId());
extraMetadata.put("targetVersion", target.getVersion());
// Fill out type (camel casing of the object type)
extraMetadata.put("targetType", target.getClass().getSimpleName());
// Fill out the SobaObject information
if (target instanceof SobaObject) {
SobaObject tSobaObject = (SobaObject) target;
// Attempt to gather the user/account from the target of the event when there is no user logged in.
// We can only do this for subclasses of SobaObject because the other two objects that create events
// (Account/User) can only provide one piece of the puzzle.
if (user == null) {
user = tSobaObject.getUser();
account = tSobaObject.getAccount();
}
extraMetadata.put("targetVisibility", tSobaObject.getVisibility());
extraMetadata.put("targetAlias", tSobaObject.getAlias());
extraMetadata.put("targetHashtags", tSobaObject.getHashtags());
}
// Fill in specific object information
if (target instanceof Account) {
Account tAccount = (Account) target;
extraMetadata.put("targetFuid", tAccount.getFuid());
extraMetadata.put("targetName", tAccount.getName());
} else if (target instanceof InventoryItem) {
InventoryItem inventoryItem = (InventoryItem)target;
Connection connection = inventoryItem.getConnection();
ConnectionProvider tConnectionProvider = connectionProviderFactory.connectionProviderFromId(
connection.getProviderId());
extraMetadata.put("targetExternalId", inventoryItem.getExternalId());
extraMetadata.put("targetExternalType", inventoryItem.getType());
extraMetadata.put("targetConnectionId", connection.getId());
extraMetadata.put("targetConnectionAlias", connection.getAlias());
extraMetadata.put("targetConnectionHashtags", connection.getHashtags());
extraMetadata.put("targetConnectionVersion", connection.getVersion());
extraMetadata.put("targetProviderId", tConnectionProvider.getId());
extraMetadata.put("targetProviderDisplayName", tConnectionProvider.getDisplayName());
extraMetadata.put("targetProviderType", tConnectionProvider.getType());
// Fill in the extra metadata stored in the nodeMetadata
extraMetadata.putAll(getMetadataFromInventoryItem(inventoryItem));
} else if (target instanceof Connection) {
Connection tConnection = (Connection) target;
ConnectionProvider tConnectionProvider = connectionProviderFactory.connectionProviderFromId(
tConnection.getProviderId());
extraMetadata.put("targetProviderId", tConnectionProvider.getId());
extraMetadata.put("targetProviderDisplayName", tConnectionProvider.getDisplayName());
extraMetadata.put("targetProviderType", tConnectionProvider.getType());
} else if (target instanceof User) {
User tUser = (User) target;
extraMetadata.put("targetFuid", tUser.getFuid());
extraMetadata.put("targetFullname", tUser.getFullname());
extraMetadata.put("targetUsername", tUser.getUsername());
} else if (target instanceof SobaMessage) {
// This is actually already handled in MessageServiceImpl. This was just put here to help keep track
// of the different types of objects we're supporting in case we want to do more later.
}
// If there is no user/account set and the event is not an Account/User/SobaMessage event, quick return.
// Otherwise, set the user and/or account based on circumstances unique to each EventId.
if (user == null) {
if (!(target instanceof Account) && !(target instanceof User) && !(target instanceof SobaMessage)) {
logger.debug("Anonymous SobaObject events are not persisted (" + target + "): " + eventId);
return null;
} else {
switch (eventId) {
case CREATE:
if (target instanceof Account) {
account = (Account) target;
user = null;
} else if (target instanceof User) {
account = user.getAccount();
user = null; // Nullify because this is a system event
} else if (target instanceof SobaMessage) {
// If this is a logged in user, no need to try and figure out the user/account
if (user != null) {
break;
}
ObjectId originalTargetId = extraMetadata.get("messageEventTargetId") != null ?
(ObjectId)extraMetadata.get("messageEventTargetId") :
null;
Event previousEvent = getLastEventForTarget(originalTargetId);
// Try to get the user
ObjectId originalEventUserId = extraMetadata.get("messageEventUserId") != null ?
(ObjectId)extraMetadata.get("messageEventUserId") :
null;
if (originalEventUserId != null) {
try {
user = userService.getUserById(originalEventUserId);
} catch (UserNotFoundException unfe) {
if (previousEvent != null) {
try {
user = userService.getUserById(previousEvent.getUserId());
} catch (UserNotFoundException unfe2) {
// There is nothing we can do at this point. Let's log so we can keep
// track of these unrecoverable events
logger.error("Unable to identify the sender of the SobaMessage " +
"having an id of " + target.getId());
}
}
}
}
// Try to get the account
ObjectId originalEventAccountId = extraMetadata.get("messageEventAccountId") != null ?
(ObjectId)extraMetadata.get("messageEventAccountId") :
null;
if (originalEventAccountId != null) {
try {
account = userService.getAccount(originalEventAccountId);
} catch (AccountNotFoundException anfe) {
if (previousEvent != null) {
try {
account = userService.getAccount(previousEvent.getAccountId());
} catch (AccountNotFoundException anfe2) {
// There is nothing we can do at this point. Let's log so we can
// keep track of these unrecoverable events
logger.error("Unable to identify the sender of the SobaMessage " +
"having an id of " + target.getId());
}
}
}
}
}
break;
case CREATE_USER_REQUEST:
account = null; // Nullify so this doesn't get associated with the admin user's account
break;
case USER_PASSWORD_RESET_REQUEST:
user = (User) target;
account = user.getAccount();
break;
case READ:
case UPDATE:
case DELETE:
case DELETE_USER_INVITE_REQUEST:
case CREATE_USER_INVITE_REQUEST:
case USER_MESSAGE:
if (eventId == EventId.DELETE && target instanceof Account) {
account = (Account) target;
} else {
// Both account and user should be set so if they aren't, quick return as these must be
// system events
logger.warn("Unexpected anonymous Account/User/SobaMessage event not persisted (" +
target + "): " + eventId);
return null;
}
}
}
}
}
// Extend the context with User information
if (user != null) {
extraMetadata.put("sourceAlias", user.getAlias());
extraMetadata.put("sourceFuid", user.getFuid());
extraMetadata.put("sourceFullname", user.getFullname());
extraMetadata.put("sourceUsername", user.getUsername());
extraMetadata.put("sourceVersion", user.getVersion());
}
// Extend the context with Account information
if (account != null) {
extraMetadata.put("accountFuid", account.getFuid());
extraMetadata.put("accountName", account.getName());
extraMetadata.put("accountVersion", account.getVersion());
}
// Convert JSONObject entries to BasicDBObject to avoid MongoDB serialization issues
for (Map.Entry<String, Object> metadataEntry : extraMetadata.entrySet()) {
String key = metadataEntry.getKey();
Object rawValue = metadataEntry.getValue();
if (rawValue instanceof JSONObject) {
extraMetadata.put(key, JSON.parse(rawValue.toString()));
}
}
return logAndSaveEvent(new Event.Builder()
.eventId(eventId)
.accountId(account != null ? account.getId() : null)
.actorId(user != null ? user.getId() : null)
.targetId(target != null ? target.getId() : null)
.context(extraMetadata)
.build());
}
/**
* {@inheritDoc}
*/
@Override
public List<Event> getEventsForAccount(Account account) {
return eventDAO.forAccount(account);
}
/**
* {@inheritDoc}
*/
@Override
public List<Event> getAllEvents() {
return eventDAO.allEvents();
}
/**
* Helper method to get the last {@link Event} based on an object id.
*
* @param targetId the target id
* @return the event or null if there is none
*/
private Event getLastEventForTarget(ObjectId targetId) {
return eventDAO.previousTargetEvent(targetId);
}
/**
* Helper method that returns all metadata for a {@link InventoryItem}.
*
* @param inventoryItem the cloud inventory item to retrieve metadata for/about
* @return the metadata
*/
private Map<String, Object> getMetadataFromInventoryItem(InventoryItem inventoryItem) {
// NOTE: We're not using CloudService methods here for performance reasons
Map<String, Object> civMetadata = new HashMap<>();
// Right now, we are only creating extended metadata for AWS EC2 instance items
if (inventoryItem.getConnection().getProviderId().equals(ProviderIdConstants.AWS_PROVIDER_ID) &&
inventoryItem.getType().equals(Constants.COMPUTE_INSTANCE_TYPE)) {
DBObject cMetadata = genericCollectionDAO.getById(DAODatasourceType.BUSINESS,
Constants.INVENTORY_ITEM_METADATA_COLLECTION_NAME,
inventoryItem.getMetadataId());
if (cMetadata == null) {
// Fill in the metadata based on the last event for this target
Event previousEvent = getLastEventForTarget(inventoryItem.getId());
if (previousEvent != null) {
Map<String, Object> peMetadata = previousEvent.getMetadata();
if (peMetadata != null) {
civMetadata.put("targetIP", peMetadata.get("targetIP"));
civMetadata.put("targetOS", peMetadata.get("targetOS"));
civMetadata.put("targetISO3166Code", peMetadata.get("targetISO3166Code"));
civMetadata.put("targetRegion", peMetadata.get("targetRegion"));
civMetadata.put("targetZone", peMetadata.get("targetZone"));
}
}
} else {
// Fill in the metadata from the available node metadata
// Get the IP address
if (cMetadata.containsField("publicAddresses")) {
BasicDBList publicAddresses = (BasicDBList) cMetadata.get("publicAddresses");
// TODO: How do we want to handle multiple IP addresses?
if (publicAddresses.size() > 0) {
civMetadata.put("targetIP", publicAddresses.get(0));
}
}
// Get location information (ISO 3166 code, region and availability zone)
if (cMetadata.containsField("location") && cMetadata.get("location") != null) {
BasicDBObject location = (BasicDBObject) cMetadata.get("location");
boolean regionProcessed = false;
boolean zoneProcessed = false;
while (location != null) {
if (regionProcessed && zoneProcessed) {
break;
}
String locationScope = location.containsField("scope") ? location.getString("scope") : null;
if (locationScope != null) {
LocationScope scope = LocationScope.valueOf(locationScope);
switch (scope) {
case REGION:
civMetadata.put("targetRegion", location.get("id"));
regionProcessed = true;
break;
case ZONE:
BasicDBList iso3166Codes = (BasicDBList) location.get("iso3166Codes");
civMetadata.put("targetISO3166Code", iso3166Codes.get(0));
civMetadata.put("targetZone", location.get("id"));
zoneProcessed = true;
break;
}
}
location = location.containsField("parent") && location.get("parent") != null ?
(BasicDBObject) location.get("parent") :
null;
}
}
// Get OS name
if (cMetadata.containsField("operatingSystem")) {
BasicDBObject operatingSystem = (BasicDBObject) cMetadata.get("operatingSystem");
if (operatingSystem != null) {
if (operatingSystem.containsField("family")) {
civMetadata.put("targetOS", operatingSystem.get("family"));
}
}
}
}
}
return civMetadata;
}
/**
* Logs the event (DEBUG) and then persists the {@link Event}.
*
* @param event the event to log and persist
* @return the event after being persisted
*/
private Event logAndSaveEvent(Event event) {
logger.debug(event.toString());
eventDAO.save(event);
return event;
}
}