001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
018 */
019
020package org.nuxeo.segment.io;
021
022import java.io.Serializable;
023import java.lang.reflect.Field;
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.joda.time.DateTime;
034import org.nuxeo.ecm.core.api.NuxeoPrincipal;
035import org.nuxeo.runtime.api.Framework;
036import org.nuxeo.runtime.model.ComponentContext;
037import org.nuxeo.runtime.model.ComponentInstance;
038import org.nuxeo.runtime.model.DefaultComponent;
039import org.nuxeo.segment.io.extension.Group;
040import org.osgi.framework.Bundle;
041
042import com.github.segmentio.Analytics;
043import com.github.segmentio.AnalyticsClient;
044import com.github.segmentio.flush.Flusher;
045import com.github.segmentio.models.Context;
046import com.github.segmentio.models.EventProperties;
047import com.github.segmentio.models.Providers;
048import com.github.segmentio.models.Traits;
049
050/**
051 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
052 */
053public class SegmentIOComponent extends DefaultComponent implements SegmentIO {
054
055    protected static Log log = LogFactory.getLog(SegmentIOComponent.class);
056
057    protected static final String DEFAULT_DEBUG_KEY = "FakeKey_ChangeMe";
058
059    public final static String WRITE_KEY = "segment.io.write.key";
060
061    public final static String CONFIG_EP = "config";
062
063    public final static String MAPPER_EP = "mapper";
064
065    public final static String PROVIDER_EP = "providers";
066
067    public final static String FILTERS_EP = "filters";
068
069    protected boolean debugMode = false;
070
071    protected Map<String, SegmentIOMapper> mappers;
072
073    protected Map<String, List<SegmentIOMapper>> event2Mappers = new HashMap<>();
074
075    protected List<Map<String, Object>> testData = new LinkedList<>();
076
077    protected SegmentIOConfig config;
078
079    protected SegmentIOProviders providersConfig;
080
081    protected SegmentIOUserFilter userFilters;
082
083    protected Providers providers;
084
085    protected Bundle bundle;
086
087    protected Flusher flusher;
088
089    public Bundle getBundle() {
090        return bundle;
091    }
092
093    @Override
094    public void activate(ComponentContext context) {
095        bundle = context.getRuntimeContext().getBundle();
096        mappers = new HashMap<>();
097    }
098
099    @Override
100    public void deactivate(ComponentContext context) {
101        flush();
102        bundle = null;
103    }
104
105    @Override
106    public void registerContribution(Object contribution,
107            String extensionPoint, ComponentInstance contributor) {
108        if (CONFIG_EP.equalsIgnoreCase(extensionPoint)) {
109            config = (SegmentIOConfig) contribution;
110        } else if (MAPPER_EP.equalsIgnoreCase(extensionPoint)) {
111            SegmentIOMapper mapper = (SegmentIOMapper) contribution;
112            mappers.put(mapper.name, mapper);
113        } else if (PROVIDER_EP.equalsIgnoreCase(extensionPoint)) {
114            providersConfig = (SegmentIOProviders) contribution;
115            providers = null;
116        } else if (FILTERS_EP.equalsIgnoreCase(extensionPoint)) {
117            userFilters = (SegmentIOUserFilter) contribution;
118        }
119    }
120
121    @Override
122    public void applicationStarted(ComponentContext context)  {
123        String key = getWriteKey();
124        if (DEFAULT_DEBUG_KEY.equals(key)) {
125            log.info("Run Segment.io in debug mode : nothing will be sent to the server");
126            debugMode = true;
127        } else {
128            Analytics.initialize(key);
129        }
130        computeEvent2Mappers();
131    }
132
133    protected void computeEvent2Mappers() {
134        event2Mappers = new HashMap<String, List<SegmentIOMapper>>();
135        for (SegmentIOMapper mapper : mappers.values()) {
136            for (String event : mapper.events) {
137                List<SegmentIOMapper> m4event = event2Mappers.get(event);
138                if (m4event == null) {
139                    event2Mappers.put(event, new ArrayList<SegmentIOMapper>());
140                    m4event = event2Mappers.get(event);
141                }
142                if (!m4event.contains(mapper)) {
143                    m4event.add(mapper);
144                }
145            }
146        }
147    }
148
149    @Override
150    public String getWriteKey() {
151        if (config != null) {
152            if (config.writeKey != null) {
153                return config.writeKey;
154            }
155        }
156        return Framework.getProperty(WRITE_KEY, DEFAULT_DEBUG_KEY);
157    }
158
159    @Override
160    public Map<String, String> getGlobalParameters() {
161        if (config != null) {
162            if (config.parameters != null) {
163                return config.parameters;
164            }
165        }
166        return new HashMap<>();
167    }
168
169    protected Flusher getFlusher() {
170        if (flusher == null) {
171            try {
172                AnalyticsClient client = Analytics.getDefaultClient();
173                Field field = client.getClass().getDeclaredField("flusher");
174                field.setAccessible(true);
175                flusher = (Flusher) field.get(client);
176            } catch (ReflectiveOperationException e) {
177                log.error("Unable to access SegmentIO Flusher via reflection",
178                        e);
179            }
180        }
181        return flusher;
182    }
183
184    @Override
185    public void identify(NuxeoPrincipal principal) {
186        identify(principal, null);
187    }
188
189    @Override
190    public Providers getProviders() {
191        if (providers == null) {
192            providers = new Providers();
193            if (providersConfig != null) {
194                if (!providersConfig.enableDefaults) {
195                    providers.setDefault(false);
196                }
197                for (String name : providersConfig.providers.keySet()) {
198                    providers.setEnabled(name,
199                            providersConfig.providers.get(name));
200                }
201            }
202        }
203        return providers;
204    }
205
206    @Override
207    public void identify(NuxeoPrincipal principal,
208            Map<String, Serializable> metadata) {
209
210        SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal,
211                metadata);
212
213        if (!mustTrackprincipal(wrapper.getUserId())) {
214            log.debug("Skip user " + principal.getName());
215            return;
216        }
217
218        if (Framework.isTestModeSet()) {
219            pushForTest("identify", wrapper.getUserId(), null, metadata);
220            Map<String, Serializable> groupMeta = wrapper.getGroupMetadata();
221            if (groupMeta.size() > 0 && groupMeta.containsKey("id")) {
222                pushForTest("group", wrapper.getUserId(), "group", groupMeta);
223            }
224        } else {
225            if (debugMode) {
226                log.info("send identify for " + wrapper.getUserId()
227                        + " with meta : " + metadata.toString());
228            } else {
229                log.debug("send identify with " + metadata.toString());
230                Traits traits = new Traits();
231                traits.putAll(wrapper.getMetadata());
232                Context ctx = new Context();
233                ctx.setProviders(getProviders());
234                Analytics.identify(wrapper.getUserId(), traits, ctx);
235
236                Map<String, Serializable> groupMeta = wrapper.getGroupMetadata();
237                if (groupMeta.size() > 0 && groupMeta.containsKey("id")) {
238                    Traits gtraits = new Traits();
239                    gtraits.putAll(groupMeta);
240                    group((String) groupMeta.get("id"), wrapper.getUserId(),
241                            gtraits, ctx);
242                } else {
243                    // automatic grouping
244                    if (principal.getCompany() != null) {
245                        group(principal.getCompany(), wrapper.getUserId(),
246                                null, ctx);
247                    } else if (wrapper.getMetadata().get("company") != null) {
248                        group((String) wrapper.getMetadata().get("company"),
249                                wrapper.getUserId(), null, ctx);
250                    }
251                }
252            }
253        }
254    }
255
256    protected void group(String groupId, String userId, Traits traits,
257            Context ctx) {
258        if (groupId == null || groupId.isEmpty()) {
259            return;
260        }
261        Flusher flusher = getFlusher();
262        if (flusher != null) {
263            Group grp = new Group(userId, groupId, traits, new DateTime(), ctx);
264            flusher.enqueue(grp);
265        } else {
266            log.warn("Can not use Group API");
267        }
268    }
269
270    protected void pushForTest(String action, String principalName,
271            String eventName, Map<String, Serializable> metadata) {
272        Map<String, Object> data = new HashMap<>();
273        if (metadata != null) {
274            data.putAll(metadata);
275        }
276        data.put("action", action);
277        if (eventName != null) {
278            data.put("eventName", eventName);
279        }
280        data.put(SegmentIODataWrapper.PRINCIPAL_KEY, principalName);
281        testData.add(data);
282    }
283
284    public List<Map<String, Object>> getTestData() {
285        return testData;
286    }
287
288    protected boolean mustTrackprincipal(String principalName) {
289        SegmentIOUserFilter filter = getUserFilters();
290        if (filter == null) {
291            return true;
292        }
293        return filter.canTrack(principalName);
294    }
295
296    @Override
297    public void track(NuxeoPrincipal principal, String eventName) {
298        track(principal, null);
299    }
300
301    @Override
302    public void track(NuxeoPrincipal principal, String eventName,
303            Map<String, Serializable> metadata) {
304
305        SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal,
306                metadata);
307
308        if (!mustTrackprincipal(wrapper.getUserId())) {
309            log.debug("Skip user " + principal.getName());
310            return;
311        }
312
313        if (Framework.isTestModeSet()) {
314            pushForTest("track", wrapper.getUserId(), eventName, metadata);
315        } else {
316            if (debugMode) {
317                log.info("send track for " + eventName + " user : "
318                        + wrapper.getUserId() + " with meta : "
319                        + metadata.toString());
320            } else {
321                log.debug("send track with " + metadata.toString());
322                EventProperties eventProperties = new EventProperties();
323                eventProperties.putAll(wrapper.getMetadata());
324                Context ctx = new Context();
325                ctx.setProviders(getProviders());
326                Analytics.track(wrapper.getUserId(), eventName,
327                        eventProperties, new DateTime(), ctx);
328            }
329        }
330    }
331
332    @Override
333    public void flush() {
334        if (!debugMode) {
335            // only flush if Analytics was actually initialized
336            Analytics.flush();
337        }
338    }
339
340    public Map<String, List<SegmentIOMapper>> getMappers(List<String> events) {
341        Map<String, List<SegmentIOMapper>> targetMappers = new HashMap<String, List<SegmentIOMapper>>();
342        for (String event : events) {
343            if (event2Mappers.containsKey(event)) {
344                targetMappers.put(event, event2Mappers.get(event));
345            }
346        }
347        return targetMappers;
348    }
349
350    public Set<String> getMappedEvents() {
351        return event2Mappers.keySet();
352    }
353
354    public Map<String, List<SegmentIOMapper>> getAllMappers() {
355        return event2Mappers;
356    }
357
358    @Override
359    public SegmentIOUserFilter getUserFilters() {
360        return userFilters;
361    }
362
363}