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