package org.oasis_open.contextserver.web; /* * #%L * context-server-wab * $Id:$ * $HeadURL:$ * %% * Copyright (C) 2014 - 2015 Jahia Solutions * %% * 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. * #L% */ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; import org.oasis_open.contextserver.api.*; import org.oasis_open.contextserver.api.conditions.Condition; import org.oasis_open.contextserver.api.services.EventService; import org.oasis_open.contextserver.api.services.ProfileService; import org.oasis_open.contextserver.api.services.RulesService; import org.oasis_open.contextserver.api.services.SegmentService; import org.oasis_open.contextserver.persistence.spi.CustomObjectMapper; import org.ops4j.pax.cdi.api.OsgiService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.util.*; /** * A servlet filter to serve a context-specific Javascript containing the current request context object. */ @WebServlet(urlPatterns = {"/context.js", "/context.json"}) public class ContextServlet extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(ContextServlet.class.getName()); private static final long serialVersionUID = 2928875830103325238L; public static final String BASE_SCRIPT_LOCATION = "/WEB-INF/javascript/base.js"; public static final String IMPERSONATE_BASE_SCRIPT_LOCATION = "/WEB-INF/javascript/impersonateBase.js"; public static final String PROFILE_OVERRIDE_MARKER = "---IGNORE---"; @Inject @OsgiService private ProfileService profileService; @Inject @OsgiService private SegmentService segmentService; @Inject @OsgiService private RulesService rulesService; private String profileIdCookieName = "context-profile-id"; // private String personaIdCookieName = "context-persona-id"; @Inject @OsgiService private EventService eventService; @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { final Date timestamp = new Date(); if (request.getParameter("timestamp") != null) { timestamp.setTime(Long.parseLong(request.getParameter("timestamp"))); } // first we must retrieve the context for the current visitor, and build a Javascript object to attach to the // script output. String profileId; HttpServletRequest httpServletRequest = (HttpServletRequest) request; String httpMethod = httpServletRequest.getMethod(); // logger.debug(HttpUtils.dumpRequestInfo(httpServletRequest)); // set up CORS headers as soon as possible so that errors are not misconstrued on the client for CORS errors HttpUtils.setupCORSHeaders(httpServletRequest, response); if ("options".equals(httpMethod.toLowerCase())) { response.flushBuffer(); return; } Profile profile = null; String cookieProfileId = null; String cookiePersonaId = null; Cookie[] cookies = httpServletRequest.getCookies(); for (Cookie cookie : cookies) { if (profileIdCookieName.equals(cookie.getName())) { cookieProfileId = cookie.getValue(); } } Session session = null; String personaId = request.getParameter("personaId"); if (personaId != null) { PersonaWithSessions personaWithSessions = profileService.loadPersonaWithSessions(personaId); if (personaWithSessions == null) { logger.error("Couldn't find persona with id=" + personaId); profile = null; } else { profile = personaWithSessions.getPersona(); session = personaWithSessions.getLastSession(); } } String sessionId = request.getParameter("sessionId"); boolean profileCreated = false; ContextRequest contextRequest = null; String scope = null; String stringPayload = HttpUtils.getPayload(httpServletRequest); if (stringPayload != null) { ObjectMapper mapper = CustomObjectMapper.getObjectMapper(); JsonFactory factory = mapper.getFactory(); try { contextRequest = mapper.readValue(factory.createParser(stringPayload), ContextRequest.class); } catch (Exception e) { logger.error("Cannot read payload " + stringPayload, e); return; } scope = contextRequest.getSource().getScope(); } int changes = EventService.NO_CHANGE; if (profile == null) { if (sessionId != null) { session = profileService.loadSession(sessionId, timestamp); if (session != null) { profileId = session.getProfileId(); profile = profileService.load(profileId); profile = checkMergedProfile(response, profile, session); } } if (profile == null) { // profile not stored in session if (cookieProfileId == null) { // no profileId cookie was found, we generate a new one and create the profile in the profile service profile = createNewProfile(null, response, timestamp); profileCreated = true; } else { profile = profileService.load(cookieProfileId); if (profile == null) { // this can happen if we have an old cookie but have reset the server, // or if we merged the profiles and somehow this cookie didn't get updated. profile = createNewProfile(null, response, timestamp); profileCreated = true; HttpUtils.sendProfileCookie(profile, response, profileIdCookieName); } else { profile = checkMergedProfile(response, profile, session); } } } else if (cookieProfileId == null || !cookieProfileId.equals(profile.getItemId())) { // profile if stored in session but not in cookie HttpUtils.sendProfileCookie(profile, response, profileIdCookieName); } // associate profile with session if (sessionId != null && session == null) { session = new Session(sessionId, profile, timestamp, scope); changes |= EventService.SESSION_UPDATED; Event event = new Event("sessionCreated", session, profile, scope, null, session, timestamp); event.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request); event.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response); logger.debug("Received event " + event.getEventType() + " for profile=" + profile.getItemId() + " session=" + session.getItemId() + " target=" + event.getTarget() + " timestamp=" + timestamp); changes |= eventService.send(event); } } if (profileCreated) { changes |= EventService.PROFILE_UPDATED; Event profileUpdated = new Event("profileUpdated", session, profile, scope, null, profile, timestamp); profileUpdated.setPersistent(false); profileUpdated.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request); profileUpdated.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response); logger.debug("Received event {} for profile={} {} target={} timestamp={}", profileUpdated.getEventType(), profile.getItemId(), session != null ? " session=" + session.getItemId() : "", profileUpdated.getTarget(), timestamp); changes |= eventService.send(profileUpdated); } ContextResponse data = new ContextResponse(); if(contextRequest != null){ changes |= handleRequest(contextRequest, profile, session, data, request, response, timestamp); } if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED && profile != null) { profileService.save(profile); } if ((changes & EventService.SESSION_UPDATED) == EventService.SESSION_UPDATED && session != null) { profileService.saveSession(session); } String extension = httpServletRequest.getRequestURI().substring(httpServletRequest.getRequestURI().lastIndexOf(".") + 1); boolean noScript = "json".equals(extension); String contextAsJSONString = CustomObjectMapper.getObjectMapper().writeValueAsString(data); Writer responseWriter; if(noScript){ response.setCharacterEncoding("UTF-8"); responseWriter = response.getWriter(); response.setContentType("application/json"); IOUtils.write(contextAsJSONString, responseWriter); }else { responseWriter = response.getWriter(); responseWriter.append("window.digitalData = window.digitalData || {};\n") .append("var cxs = ") .append(contextAsJSONString) .append(";\n"); // now we copy the base script source code InputStream baseScriptStream = getServletContext().getResourceAsStream(profile instanceof Persona ? IMPERSONATE_BASE_SCRIPT_LOCATION : BASE_SCRIPT_LOCATION); IOUtils.copy(baseScriptStream, responseWriter); } responseWriter.flush(); } private Profile checkMergedProfile(ServletResponse response, Profile profile, Session session) { String profileId; if (profile != null && profile.getMergedWith() != null) { profileId = profile.getMergedWith(); Profile profileToDelete = profile; profile = profileService.load(profileId); if (profile != null) { logger.debug("Session profile was merged with profile " + profileId + ", replacing profile in session"); if (session != null) { session.setProfile(profile); profileService.saveSession(session); } HttpUtils.sendProfileCookie(profile, response, profileIdCookieName); } else { logger.warn("Couldn't find merged profile" + profileId + ", falling back to profile " + profileToDelete.getItemId()); profile = profileToDelete; profile.setMergedWith(null); profileService.save(profile); } } return profile; } private int handleRequest(ContextRequest contextRequest, Profile profile, Session session, ContextResponse data, ServletRequest request, ServletResponse response, Date timestamp) throws IOException { int changes = EventService.NO_CHANGE; // execute provided events if any if(contextRequest.getEvents() != null) { for (Event event : contextRequest.getEvents()){ if(event.getEventType() != null) { Event eventToSend; if(event.getProperties() != null){ eventToSend = new Event(event.getEventType(), session, profile, contextRequest.getSource().getScope(), event.getSource(), event.getTarget(), event.getProperties(), timestamp); } else { eventToSend = new Event(event.getEventType(), session, profile, contextRequest.getSource().getScope(), event.getSource(), event.getTarget(), timestamp); } event.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request); event.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response); logger.debug("Received event " + event.getEventType() + " for profile=" + profile.getItemId() + " session=" + session.getItemId() + " target=" + event.getTarget() + " timestamp=" + timestamp); changes |= eventService.send(eventToSend); } } } data.setProfileId(profile.getItemId()); if (contextRequest.isRequireSegments()) { data.setProfileSegments(profile.getSegments()); } if (contextRequest.getRequiredProfileProperties() != null) { Map<String, Object> profileProperties = new HashMap<String, Object>(profile.getProperties()); if (!contextRequest.getRequiredProfileProperties().contains("*")) { profileProperties.keySet().retainAll(contextRequest.getRequiredProfileProperties()); } data.setProfileProperties(profileProperties); } if (session != null) { data.setSessionId(session.getItemId()); if (contextRequest.getRequiredSessionProperties() != null) { Map<String, Object> sessionProperties = new HashMap<String, Object>(session.getProperties()); if (!contextRequest.getRequiredSessionProperties().contains("*")) { sessionProperties.keySet().retainAll(contextRequest.getRequiredSessionProperties()); } data.setSessionProperties(sessionProperties); } } processOverrides(contextRequest, profile, session); List<ContextRequest.FilteredContent> filterNodes = contextRequest.getFilters(); if (filterNodes != null) { data.setFilteringResults(new HashMap<String, Boolean>()); for (ContextRequest.FilteredContent filteredContent : filterNodes) { boolean result = true; for (ContextRequest.Filter filter : filteredContent.getFilters()) { Condition condition = filter.getCondition(); result &= profileService.matchCondition(condition, profile, session); } data.getFilteringResults().put(filteredContent.getFilterid(), result); } } data.setTrackedConditions(rulesService.getTrackedConditions(contextRequest.getSource())); return changes; } private void processOverrides(ContextRequest contextRequest, Profile profile, Session session) { if (contextRequest.getSegmentOverrides() != null && contextRequest.getSegmentOverrides().size() > 0) { Set<String> segments = profile.getSegments(); for (String segmentOverride : contextRequest.getSegmentOverrides()) { if (segmentOverride == null) { continue; } if (segmentOverride.startsWith(PROFILE_OVERRIDE_MARKER)) { segments.remove(segmentOverride.substring(PROFILE_OVERRIDE_MARKER.length())); } else { segments.add(segmentOverride); } } profile.setSegments(segments); } if (contextRequest.getProfilePropertiesOverrides() != null && contextRequest.getProfilePropertiesOverrides().size() > 0) { Map<String,Object> profileProperties = profile.getProperties(); for (Map.Entry<String,Object> profilePropertyOverride : contextRequest.getProfilePropertiesOverrides().entrySet()) { if (profilePropertyOverride.getKey() == null) { continue; } if (PROFILE_OVERRIDE_MARKER.equals(profilePropertyOverride.getValue())) { profileProperties.remove(profilePropertyOverride.getKey()); } else { profileProperties.put(profilePropertyOverride.getKey(), profilePropertyOverride.getValue()); } } profile.setProperties(profileProperties); // we do this just in case a cache is behind this } if (contextRequest.getSessionPropertiesOverrides() != null && contextRequest.getSessionPropertiesOverrides().size() > 0) { Map<String,Object> sessionProperties = session.getProperties(); for (Map.Entry<String,Object> sessionPropertyOverride : contextRequest.getSessionPropertiesOverrides().entrySet()) { if (sessionPropertyOverride.getKey() == null) { continue; } if (PROFILE_OVERRIDE_MARKER.equals(sessionPropertyOverride.getValue())) { sessionProperties.remove(sessionPropertyOverride.getKey()); } else { sessionProperties.put(sessionPropertyOverride.getKey(), sessionPropertyOverride.getValue()); } } session.setProperties(sessionProperties); // we do this just in case a cache is behind this } } private Profile createNewProfile(String existingProfileId, ServletResponse response, Date timestamp) { Profile profile; String profileId = existingProfileId; if (profileId == null) { profileId = UUID.randomUUID().toString(); } profile = new Profile(profileId); profile.setProperty("firstVisit", timestamp); HttpUtils.sendProfileCookie(profile, response, profileIdCookieName); return profile; } public void destroy() { } }