/* * Seldon -- open source prediction engine * ======================================= * * Copyright 2011-2015 Seldon Technologies Ltd and Rummble Ltd (http://www.seldon.io/) * * ******************************************************************************************** * * 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 io.seldon.api.controller; import io.seldon.api.APIException; import io.seldon.api.Constants; import io.seldon.api.Util; import io.seldon.api.locale.DimensionsMappingManager; import io.seldon.api.logging.CtrFullLogger; import io.seldon.api.logging.MDCKeys; import io.seldon.api.resource.ActionBean; import io.seldon.api.resource.ConsumerBean; import io.seldon.api.resource.ErrorBean; import io.seldon.api.resource.ItemBean; import io.seldon.api.resource.ResourceBean; import io.seldon.api.resource.UserBean; import io.seldon.api.resource.service.ItemService; import io.seldon.api.resource.service.business.ActionBusinessService; import io.seldon.api.resource.service.business.PredictionBusinessService; import io.seldon.api.resource.service.business.RecommendationBusinessService; import io.seldon.api.resource.service.business.UserBusinessService; import io.seldon.api.resource.service.business.UserProfileService; import io.seldon.api.statsd.StatsdPeer; import io.seldon.recommendation.userdimensionmapping.UserDimensionMappingModelManager; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.databind.util.JSONPObject; /** * Created by: marc on 05/07/2012 at 11:17 */ @Controller @RequestMapping("/js") public class JsClientController { private static Logger logger = Logger.getLogger(JsClientController.class.getName()); @Autowired private UserBusinessService userBusinessService; @Autowired private ActionBusinessService actionBusinessService; @Autowired private RecommendationBusinessService recommendationBusinessService; @Autowired private PredictionBusinessService predictionBusinessService; @Autowired private MessageSource messageSource; @Autowired private ItemService itemService; @Autowired private UserProfileService userProfileService; @Autowired private DimensionsMappingManager dimensionsMappingManager; @Autowired private UserDimensionMappingModelManager userDimensionMappingModelManager; private ConsumerBean retrieveConsumer(HttpSession session) { return (ConsumerBean) session.getAttribute("consumer"); } @ExceptionHandler(value = APIException.class) public @ResponseBody JSONPObject handleException(APIException ex, HttpServletRequest request) { String jsonpCallback = request.getParameter("jsonpCallback"); if (StringUtils.isBlank(jsonpCallback)) { jsonpCallback = "jsonpCallback"; } return asCallback(jsonpCallback, new ErrorBean(ex)); } @RequestMapping("/action/new") public @ResponseBody JSONPObject registerAction(HttpSession session, @RequestParam("user") String userId, @RequestParam("item") String itemId, @RequestParam("type") Integer type, @RequestParam("jsonpCallback") String callback, @RequestParam(value = "source", required = false) String referrer, @RequestParam(value = "rectag", required = false) String recTag, @RequestParam(value = "pos", required = false) Integer pos, @RequestParam(value = "rlabs", required = false) String rlabs, @RequestParam(value = "extra_data", required = false) String extraData, @RequestParam(value = "click_only", required = false) Boolean click_only, @RequestParam(value = "zehtg", required = false) String req) { final ConsumerBean consumerBean = retrieveConsumer(session); MDCKeys.addKeys(consumerBean, userId, itemId,recTag); //added zehtg parameter as additional option for rlabs if(StringUtils.isNotBlank(req)) { rlabs=req; } if (logger.isDebugEnabled()) logger.debug("Creating action for consumer: " + consumerBean.getShort_name()); ActionBean actionBean = createAction(userId, itemId, type, referrer,recTag,extraData); boolean isCTR = StringUtils.isNotBlank(rlabs); boolean clickOnly = (isCTR && (click_only != null) && (click_only == true)) ? true : false; int clickPos = -1; if (pos != null) clickPos = pos.intValue(); return asCallback(callback, actionBusinessService.addAction(consumerBean, actionBean, isCTR, rlabs,recTag,clickPos, clickOnly)); } @RequestMapping("/user/new") public @ResponseBody JSONPObject registerUser(HttpSession session, @RequestParam("user") String userId, @RequestParam(value = "username", required = false) String username, @RequestParam("jsonpCallback") String callback) { username = (username != null) ? username : userId; final ConsumerBean consumerBean = retrieveConsumer(session); UserBean user = new UserBean(userId, username); user.setType(1); logger.debug("Creating user: " + userId + " for consumer: " + consumerBean.getShort_name()); ResourceBean responseBean = userBusinessService.updateUser((ConsumerBean) consumerBean, user, null, false, false); return asCallback(callback, responseBean); } @RequestMapping("/user/profile") public @ResponseBody JSONPObject userProfile(HttpSession session, @RequestParam("user") String userId, @RequestParam(value = "models", required = false) String models, @RequestParam("jsonpCallback") String callback) { final ConsumerBean consumerBean = retrieveConsumer(session); if (logger.isDebugEnabled()) logger.debug("get user profile: " + userId + " for consumer: " + consumerBean.getShort_name()); ResourceBean responseBean = userProfileService.getProfile(consumerBean, userId, models); return asCallback(callback, responseBean); } @RequestMapping("/recommendations") public @ResponseBody JSONPObject userRecommendations(HttpSession session, @RequestParam(value = "attributes", required = false) String attributes, @RequestParam(value = "item", required = false) String itemId, @RequestParam(value = "rlabs", required = false) String lastRecommendationListUuid, @RequestParam(value = "zehtg", required = false) String lastRecommendationListUuid2, @RequestParam(value = "dimension", defaultValue = "0") Integer dimensionId, @RequestParam(value = "dimensions", required = false) String dimensionIds, @RequestParam(value = "limit", defaultValue = "10") Integer recommendationsLimit, @RequestParam(value = "algorithms", required = false) String algorithms, @RequestParam(value = "source", required = false) String referrer, @RequestParam(value = "rectag", required = false) String recTag, @RequestParam(value = "cohort", required = false, defaultValue = "false") Boolean includeCohort, @RequestParam(value = "sort", required = false) String scoreItems, @RequestParam(value = "rec_locale", required = false) String locale, @RequestParam("user") String userId, @RequestParam("jsonpCallback") String callback) { final ConsumerBean consumerBean = retrieveConsumer(session); MDCKeys.addKeys(consumerBean, userId, itemId,recTag); //added zehtg parameter as additional option for rlabs if(StringUtils.isNotBlank(lastRecommendationListUuid2)) { lastRecommendationListUuid=lastRecommendationListUuid2;} if (logger.isDebugEnabled()) logger.debug("Retrieving recommendations for user " + userId + ", consumer: " + consumerBean.getShort_name() + " with tag " + recTag); Set<Integer> dimensions; if (dimensionIds != null) { String[] parts = dimensionIds.split(","); dimensions = new HashSet<Integer>(parts.length); for(int i=0;i<parts.length;i++) dimensions.add(Integer.parseInt(parts[i])); } else { dimensions = new HashSet<Integer>(1); dimensions.add(dimensionId); } { // Map dimensions if necessary // map dimensions based on user String client_user_id = userId; dimensions = userDimensionMappingModelManager.getMappedDimensionsByUser(consumerBean.getShort_name(), dimensions, client_user_id); if (locale != null) { // map dimensions based on locale dimensions = dimensionsMappingManager.getMappedDimensionsByLocale(consumerBean.getShort_name(), dimensions, locale); } } final ResourceBean recommendations = getRecommendations(consumerBean, userId, itemId, dimensions, lastRecommendationListUuid, recommendationsLimit, attributes,algorithms,referrer,recTag,includeCohort,scoreItems,locale); //tracking recommendations impression StatsdPeer.logImpression(consumerBean.getShort_name(),recTag); CtrFullLogger.log(false, consumerBean.getShort_name(), userId, itemId,recTag); return asCallback(callback, recommendations); } @RequestMapping("/event/new") public @ResponseBody JSONPObject registerEvent(HttpSession session, HttpServletRequest request, @RequestParam("jsonpCallback") String callback) { final ConsumerBean consumerBean = retrieveConsumer(session); @SuppressWarnings("unchecked") Map<String,String[]> parameters = request.getParameterMap(); return asCallback(callback, predictionBusinessService.addEvent(consumerBean, parameters)); } @RequestMapping("/predict") public @ResponseBody JSONPObject predict(HttpSession session, HttpServletRequest request, @RequestParam(value = "json", required = false) String json, @RequestParam("jsonpCallback") String callback) { final ConsumerBean consumerBean = retrieveConsumer(session); @SuppressWarnings("unchecked") Map<String,String[]> parameters = request.getParameterMap(); return asCallback(callback, predictionBusinessService.predict(consumerBean, parameters)); } private ResourceBean getRecommendations(ConsumerBean consumerBean, String userId, String itemId, Set<Integer> dimensions, String lastRecommendationListUuid, Integer recommendationsLimit, String attributes, String algorithms,String referrer,String recTag, boolean includeCohort, String scoreItems,String locale) { Long internalItemId = null; if (itemId != null) { try { internalItemId = itemService.getInternalItemId(consumerBean, itemId); } catch (APIException e) { logger.warn("userRecommendations: item not found."); } } Set<Long> scoreItemsInternal = null; if (scoreItems != null && !scoreItems.isEmpty()) { String[] parts = scoreItems.split(","); scoreItemsInternal = new HashSet<Long>(); for(String externalSortId : parts) { try { Long internalSortId = itemService.getInternalItemId(consumerBean, externalSortId); scoreItemsInternal.add(internalSortId); } catch (APIException e) { logger.warn("userRecommendations: sort item not found."+externalSortId); } } } List<String> algList = null; if (algorithms != null && !algorithms.isEmpty()) { if (logger.isDebugEnabled()) logger.debug("ALGORITHM STRING: " + algorithms); algList = Arrays.asList(algorithms.split(Util.algOptionSeparator)); if (logger.isDebugEnabled()) logger.debug("alglist size: " + algList.size()); } if (logger.isDebugEnabled()) { logger.debug("JsClientController#getRecommendations: internal ID => " + internalItemId); logger.debug("JsClientController#getRecommendations: last recommendation list uuid => " + lastRecommendationListUuid); } return recommendationBusinessService.recommendedItemsForUser(consumerBean, userId, internalItemId, dimensions, lastRecommendationListUuid, recommendationsLimit, attributes,algList,referrer,recTag, includeCohort,scoreItemsInternal,locale); } // TODO category, tags private ItemBean createItem(ConsumerBean c, String itemId, String title, String type, String category, String subcategory, String tags, String imgUrl) { ItemBean item = null; try { item = itemService.getItem(c, itemId, false); } catch(APIException e) { //item does not exist return item; } //item not imported yet if(item.getType() == Constants.ITEM_NOT_VALID) { return null; } //Item Type int iType = item.getType(); if ( type != null && type.length()>0 ) { iType = Integer.getInteger(type); } //Attributes final Map<String, String> attributes = item.getAttributesName(); final Map<String, String> newAttributes = new HashMap<>(); //title if(title != null && title.length()>0 && !title.equals(attributes.get(Constants.ITEM_ATTR_TITLE))) { newAttributes.put(Constants.ITEM_ATTR_TITLE, title.trim()); } //image if(imgUrl != null && imgUrl.length()>0 && !imgUrl.equals(attributes.get(Constants.ITEM_ATTR_IMG))) { newAttributes.put(Constants.ITEM_ATTR_IMG, imgUrl.trim()); } //category if(category != null && category.length()>0 && !category.equals(attributes.get(Constants.ITEM_ATTR_CAT))) { newAttributes.put(Constants.ITEM_ATTR_CAT, category.trim()); } //subcategory if(subcategory != null && subcategory.length()>0 && !subcategory.equals(attributes.get(Constants.ITEM_ATTR_SUBCAT))) { newAttributes.put(Constants.ITEM_ATTR_SUBCAT, subcategory.trim()); } //tags if(tags != null && tags.length()>0 && !tags.equals(attributes.get(Constants.ITEM_ATTR_TAGS))) { newAttributes.put(Constants.ITEM_ATTR_TAGS, tags.trim()); } //Check if the item needs to be updated boolean update = false; if(iType != item.getType()) { item.setType(iType); update = true; } if(newAttributes.size()>0) { item.setAttributesName(newAttributes); update = true; } if(update) { return item; } else { return null; } } private ActionBean createAction(String userId, String itemId, Integer type,String referrer,String recTag,String extraData) { int safeType = type == null ? 0 : type; ActionBean a = new ActionBean(null, userId, itemId, safeType, new Date(), 0.0, 1); a.setReferrer(referrer); a.setRecTag(recTag); a.setExtraData(extraData); return a; } private JSONPObject asCallback(String callbackName, Object valueObject) { return new JSONPObject(callbackName, valueObject); } }