/*
* Copyright 2012 Research Studios Austria Forschungsgesellschaft mBH
*
* This file is part of easyrec.
*
* easyrec is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* easyrec 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with easyrec. If not, see <http://www.gnu.org/licenses/>.
*/
package org.easyrec.rest;
import com.jamonapi.Monitor;
import com.jamonapi.MonitorFactory;
import com.sun.jersey.api.json.JSONWithPadding;
import com.sun.jersey.spi.resource.Singleton;
import org.easyrec.model.core.web.Message;
import org.easyrec.model.core.web.SuccessMessage;
import org.easyrec.service.core.ProfileService;
import org.easyrec.service.core.exception.FieldNotFoundException;
import org.easyrec.service.core.exception.MultipleProfileFieldsFoundException;
import org.easyrec.store.dao.web.OperatorDAO;
import org.easyrec.vocabulary.MSG;
import org.easyrec.vocabulary.WS;
import org.w3c.dom.DOMException;
import org.xml.sax.SAXException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* This class is a REST webservice facade for the ProfileService
*
* @author Fabian Salcher
*/
/*
* The path of the URI must begin with the version number of the API.
* Followed by an optional "json" part. If this part is missing the result
* will be returned as XML.
*/
@Path("1.0{responseType: (/json)?}/profile")
@Produces({"application/xml", "application/json"})
@Singleton
public class ProfileWebservice {
@Context
public HttpServletRequest request;
private ProfileService profileService;
private OperatorDAO operatorDAO;
private final static String JAMON_PROFILE_LOAD = "rest.profile.load";
private final static String JAMON_PROFILE_STORE = "rest.profile.store";
private final static String JAMON_PROFILE_DELETE = "rest.profile.delete";
private final static String JAMON_PROFILE_FIELD_STORE = "rest.profile.field.store";
private final static String JAMON_PROFILE_FIELD_LOAD = "rest.profile.field.load";
private final static String JAMON_PROFILE_FIELD_DELETE = "rest.profile.field.delete";
public ProfileWebservice(ProfileService profileService, OperatorDAO operatorDAO) {
this.profileService = profileService;
this.operatorDAO = operatorDAO;
}
/**
* This method stores the given profile to the Item defined by the tenantID,
* itemID and the itemTypeID. If there is already a Profile it will be overwritten.
* If the Item does not exist it will be created.
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the item where the profile will be stored
* @param itemID the itemID if the item where the profile will be stored
* @param itemType the itemType of the item where the profile will be stored
* @param profile the XML profile which will be stored
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing information about the success of the operation
*/
@GET
@Path("/store")
public Response storeProfile(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("profile") String profile,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_STORE);
List<Message> errorMessages = new ArrayList<Message>();
List<Message> responseObject = new ArrayList<Message>();
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages) &&
checkParameterProfile(profile, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
if (profileService.storeProfile(coreTenantID, itemID, itemType, profile))
responseObject.add(MSG.PROFILE_SAVED);
else
errorMessages.add(MSG.PROFILE_NOT_SAVED);
}
}
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_NOT_SAVED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_NOT_SAVED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_STORE, responseType, callback);
mon.stop();
return response;
}
/**
* This method deletes the profile of the item defined by the tenantID,
* itemID and the itemTypeID
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the item whose profile will be deleted
* @param itemID the itemID if the item whose profile will be deleted
* @param itemType the itemType of the item whose profile will be deleted
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing information about the success of the operation
*/
@GET
@Path("/delete")
public Response deleteProfile(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_DELETE);
List<Message> errorMessages = new ArrayList<Message>();
List<Message> responseObject = new ArrayList<Message>();
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
if (profileService.deleteProfile(coreTenantID, itemID, itemType))
responseObject.add(MSG.PROFILE_DELETED);
else
errorMessages.add(MSG.PROFILE_NOT_DELETED);
}
}
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_NOT_DELETED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_NOT_DELETED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_DELETE, responseType, callback);
mon.stop();
return response;
}
/**
* This method returns the profile of the item defined by the tenantID,
* itemID and the itemTypeID
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the item whose profile will be returned
* @param itemID the itemID if the item whose profile will be returned
* @param itemType the itemType of the item whose profile will be returned
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing the wanted profile
*/
@GET
@Path("/load")
public Response loadProfile(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_LOAD);
List<Message> errorMessages = new ArrayList<Message>();
Object responseObject = null;
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
String profile = profileService.getProfile(coreTenantID, itemID, itemType);
if (profile != null)
responseObject = new ResponseProfile("profile/load", tenantID, itemID, itemType, profile);
else
errorMessages.add(MSG.PROFILE_NOT_LOADED);
}
}
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_NOT_LOADED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_NOT_LOADED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_LOAD, responseType, callback);
mon.stop();
return response;
}
/**
* This method stores a value into a specific field of the profile which belongs to the item
* defined by the tenantID, itemID and the itemTypeID. The field can be addressed
* by a XPath expression. If the field does not exist it will be created.
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the addressed item
* @param itemID the itemID if the addressed item
* @param itemType the itemType of the addressed item
* @param field an XPath expression pointing to the field
* where the value will be saved
* @param value the value which will be saved in the field
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing information about the success of the operation
*/
@GET
@Path("/field/store")
public Response storeField(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("field") String field,
@QueryParam("value") String value,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_FIELD_STORE);
List<Message> errorMessages = new ArrayList<Message>();
List<Message> responseObject = new ArrayList<Message>();
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages) &&
checkParameterField(field, errorMessages) &&
checkParameterValue(value, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
if (profileService.storeProfileField(
coreTenantID, itemID, itemType,
field, value))
responseObject.add(MSG.PROFILE_FIELD_SAVED);
else
errorMessages.add(MSG.PROFILE_FIELD_NOT_SAVED);
}
}
} catch (SAXException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" SAXException: " + e.getMessage()));
} catch (XPathExpressionException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" XPathExpressionException: " + e.getMessage()));
} catch (TransformerException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" TransformerException: " + e.getMessage()));
} catch (DOMException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" DOMException: " + e.getMessage()));
} catch (MultipleProfileFieldsFoundException e) {
errorMessages.add(MSG.PROFILE_MULTIPLE_FIELDS_WITH_SAME_NAME);
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_FIELD_NOT_SAVED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_FIELD_NOT_SAVED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_FIELD_STORE, responseType, callback);
mon.stop();
return response;
}
/**
* This method deletes a specific field of the profile which belongs to the item
* defined by the tenantID, itemID and the itemTypeID. The field can be addressed
* by a XPath expression
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the addressed item
* @param itemID the itemID if the addressed item
* @param itemType the itemType of the addressed item
* @param field an XPath expression pointing to the field
* which will be deleted
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing information about the success of the operation
*/
@GET
@Path("/field/delete")
public Response deleteField(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("field") String field,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_FIELD_DELETE);
List<Message> errorMessages = new ArrayList<Message>();
List<Message> responseObject = new ArrayList<Message>();
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages) &&
checkParameterField(field, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
if (profileService.deleteProfileField(coreTenantID, itemID, itemType, field))
responseObject.add(MSG.PROFILE_FIELD_DELETED);
else
errorMessages.add(MSG.PROFILE_FIELD_NOT_DELETED);
}
}
} catch (SAXException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" SAXException: " + e.getMessage()));
} catch (XPathExpressionException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" XPathExpressionException: " + e.getMessage()));
} catch (TransformerException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" TransformerException: " + e.getMessage()));
} catch (DOMException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" DOMException: " + e.getMessage()));
} catch (FieldNotFoundException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" FieldNotFoundException: " + e.getMessage()));
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_FIELD_NOT_DELETED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_FIELD_NOT_DELETED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_FIELD_DELETE, responseType, callback);
mon.stop();
return response;
}
/**
* This method loads the value from a specific field of the profile which belongs to the item
* defined by the tenantID, itemID and the itemTypeID. The field can be addressed
* by a XPath expression. If multiple profile fields are found an error message will be returned.
*
* @param responseType defines the media type of the result
* @param apiKey the apiKey which admits access to the API
* @param tenantID the tenantID of the addressed item
* @param itemID the itemID if the addressed item
* @param itemType the itemType of the addressed item
* @param field an XPath expression pointing to the field(s)
* whose value will be returned
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a response object containing the value of the field.
* @see ResponseProfileField
*/
@GET
@Path("/field/load")
public Response loadField(@PathParam("responseType") String responseType,
@QueryParam("apikey") String apiKey,
@QueryParam("tenantid") String tenantID,
@QueryParam("itemid") String itemID,
@DefaultValue("ITEM") @QueryParam("itemtype") String itemType,
@QueryParam("field") String field,
@QueryParam("callback") String callback) {
Monitor mon = MonitorFactory.start(JAMON_PROFILE_FIELD_LOAD);
List<Message> errorMessages = new ArrayList<Message>();
Object responseObject = null;
try {
if (checkParameters(apiKey, tenantID, itemID, itemType, errorMessages) &&
checkParameterField(field, errorMessages)) {
Integer coreTenantID = operatorDAO.getTenantId(apiKey, tenantID);
if (coreTenantID == null)
errorMessages.add(MSG.TENANT_WRONG_TENANT_APIKEY);
else {
Set<String> values = profileService.loadProfileField(coreTenantID, itemID, itemType, field);
if (values != null || values.size() == 0)
if (values.size() > 1)
errorMessages.add(MSG.PROFILE_MULTIPLE_FIELDS_WITH_SAME_NAME);
else
responseObject = new ResponseProfileField("profile/field/load",
tenantID, itemID, itemType, field, (String) values.toArray()[0]);
else
errorMessages.add(MSG.PROFILE_FIELD_NOT_LOADED);
}
}
} catch (SAXException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" SAXException: " + e.getMessage()));
} catch (XPathExpressionException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" XPathExpressionException: " + e.getMessage()));
} catch (DOMException e) {
errorMessages.add(MSG.OPERATION_FAILED.append(
" DOMException: " + e.getMessage()));
} catch (IllegalArgumentException illegalArgumentException) {
if (illegalArgumentException.getMessage().contains("unknown item type")) {
errorMessages.add(MSG.OPERATION_FAILED.append(
String.format(" itemType %s not found for tenant %s", itemType, tenantID)));
} else
errorMessages.add(MSG.PROFILE_FIELD_NOT_LOADED);
} catch (RuntimeException runtimeException) {
errorMessages.add(MSG.PROFILE_FIELD_NOT_LOADED);
}
Response response = formatResponse(responseObject, errorMessages,
WS.PROFILE_FIELD_LOAD, responseType, callback);
mon.stop();
return response;
}
/**
* This method takes an object and creates a <code>Response</code> object
* out of it which will be returned. If <code>messages</code> contains error
* messages they will be send back instead.
* The format of the <code>Response</code>
* depends on the <code>responseType</code>.
* Supported types are <code>application/xml</code> and <code>application/json</code>
*
* @param respondData an object which will be returned as a
* <code>Response</code> object
* @param messages a list of <code>Message</code> objects which contain
* error messages of the API request
* @param responseType defines the format of the <code>Response</code> object
* @param callback if set and responseType is jason the result will be returned
* via this javascript callback function (optional)
* @return a <code>Response</code> object containing the <code>responseData</code>
* in the format defined with <code>responseType</code>
*/
private Response formatResponse(Object respondData,
List<Message> messages,
String serviceName,
String responseType,
String callback) {
//handle error messages if existing
if (messages.size() > 0) {
if ((WS.RESPONSE_TYPE_PATH_JSON.equals(responseType)))
throw new EasyRecException(messages, serviceName, WS.RESPONSE_TYPE_JSON, callback);
else
throw new EasyRecException(messages, serviceName);
}
if (respondData instanceof List) {
respondData = new ResponseSuccessMessage(serviceName, (List<SuccessMessage>) respondData);
}
//convert respondData to Respond object
if (WS.RESPONSE_TYPE_PATH_JSON.equals(responseType)) {
if (callback != null) {
return Response.ok(new JSONWithPadding(respondData, callback),
WS.RESPONSE_TYPE_JSCRIPT).build();
} else {
return Response.ok(respondData, WS.RESPONSE_TYPE_JSON).build();
}
} else if (WS.RESPONSE_TYPE_PATH_XML.equals(responseType)) {
return Response.ok(respondData, WS.RESPONSE_TYPE_XML).build();
} else {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).build();
}
}
/**
* This method checks if the <code>apiKey, tenantID, itemID</code> and <code>itemType</code>
* is not null or an empty string. If this is not the case the corresponding error message is
* added to the <code>messages</code> list.
*
* @param apiKey the apiKey which will be checked
* @param tenantID the tenantID which will be checked
* @param itemID the itemID which will be checked
* @param itemType the itemType which will be checked
* @param messages a <code>List<Message></code> where the error messages are appended
* @return returns <code>true</code> if all parameters are positively checked and
* <code>false</code> otherwise
*/
private boolean checkParameters(String apiKey, String tenantID,
String itemID, String itemType, List<Message> messages) {
boolean result = true;
if (apiKey == null || apiKey.equals("")) {
messages.add(MSG.PROFILE_NO_API_KEY);
result = false;
}
if (tenantID == null || tenantID.equals("")) {
messages.add(MSG.PROFILE_NO_TENANT_ID);
result = false;
}
if (itemID == null || itemID.equals("")) {
messages.add(MSG.ITEM_NO_ID);
result = false;
}
if (itemType == null || itemType.equals("")) {
messages.add(MSG.PROFILE_NO_ITEM_TYPE);
result = false;
}
return result;
}
/**
* This method checks if the <code>profile</code> is not null
* or an empty string. If this is not the case the corresponding
* error message is added to the <code>messages</code> list.
*
* @param profile the profile which will be checked
* @param messages a <code>List<Message></code> where the error messages are appended
* @return returns <code>true</code> if all parameters are positively checked and
* <code>false</code> otherwise
*/
private boolean checkParameterProfile(String profile, List<Message> messages) {
if (profile == null) {
messages.add(MSG.PROFILE_NO_PROFILE_PROVIDED);
return false;
} else
return true;
}
/**
* This method checks if the <code>profile field</code> is not null
* or an empty string. If this is not the case the corresponding
* error message is added to the <code>messages</code> list.
*
* @param field the field which will be checked
* @param messages a <code>List<Message></code> where the error messages are appended
* @return returns <code>true</code> if all parameters are positively checked and
* <code>false</code> otherwise
*/
private boolean checkParameterField(String field, List<Message> messages) {
if (field == null || field.equals("")) {
messages.add(MSG.PROFILE_NO_FIELD_PROVIDED);
return false;
} else
return true;
}
/**
* This method checks if the <code>profile field value</code> is not null
* or an empty string. If this is not the case the corresponding
* error message is added to the <code>messages</code> list.
*
* @param value the value which will be checked
* @param messages a <code>List<Message></code> where the error messages are appended
* @return returns <code>true</code> if all parameters are positively checked and
* <code>false</code> otherwise
*/
private boolean checkParameterValue(String value, List<Message> messages) {
if (value == null) {
messages.add(MSG.PROFILE_NO_VALUE_PROVIDED);
return false;
} else
return true;
}
}