/*
* 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.rest.resource.gateway;
import com.google.common.collect.ImmutableSet;
import com.streamreduce.ConnectionNotFoundException;
import com.streamreduce.OutboundStorageException;
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.SobaObject;
import com.streamreduce.core.service.EventService;
import com.streamreduce.core.service.InventoryService;
import com.streamreduce.core.service.MessageService;
import com.streamreduce.core.service.OutboundStorageService;
import com.streamreduce.core.service.exception.InvalidCredentialsException;
import com.streamreduce.core.service.exception.InventoryItemNotFoundException;
import com.streamreduce.rest.resource.AbstractResource;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import net.sf.json.JSONArray;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Path("gateway") //Exposed on /gateway
public class GatewayResource extends AbstractResource {
@Autowired
protected MessageService messageService;
@Autowired
protected OutboundStorageService outboundService;
@Autowired
protected InventoryService inventoryService;
@Autowired
protected EventService eventService;
final Set<String> validMetricTypes = ImmutableSet.of(
"ABSOLUTE",
"DELTA"
);
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createGatewayMessage(JSONObject json) {
//GateProviderType type = GateProviderType.valueOf(getJSON(json, "providerId"));
// TODO: switch on type?
return createCustomConnectionMessage(json);
}
/**
* Processes an IMG message that can be a message and/or metrics for a connection or inventory item. Your message
* payload must contain either message and/or metrics attributes. If your message has the inventoryItemId
* attribute, the message and/or metrics will be associated with the inventory item. If the inventory item for the
* provided inventoryItemId does not exist, one will be created for you.
*
* @param json the JSON representing the payload
* @resource.representation.201 Returned when the create is successful
* @resource.representation.403 Returned when the account the connection is in does not allow IMG messages
* @resource.representation.405 Returned whenever the payload is invalid
* @deprecated @see #createCustomConnectionMessage(JSONObject)
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("generic")
public Response createGenericConnectionMessage(JSONObject json) {
return createCustomConnectionMessage(json);
}
/**
* Processes an IMG message that can be a message and/or metrics for a connection or inventory item. Your message
* payload must contain either message and/or metrics attributes. If your message has the inventoryItemId
* attribute, the message and/or metrics will be associated with the inventory item. If the inventory item for the
* provided inventoryItemId does not exist, one will be created for you.
*
* @param json the JSON representing the payload
* @resource.representation.201 Returned when the create is successful
* @resource.representation.403 Returned when the account the connection is in does not allow IMG messages
* @resource.representation.405 Returned whenever the payload is invalid
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("custom")
public Response createCustomConnectionMessage(JSONObject json) {
Connection connection = securityService.getCurrentGatewayConnection();
// Make sure this account has IMG support enabled
if (connection.getAccount().getConfigValue(Account.ConfigKey.DISABLE_INBOUND_API)) {
return error("This account is not provisioned for inbound payloads, please contact support@nodeable.com.",
Response.status(Response.Status.FORBIDDEN));
}
JSONArray entries = new JSONArray();
if (json.containsKey("data")) {
Object raw = json.get("data");
if (!(raw instanceof JSONArray)) {
return error("'data' must be an object array.", Response.status(Response.Status.BAD_REQUEST));
}
JSONArray rawEntries = (JSONArray) raw;
for (Object rawEntry : rawEntries) {
if (!(rawEntry instanceof JSONObject)) {
return error("Every object in the 'data' array should be an object.",
Response.status(Response.Status.BAD_REQUEST));
}
entries.add(rawEntry);
}
} else {
// The request is for a single "entry"
entries.add(json);
}
for (Object rawEntry : entries) {
if (!(rawEntry instanceof JSONObject)) {
return error("'data' entries must be JSON objects.", Response.status(Response.Status.BAD_REQUEST));
}
JSONObject entry = (JSONObject) rawEntry;
Date dateGenerated = new Date();
// Make sure that at least a message is being created or metrics are being gathered or both
if (getJSON(entry, "message") == null && getJSON(entry, "metrics") == null) {
return error("You must supply at least a 'message' or 'metrics' attribute in the payload.",
Response.status(Response.Status.BAD_REQUEST));
}
// Make sure hashtags is an array if specified (Type validation occurs later)
if (getJSON(entry, "hashtags") != null && !(entry.get("hashtags") instanceof JSONArray)) {
return error("'hashtags' must be a string array.", Response.status(Response.Status.BAD_REQUEST));
}
// Make sure metrics is an array if specified (Type validation occurs later)
if (getJSON(entry, "metrics") != null && !(entry.get("metrics") instanceof JSONArray)) {
return error("'metrics' must be an object array.", Response.status(Response.Status.BAD_REQUEST));
}
// Use the "generatedDate" attribute if applicable
if (entry.containsKey("dateGenerated")) {
try {
dateGenerated = new Date(Long.valueOf(entry.get("dateGenerated").toString()));
} catch (NumberFormatException nfe) {
return error("'dateGenerated' must be an number.", Response.status(Response.Status.BAD_REQUEST));
}
}
// See if the user specified an inventory item to associate the message and/or metrics to
String inventoryItemId = getJSON(entry, "inventoryItemId");
String message = getJSON(entry, "message");
InventoryItem inventoryItem = null;
if (inventoryItemId != null) {
try {
inventoryItem = inventoryService.getInventoryItemForExternalId(connection, inventoryItemId);
} catch (InventoryItemNotFoundException e) {
// This is handled below
}
if (inventoryItem == null) {
try {
// Create the inventory item referenced by the inventoryItemId if one does not exist
inventoryItem = inventoryService.createInventoryItem(connection, entry);
} catch (ConnectionNotFoundException e) {
// Should never happen
return error("No connection could be found based on the API key used.",
Response.status(Response.Status.BAD_REQUEST));
} catch (InvalidCredentialsException e) {
// Should never happen
return error("The credentials for the connection are invalid.",
Response.status(Response.Status.BAD_REQUEST));
} catch (IOException e) {
// Should never happen
return error("Unexpected IO exception.", Response.status(Response.Status.BAD_REQUEST));
}
}
}
// Add tags to the Connection/InventoryItem for message creation only, do not persist (Validation occurs)
JSONArray hashtags = entry.has("hashtags") ? entry.getJSONArray("hashtags") : null;
SobaObject target = inventoryItemId == null ? connection : inventoryItem;
// TODO: add Custom TAG hack, remove this and do it right
target.addHashtag("custom");
if (hashtags != null) {
// Pull in hashtags from the actual IMG message
for (Object rawHashtag : hashtags) {
if (rawHashtag instanceof String) {
target.addHashtag((String) rawHashtag);
} else {
return error("All hashtags specified in the 'hashtags' attribute must be strings.",
Response.status(Response.Status.BAD_REQUEST));
}
}
}
// Validate the metrics
JSONArray metrics = entry.has("metrics") ? entry.getJSONArray("metrics") : null;
if (metrics != null) {
for (Object rawMetric : metrics) {
if (rawMetric instanceof JSONObject) {
JSONObject metric = (JSONObject) rawMetric;
String name = getJSON(metric, "name");
String type = getJSON(metric, "type");
Object rawValue = metric.get("value");
String errorMessage = null;
if (!StringUtils.hasText(name)) {
errorMessage = "'name' is required for each metric in the 'metrics' attribute.";
} else if (!StringUtils.hasText("type") || !validMetricTypes.contains(type)) {
errorMessage = "'type' is required for each metric in the 'metrics' attribute and must " +
"be either 'ABSOLUTE' or 'DELTA'.";
} else if (rawValue instanceof JSONNull || rawValue == null) {
errorMessage = "'value' is required for each metric in the 'metrics' attribute and " +
"must be a valid numerical value.";
} else {
try {
Double.valueOf(rawValue.toString());
} catch (NumberFormatException e) {
errorMessage = "'value' for each metric in the 'metrics' attribute must be a " +
"numerical value.";
}
}
if (errorMessage != null) {
return error(errorMessage, Response.status(Response.Status.BAD_REQUEST));
}
} else {
return error("All metrics specified in the 'metrics' attribute must be objects.",
Response.status(Response.Status.BAD_REQUEST));
}
}
}
// optional config for user
try {
outboundService.sendRawMessage(entry, connection);
} catch (OutboundStorageException e) {
logger.error("Unable to send RawMessage outbound to Connection.", e);
}
// Create the event stream entry
Map<String, Object> eventContext = new HashMap<>();
// Persist the full payload
eventContext.put("message", message);
eventContext.put("dateGenerated", dateGenerated);
eventContext.put("payload", entry);
Event event = eventService.createEvent(EventId.ACTIVITY, target, eventContext);
if (message != null) {
if (inventoryItem != null) {
messageService.sendGatewayMessage(event, inventoryItem, dateGenerated.getTime());
} else {
messageService.sendGatewayMessage(event, connection, dateGenerated.getTime());
}
}
}
return Response
.status(Response.Status.CREATED)
.build();
}
}