You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2017/07/19 12:29:02 UTC

[1/2] incubator-unomi git commit: UNOMI-116 Salesforce CRM Connector - Initial commit

Repository: incubator-unomi
Updated Branches:
  refs/heads/master 139e1b751 -> a897ab843


http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImpl.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImpl.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImpl.java
new file mode 100644
index 0000000..7ef6bea
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImpl.java
@@ -0,0 +1,815 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services.internal;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
+import org.apache.http.Header;
+import org.apache.http.HttpException;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.*;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.sfdc.services.SFDCConfiguration;
+import org.apache.unomi.sfdc.services.SFDCService;
+import org.apache.unomi.sfdc.services.SFDCSession;
+import org.cometd.bayeux.Channel;
+import org.cometd.bayeux.Message;
+import org.cometd.bayeux.client.ClientSessionChannel;
+import org.cometd.client.BayeuxClient;
+import org.cometd.client.transport.ClientTransport;
+import org.cometd.client.transport.LongPollingTransport;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.ajax.JSON;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.*;
+
+/**
+ * Implementation of the Salesforce connector interface
+ */
+public class SFDCServiceImpl implements SFDCService {
+    private static final Logger logger = LoggerFactory.getLogger(SFDCServiceImpl.class.getName());
+
+    private static final String REST_ENDPOINT_URI = "/services/data/v38.0";
+    private static final String STREAMING_ENDPOINT_URI = "/cometd/38.0";
+
+    private static final int CONNECTION_TIMEOUT = 20 * 1000;  // milliseconds
+    private static final int READ_TIMEOUT = 120 * 1000; // milliseconds
+
+    private SFDCConfiguration sfdcConfiguration;
+    private SFDCConfiguration defaultSFDCConfiguration;
+
+    private Set<String> sfdcLeadMandatoryFields = new TreeSet<>();
+    private Set<String> sfdcLeadUpdateableFields = new TreeSet<>();
+
+    private SFDCSession sfdcSession;
+    private DateFormat iso8601DateFormat = new ISO8601DateFormat();
+
+    private PersistenceService persistenceService;
+
+    public void setPersistenceService(PersistenceService persistenceService) {
+        this.persistenceService = persistenceService;
+    }
+
+    public void setDefaultSFDCConfiguration(SFDCConfiguration defaultSFDCConfiguration) {
+        this.defaultSFDCConfiguration = defaultSFDCConfiguration;
+    }
+
+    public SFDCSession getSFDCSession() {
+        return sfdcSession;
+    }
+
+    @Override
+    public SFDCConfiguration loadConfiguration() {
+        if (persistenceService == null) {
+            return null;
+        }
+        SFDCConfiguration sfdcConfiguration = persistenceService.load("sfdcConfiguration", SFDCConfiguration.class);
+        return sfdcConfiguration;
+    }
+
+    @Override
+    public boolean saveConfiguration(SFDCConfiguration sfdcConfiguration) {
+        if (persistenceService == null) {
+            return false;
+        }
+        boolean result = persistenceService.save(sfdcConfiguration);
+        if (result) {
+            this.sfdcConfiguration = sfdcConfiguration;
+            try {
+                if (login(sfdcConfiguration)) {
+                    return true;
+                }
+            } catch (HttpException e) {
+                logger.warn("Error trying to login with new configuration {}", sfdcConfiguration, e);
+                result = false;
+            } catch (IOException e) {
+                logger.warn("Error trying to login with new configuration {}", sfdcConfiguration, e);
+                result = false;
+            }
+        } else {
+            logger.error("Error trying to save new Salesforce connection configuration !");
+        }
+        return result;
+    }
+
+    public void start() {
+        try {
+            iso8601DateFormat = new ISO8601DateFormat();
+
+            SFDCConfiguration sfdcConfiguration = loadConfiguration();
+            if (sfdcConfiguration != null) {
+                this.sfdcConfiguration = sfdcConfiguration;
+            } else {
+                this.sfdcConfiguration = defaultSFDCConfiguration;
+            }
+
+            if (this.sfdcConfiguration.isComplete()) {
+                boolean loginSuccessful = login(this.sfdcConfiguration);
+                if (!loginSuccessful) {
+                    throw new Exception("Login failed");
+                }
+                sfdcLeadMandatoryFields = getLeadMandatoryFields();
+                // setupPushTopics(SFDCSession.getEndPoint(), SFDCSession.getSessionId());
+                logger.info("Salesforce connector initialized successfully.");
+            } else {
+                logger.warn("Salesforce connector is not yet configured.");
+            }
+        } catch (HttpException | IOException e) {
+            logger.error("Failed to init SFDCService properly", e);
+        } catch (Exception e) {
+            logger.error("Failed to init SFDCService properly", e);
+        }
+    }
+
+    public void stop() {
+    }
+
+    public Set<String> getRecentLeadIds() {
+        if (!isConnected()) {
+            return null;
+        }
+        Set<String> recentLeadIds = new LinkedHashSet<>();
+
+        String baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/Lead";
+        HttpGet getRecentLeads = new HttpGet(baseUrl);
+
+        try {
+            Object responseObject = handleRequest(getRecentLeads);
+            if (responseObject == null) {
+                logger.warn("Couldn't retrieve recent leads");
+                return null;
+            }
+            Map<String, Object> queryResponse = (Map<String, Object>) responseObject;
+            if (queryResponse.containsKey("recentItems")) {
+                logger.debug("Response received from Salesforce: {}", queryResponse);
+                Object[] recentItems = (Object[]) queryResponse.get("recentItems");
+                for (Object recentItem : recentItems) {
+                    Map<String, String> recentItemMap = (Map<String, String>) recentItem;
+                    recentLeadIds.add(recentItemMap.get("Id"));
+                }
+            }
+
+        } catch (IOException e) {
+            logger.error("Error getting recent leads", e);
+        } catch (HttpException e) {
+            logger.error("Error getting recent leads", e);
+        }
+
+        return recentLeadIds;
+    }
+
+    public Map<String,Object> getSObject(String sobjectName, String objectId) {
+        if (!isConnected()) {
+            return null;
+        }
+        Map<String, Object> sobjectMap = new LinkedHashMap<>();
+
+        String baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/" + sobjectName +"/" + objectId;
+        HttpGet getSObject = new HttpGet(baseUrl);
+
+        try {
+            Object responseObject = handleRequest(getSObject);
+            if (responseObject == null) {
+                logger.warn("Couldn't retrieve sobject {} with id {}", sobjectName, objectId);
+                return null;
+            }
+            Map<String, Object> queryResponse = (Map<String, Object>) responseObject;
+            if (queryResponse != null) {
+                logger.debug("Response received from Salesforce: {}", queryResponse);
+                sobjectMap = new LinkedHashMap<>(queryResponse);
+            }
+
+        } catch (IOException e) {
+            logger.error("Error getting sobject {} with id {}", sobjectName, objectId, e);
+        } catch (HttpException e) {
+            logger.error("Error getting sobject {} with id {}", sobjectName, objectId, e);
+        }
+        return sobjectMap;
+    }
+
+    public Map<String,Object> getSObjectDescribe(String sobjectName) {
+        Map<String, Object> sobjectDescribe = new LinkedHashMap<>();
+        if (!isConnected()) {
+            return null;
+        }
+
+        String baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/" + sobjectName +"/describe";
+        HttpGet getSObjectDescribe = new HttpGet(baseUrl);
+
+        try {
+            Object responseObject = handleRequest(getSObjectDescribe);
+            if (responseObject == null) {
+                logger.warn("Couldn't retrieve sobject {} describe", sobjectName);
+                return null;
+            }
+            Map<String, Object> queryResponse = (Map<String, Object>) responseObject;
+            if (queryResponse != null) {
+                logger.debug("Response received from Salesforce: {}", queryResponse);
+                sobjectDescribe = new LinkedHashMap<>(queryResponse);
+            }
+
+        } catch (IOException e) {
+            logger.error("Error getting sobject {}", sobjectName, e);
+        } catch (HttpException e) {
+            logger.error("Error getting sobject {}", sobjectName, e);
+        }
+        return sobjectDescribe;
+    }
+
+    public Map<String, Object> getLead(String leadId) {
+        return getSObject("Lead", leadId);
+    }
+
+    public Set<String> getLeadMandatoryFields() {
+        Set<String> mandatoryFields = new TreeSet<>();
+        if (!isConnected()) {
+            return null;
+        }
+        Map<String,Object> leadDescribe = getSObjectDescribe("Lead");
+        Object[] fields = (Object[]) leadDescribe.get("fields");
+        Set<String> updateableFields = new TreeSet<>();
+        Set<String> compoundFieldNames = new TreeSet<>();
+        for (Object field : fields) {
+            Map<String,Object> fieldDescribe = (Map<String,Object>) field;
+            String fieldName = (String) fieldDescribe.get("name");
+            String compoundFieldName = (String) fieldDescribe.get("compoundFieldName");
+            if (compoundFieldName != null) {
+                compoundFieldNames.add(compoundFieldName);
+            }
+            String fieldType = (String) fieldDescribe.get("type");
+            Boolean fieldUpdateable = (Boolean) fieldDescribe.get("updateable");
+            Boolean fieldCreateable = (Boolean) fieldDescribe.get("createable");
+            Boolean fieldDefaultedOnCreate = (Boolean) fieldDescribe.get("defaultedOnCreate");
+            Boolean fieldNillable = (Boolean) fieldDescribe.get("nillable");
+            if (fieldUpdateable) {
+                updateableFields.add(fieldName);
+            }
+            if (!fieldNillable && !fieldDefaultedOnCreate) {
+                mandatoryFields.add(fieldName);
+            }
+        }
+        mandatoryFields.removeAll(compoundFieldNames);
+        updateableFields.removeAll(compoundFieldNames);
+        sfdcLeadUpdateableFields = updateableFields;
+        return mandatoryFields;
+    }
+
+    public boolean deleteLead(String leadId) {
+        if (!isConnected()) {
+            return false;
+        }
+        String baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/Lead/" + leadId;
+        HttpDelete deleteLead = new HttpDelete(baseUrl);
+        try {
+            Object responseObject = handleRequest(deleteLead);
+        } catch (IOException e) {
+            logger.error("Error deleting lead {}", leadId, e);
+        } catch (HttpException e) {
+            logger.error("Error deleting lead {}", leadId, e);
+        }
+        return true;
+    }
+
+    public Set<String> findLeadIdsByIdentifierValue(String identifierFieldValue) {
+        Set<String> results = new LinkedHashSet<String>();
+        if (!isConnected()) {
+            return results;
+        }
+        Object response = query("SELECT Id FROM Lead WHERE " + sfdcConfiguration.getSfdcIdentifierField() + "='" + identifierFieldValue + "'");
+        if (response == null) {
+            return results;
+        }
+        Map<String, Object> result = (Map<String, Object>) response;
+        Long totalSize = (Long) result.get("totalSize");
+        Boolean done = (Boolean) result.get("done");
+        Object[] recordObjects = (Object[]) result.get("records");
+        if (totalSize == null || totalSize < 1) {
+            return results;
+        }
+        for (Object recordObject : recordObjects) {
+            Map<String, Object> record = (Map<String, Object>) recordObject;
+            if (record.containsKey("Id")) {
+                results.add((String) record.get("Id"));
+            }
+        }
+        return results;
+    }
+
+    @Override
+    public String createOrUpdateLead(Profile profile) {
+        if (!isConnected()) {
+            return null;
+        }
+        // first we must check if an existing lead exists for the profile.
+        String unomiIdentifierValue = (String) profile.getProperty(sfdcConfiguration.getUnomiIdentifierField());
+        logger.info("Checking if we have a lead for identifier value {}...", unomiIdentifierValue);
+        Set<String> foundExistingSfdcLeadIds = findLeadIdsByIdentifierValue(unomiIdentifierValue);
+
+        Map<String, Object> sfdcLeadFields = new HashMap<>();
+        Map<String, Object> existingSfdcLeadFields = new HashMap<>();
+        Date sfdcLastModified = null;
+
+        if (foundExistingSfdcLeadIds.size() > 1) {
+            // we found multiple leads matching the identifier value !
+            logger.warn("Found multiple matching leads for identifier value {}, will use first matching one !", unomiIdentifierValue);
+        }
+
+        if (foundExistingSfdcLeadIds.size() > 0) {
+            logger.info("Found an existing lead, attempting to update it...");
+            // we found an existing lead we must update it
+            existingSfdcLeadFields = getLead(foundExistingSfdcLeadIds.iterator().next());
+            if (existingSfdcLeadFields.get("LastModifiedDate") != null) {
+                try {
+                    sfdcLastModified = iso8601DateFormat.parse((String) existingSfdcLeadFields.get("LastModifiedDate"));
+                } catch (ParseException e) {
+                    logger.error("Error parsing date {}", existingSfdcLeadFields.get("LastModifiedDate"), e);
+                }
+            }
+        } else {
+            logger.info("No existing lead found.");
+        }
+
+        for (String profilePropertyKey : profile.getProperties().keySet()) {
+            String sfdcFieldName = sfdcConfiguration.getUnomiToSfdcFieldMappings().get(profilePropertyKey);
+            if (sfdcFieldName == null) {
+                // we skip unmapped fields
+                continue;
+            }
+            Object unomiPropertyValue = profile.getProperties().get(profilePropertyKey);
+            if (existingSfdcLeadFields.get(sfdcFieldName) == null) {
+                // we only set the field if it didn't have a value.
+                logger.info("Setting SFDC field {} value to {}", sfdcFieldName, unomiPropertyValue);
+                sfdcLeadFields.put(sfdcFieldName, unomiPropertyValue);
+            } else {
+                // current strategy : Unomi field value wins if different from Salesforce value
+                // @todo we should probably improve this by tracking last modification dates on profile/lead properties
+                Object sfdcLeadFieldValue = existingSfdcLeadFields.get(sfdcFieldName);
+                if (!unomiPropertyValue.equals(sfdcLeadFieldValue)) {
+                    logger.info("Overwriting SFDC field {} value to {}", sfdcFieldName, unomiPropertyValue);
+                    sfdcLeadFields.put(sfdcFieldName, unomiPropertyValue);
+                }
+            }
+        }
+
+        if (sfdcLeadFields.size() == 0) {
+            logger.info("No SFDC field value to send, will not send anything to Salesforce.");
+            if (foundExistingSfdcLeadIds.size() == 0) {
+                return null;
+            } else {
+                return foundExistingSfdcLeadIds.iterator().next();
+            }
+        }
+
+        if (existingSfdcLeadFields.size() == 0) {
+            // if we are creating a lead, let's make sure we have all the mandatory fields before sending the request
+            boolean missingMandatoryFields = false;
+            for (String leadMandatoryFieldName : sfdcLeadMandatoryFields) {
+                if (sfdcLeadFields.get(leadMandatoryFieldName) == null) {
+                    logger.warn("Missing mandatory field {}, aborting sending to Salesforce", leadMandatoryFieldName);
+                    missingMandatoryFields = true;
+                }
+            }
+            if (missingMandatoryFields) {
+                return null;
+            }
+        }
+
+        String baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/Lead";
+        HttpEntityEnclosingRequestBase request = new HttpPost(baseUrl);
+        if (foundExistingSfdcLeadIds.size() > 0) {
+            baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/sobjects/Lead/" + foundExistingSfdcLeadIds.iterator().next();
+            sfdcLeadFields.remove("Id");
+            request = new HttpPatch(baseUrl);
+        }
+
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            StringEntity requestEntity = new StringEntity(
+                    objectMapper.writeValueAsString(sfdcLeadFields),
+                    ContentType.APPLICATION_JSON);
+            request.setEntity(requestEntity);
+            Object responseObject = handleRequest(request);
+            if (responseObject == null) {
+                return null;
+            }
+            if (responseObject instanceof Map) {
+                Map<String, Object> responseData = (Map<String, Object>) responseObject;
+                if (responseData.get("id") != null) {
+                    String sfdcId = (String) responseData.get("id");
+                    logger.info("Lead successfully created/updated in Salesforce. sfdcId={}", sfdcId);
+                    return sfdcId;
+                }
+            }
+            logger.info("Response received from Salesforce: {}", responseObject);
+        } catch (IOException e) {
+            logger.error("Error creating or updating lead for profile {}", profile, e);
+        } catch (HttpException e) {
+            logger.error("Error creating or updating lead for profile {}", profile, e);
+        }
+
+        if (foundExistingSfdcLeadIds.size() == 0) {
+            return null;
+        } else {
+            return foundExistingSfdcLeadIds.iterator().next();
+        }
+    }
+
+    @Override
+    public boolean updateProfileFromLead(Profile profile) {
+        if (!isConnected()) {
+            return false;
+        }
+        String unomiIdentifierValue = (String) profile.getProperty(sfdcConfiguration.getUnomiIdentifierField());
+        Set<String> foundSfdcLeadIds = findLeadIdsByIdentifierValue(unomiIdentifierValue);
+        if (foundSfdcLeadIds.size() == 0) {
+            logger.info("No lead found in Salesforce corresponding to profile {}", profile);
+            // we didn't find a corresponding lead in salesforce.
+            return false;
+        } else if (foundSfdcLeadIds.size() > 1) {
+            logger.warn("Found multiple leads in Salesforce for identifier value {}, will use first one.", foundSfdcLeadIds);
+        } else {
+            logger.info("Found corresponding lead with identifier value {}", unomiIdentifierValue);
+        }
+        Map<String,Object> sfdcLead = getLead(foundSfdcLeadIds.iterator().next());
+        if (sfdcLead == null) {
+            logger.error("Error retrieving lead {} from Salesforce", foundSfdcLeadIds );
+            return false;
+        }
+        boolean profileUpdated = false;
+        for (Map.Entry<String,String> sfdcToUnomiFieldMappingEntry : sfdcConfiguration.getSfdcToUnomiFieldMappings().entrySet()) {
+            String sfdcFieldName = sfdcToUnomiFieldMappingEntry.getKey();
+            String unomiFieldName = sfdcToUnomiFieldMappingEntry.getValue();
+            if (sfdcLead.get(sfdcFieldName) != null) {
+                Object sfdcFieldValue = sfdcLead.get(sfdcFieldName);
+                if (sfdcFieldValue != null && !sfdcFieldValue.equals(profile.getProperty(unomiFieldName))) {
+                    profile.setProperty(unomiFieldName, sfdcFieldValue);
+                    profileUpdated = true;
+                }
+            }
+        }
+        logger.info("Updated profile {} from Salesforce lead {}", profile, sfdcLead);
+        return profileUpdated;
+    }
+
+    @Override
+    public Map<String,Object> query(String query) {
+        if (!isConnected()) {
+            return null;
+        }
+        // first we must check if an existing lead exists for the profile.
+
+        String baseUrl = null;
+        try {
+            baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/query?q=" + URLEncoder.encode(query, "UTF-8");
+            HttpGet get = new HttpGet(baseUrl);
+
+            Object responseObject = handleRequest(get);
+            if (responseObject == null) {
+                return null;
+            }
+            if (responseObject != null && responseObject instanceof Map) {
+                return (Map<String,Object>) responseObject;
+            }
+            return null;
+        } catch (UnsupportedEncodingException e) {
+            logger.error("Error executing query {}", query, e);
+            return null;
+        } catch (ClientProtocolException e) {
+            logger.error("Error executing query {}", query, e);
+            return null;
+        } catch (IOException e) {
+            logger.error("Error executing query {}", query, e);
+            return null;
+        } catch (HttpException e) {
+            logger.error("Error executing query {}", query, e);
+            return null;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getLimits() {
+        if (!isConnected()) {
+            return null;
+        }
+        String baseUrl = null;
+        try {
+            baseUrl = sfdcSession.getEndPoint() + REST_ENDPOINT_URI + "/limits";
+            HttpGet get = new HttpGet(baseUrl);
+
+            Object responseObject = handleRequest(get);
+            if (responseObject == null) {
+                return null;
+            }
+
+            if (responseObject instanceof Map) {
+                return (Map<String,Object>) responseObject;
+            }
+            return null;
+        } catch (UnsupportedEncodingException e) {
+            logger.error("Error retrieving Salesforce API Limits", e);
+            return null;
+        } catch (ClientProtocolException e) {
+            logger.error("Error retrieving Salesforce API Limits", e);
+            return null;
+        } catch (IOException e) {
+            logger.error("Error retrieving Salesforce API Limits", e);
+            return null;
+        } catch (HttpException e) {
+            logger.error("Error retrieving Salesforce API Limits", e);
+            return null;
+        }
+    }
+
+    private BayeuxClient makeClient() throws Exception {
+        HttpClient httpClient = new HttpClient();
+        httpClient.setConnectTimeout(CONNECTION_TIMEOUT);
+        httpClient.setTimeout(READ_TIMEOUT);
+        httpClient.start();
+
+        if (sfdcSession == null) {
+            logger.error("Invalid session !");
+            return null;
+        }
+        logger.info("Login successful!\nServer URL: " + sfdcSession.getEndPoint()
+                + "\nSession ID=" + sfdcSession.getSessionId());
+
+        Map<String, Object> options = new HashMap<String, Object>();
+        options.put(ClientTransport.TIMEOUT_OPTION, READ_TIMEOUT);
+        LongPollingTransport transport = new LongPollingTransport(
+                options, httpClient) {
+
+            @Override
+            protected void customize(ContentExchange exchange) {
+                super.customize(exchange);
+                exchange.addRequestHeader("Authorization", "OAuth " + sfdcSession.getSessionId());
+            }
+        };
+
+        BayeuxClient client = new BayeuxClient(getSalesforceStreamingEndpoint(
+                sfdcSession.getEndPoint()), transport);
+        return client;
+    }
+
+    public void setupPushListener(String channelName, ClientSessionChannel.MessageListener messageListener) throws Exception {
+        if (!isConnected()) {
+            return;
+        }
+        final BayeuxClient client = makeClient();
+        if (client == null) {
+            throw new Exception("Login failed !");
+        }
+        client.getChannel(Channel.META_HANDSHAKE).addListener
+                (new ClientSessionChannel.MessageListener() {
+                    @Override
+                    public void onMessage(ClientSessionChannel channel, Message message) {
+
+                        logger.debug("[CHANNEL:META_HANDSHAKE]: " + message);
+
+                        boolean success = message.isSuccessful();
+                        if (!success) {
+                            String error = (String) message.get("error");
+                            if (error != null) {
+                                logger.error("Error during HANDSHAKE: " + error);
+                            }
+
+                            Exception exception = (Exception) message.get("exception");
+                            if (exception != null) {
+                                logger.error("Exception during HANDSHAKE: ", exception);
+                            }
+                        }
+                    }
+
+                });
+
+        client.getChannel(Channel.META_CONNECT).addListener(
+                new ClientSessionChannel.MessageListener() {
+                    public void onMessage(ClientSessionChannel channel, Message message) {
+
+                        logger.debug("[CHANNEL:META_CONNECT]: " + message);
+
+                        boolean success = message.isSuccessful();
+                        if (!success) {
+                            String error = (String) message.get("error");
+                            if (error != null) {
+                                logger.error("Error during CONNECT: " + error);
+                            }
+                        }
+                    }
+
+                });
+
+        client.getChannel(Channel.META_SUBSCRIBE).addListener(
+                new ClientSessionChannel.MessageListener() {
+
+                    public void onMessage(ClientSessionChannel channel, Message message) {
+
+                        logger.debug("[CHANNEL:META_SUBSCRIBE]: " + message);
+                        boolean success = message.isSuccessful();
+                        if (!success) {
+                            String error = (String) message.get("error");
+                            if (error != null) {
+                                logger.error("Error during SUBSCRIBE: " + error);
+                            }
+                        }
+                    }
+                });
+
+        client.handshake();
+        logger.debug("Waiting for handshake");
+
+        boolean handshaken = client.waitFor(10 * 1000, BayeuxClient.State.CONNECTED);
+        if (!handshaken) {
+            logger.error("Failed to handshake: " + client);
+        }
+
+        logger.info("Subscribing for channel: " + channelName);
+
+        client.getChannel(channelName).subscribe(messageListener);
+
+    }
+
+    private String getSalesforceStreamingEndpoint(String endpoint) throws MalformedURLException {
+        return new URL(endpoint + STREAMING_ENDPOINT_URI).toExternalForm();
+    }
+
+    private void setupPushTopics(String host, String sessionId) throws HttpException, IOException {
+
+        String baseUrl = host + REST_ENDPOINT_URI + "/query?q=" + URLEncoder.encode("SELECT Id from PushTopic WHERE name = 'LeadUpdates'", "UTF-8");
+        HttpGet get = new HttpGet(baseUrl);
+
+        Map<String, String> queryResponse = (Map<String, String>) handleRequest(get);
+
+        if (queryResponse != null && queryResponse.containsKey("count")) {
+            logger.info("Push topics setup successfully");
+        }
+    }
+
+    public boolean login(SFDCConfiguration sfdcConfiguration)
+            throws HttpException, IOException {
+        String baseUrl = sfdcConfiguration.getSfdcLoginEndpoint() + "/services/oauth2/token";
+        HttpPost oauthPost = new HttpPost(baseUrl);
+        List<BasicNameValuePair> parametersBody = new ArrayList<>();
+        parametersBody.add(new BasicNameValuePair("grant_type", "password"));
+        parametersBody.add(new BasicNameValuePair("username", sfdcConfiguration.getSfdcUserUsername()));
+        parametersBody.add(new BasicNameValuePair("password", sfdcConfiguration.getSfdcUserPassword() + sfdcConfiguration.getSfdcUserSecurityToken()));
+        parametersBody.add(new BasicNameValuePair("client_id", sfdcConfiguration.getSfdcConsumerKey()));
+        parametersBody.add(new BasicNameValuePair("client_secret", sfdcConfiguration.getSfdcConsumerSecret()));
+        oauthPost.setEntity(new UrlEncodedFormEntity(parametersBody, "UTF-8"));
+
+        Map<String, String> oauthLoginResponse = (Map<String,String>) handleRequest(oauthPost, 0, false);
+        if (oauthLoginResponse == null) {
+            return false;
+        }
+
+        sfdcSession = new SFDCSession(
+                oauthLoginResponse.get("access_token"),
+                oauthLoginResponse.get("instance_url"),
+                oauthLoginResponse.get("signature"),
+                oauthLoginResponse.get("id"),
+                oauthLoginResponse.get("token_type"),
+                oauthLoginResponse.get("issued_at"),
+                sfdcConfiguration.getSfdcSessionTimeout());
+        return true;
+    }
+
+    public void logout() {
+        sfdcSession = null;
+    }
+
+    private SFDCSession getValidSession() {
+        if (isSessionValid()) {
+            return sfdcSession;
+        }
+        boolean loginSuccessful = false;
+        try {
+            loginSuccessful = login(sfdcConfiguration);
+            if (loginSuccessful && sfdcSession != null) {
+                return sfdcSession;
+            }
+        } catch (HttpException e) {
+            logger.error("Error logging in", e);
+            return null;
+        } catch (IOException e) {
+            logger.error("Error logging in", e);
+            return null;
+        }
+        return null;
+    }
+
+    private boolean isSessionValid() {
+        if (sfdcSession == null) {
+            return false;
+        }
+        if (sfdcSession.isExpired()) {
+            return false;
+        }
+        return true;
+    }
+
+    private Object handleRequest(HttpUriRequest request) throws IOException, HttpException {
+        return handleRequest(request, 1, true);
+    }
+
+    private Object handleRequest(HttpUriRequest request, int retryCount, boolean addAuthorizationHeader) throws IOException, HttpException {
+        CloseableHttpClient client = HttpClientBuilder.create().build();
+        if (addAuthorizationHeader) {
+            SFDCSession sfdcSession = getValidSession();
+            if (sfdcSession == null) {
+                logger.error("Couldn't get a valid session !");
+                return null;
+            }
+            if (request.containsHeader("Authorization")) {
+                logger.debug("Replacing existing authorization header with an updated one.");
+                Header[] authorizationHeaders = request.getHeaders("Authorization");
+                for (Header authorizationHeader : authorizationHeaders) {
+                    request.removeHeader(authorizationHeader);
+                }
+            }
+            request.addHeader("Authorization", "Bearer " + sfdcSession.getSessionId());
+        }
+
+        CloseableHttpResponse response = client.execute(request);
+        if (response.getStatusLine().getStatusCode() >= 400) {
+            if ((response.getStatusLine().getStatusCode() == 401 || response.getStatusLine().getStatusCode() == 403) && retryCount > 0) {
+                // probably the session has expired, let's try to login again
+                logger.warn("Unauthorized request, attempting to login again...");
+                boolean loginSuccessful = login(sfdcConfiguration);
+                if (!loginSuccessful) {
+                    logger.error("Login failed, cannot execute request {}", request);
+                    return null;
+                }
+                logger.warn("Retrying request {} once again...", request);
+                return handleRequest(request, 0, true);
+            } else {
+                logger.error("Error executing request {}: {}-{}", request, response.getStatusLine().getStatusCode(), response.getStatusLine().getStatusCode());
+                if (response.getEntity() != null) {
+                    logger.error("Entity={}", EntityUtils.toString(response.getEntity()));
+                }
+            }
+            return null;
+        }
+        if (response.getEntity() == null) {
+            return null;
+        }
+        return JSON.parse(EntityUtils.toString(response.getEntity()));
+    }
+
+    public boolean isConfigured() {
+        if (!sfdcConfiguration.isComplete()) {
+            logger.warn("Connection to Salesforce is not properly configured !");
+            return false;
+        }
+        return true;
+    }
+
+    public boolean isConnected() {
+        if (!isConfigured()) {
+            return false;
+        }
+        if (sfdcSession == null) {
+            logger.warn("Not connected to SalesForce, operation will not execute.");
+            return false;
+        } else {
+            if (sfdcSession.isExpired()) {
+                logger.warn("Connection to Salesforce has expired, will reconnect on next request");
+                return true;
+            }
+        }
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/salesforce-connector/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 0000000..1b0b0ce
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0"
+           xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
+
+    <cm:property-placeholder persistent-id="org.apache.unomi.sfdc"
+                             update-strategy="reload">
+        <cm:default-properties>
+            <cm:property name="sfdc.login.endpoint" value="https://login.salesforce.com"/>
+            <cm:property name="sfdc.user.username" value=""/>
+            <cm:property name="sfdc.user.password" value=""/>
+            <cm:property name="sfdc.user.securityToken" value=""/>
+            <cm:property name="sfdc.consumer.key" value=""/>
+            <cm:property name="sfdc.consumer.secret" value=""/>
+            <cm:property name="sfdc.channel" value="/topic/LeadUpdates"/>
+            <cm:property name="sfdc.fields.mappings.identifier" value=""/>
+            <cm:property name="sfdc.fields.mappings" value=""/>
+            <cm:property name="sfdc.session.timeout" value="900000"/>
+        </cm:default-properties>
+    </cm:property-placeholder>
+
+    <reference id="persistenceService"
+               interface="org.apache.unomi.persistence.spi.PersistenceService"/>
+
+    <bean id="defaultSFDCConfiguration" class="org.apache.unomi.sfdc.services.SFDCConfiguration">
+        <property name="itemId" value="sfdcConfiguration"/>
+        <property name="sfdcLoginEndpoint" value="${sfdc.login.endpoint}"/>
+        <property name="sfdcUserUsername" value="${sfdc.user.username}"/>
+        <property name="sfdcUserPassword" value="${sfdc.user.password}"/>
+        <property name="sfdcUserSecurityToken" value="${sfdc.user.securityToken}"/>
+        <property name="sfdcConsumerKey" value="${sfdc.consumer.key}"/>
+        <property name="sfdcConsumerSecret" value="${sfdc.consumer.secret}"/>
+        <property name="sfdcChannel" value="${sfdc.channel}"/>
+        <property name="sfdcFieldMappings" value="${sfdc.fields.mappings}"/>
+        <property name="sfdcFieldMappingsIdentifier" value="${sfdc.fields.mappings.identifier}"/>
+        <property name="sfdcSessionTimeout" value="${sfdc.session.timeout}" />
+    </bean>
+
+    <bean id="sfdcServiceImpl" class="org.apache.unomi.sfdc.services.internal.SFDCServiceImpl" init-method="start"
+          destroy-method="stop">
+        <property name="defaultSFDCConfiguration" ref="defaultSFDCConfiguration" />
+        <property name="persistenceService" ref="persistenceService" />
+    </bean>
+
+    <service id="sfdcService" ref="sfdcServiceImpl" auto-export="interfaces"/>
+
+</blueprint>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/resources/org.apache.unomi.sfdc.cfg
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/resources/org.apache.unomi.sfdc.cfg b/extensions/salesforce-connector/services/src/main/resources/org.apache.unomi.sfdc.cfg
new file mode 100644
index 0000000..939bddb
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/resources/org.apache.unomi.sfdc.cfg
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+sfdc.login.endpoint=https://login.salesforce.com
+sfdc.user.username=
+sfdc.user.password=
+sfdc.user.securityToken=
+sfdc.consumer.key=
+sfdc.consumer.secret=
+sfdc.channel=/topic/LeadUpdates
+sfdc.fields.mappings=email=Email,firstName=FirstName,lastName=LastName,company=Company,phoneNumber=Phone,jobTitle=Title,city=City,zipCode=PostalCode,address=Street,sfdcStatus=Status,sfdcRating=Rating
+sfdc.fields.mappings.identifier=email=Email
+sfdc.session.timeout=900000
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/test/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImplTest.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/test/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImplTest.java b/extensions/salesforce-connector/services/src/test/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImplTest.java
new file mode 100644
index 0000000..2a566a4
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/test/java/org/apache/unomi/sfdc/services/internal/SFDCServiceImplTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services.internal;
+
+import org.apache.http.HttpException;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.sfdc.services.SFDCConfiguration;
+import org.cometd.bayeux.Message;
+import org.cometd.bayeux.client.ClientSessionChannel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+/**
+ * A unit test class for testing the Salesforce Service implementation
+ */
+public class SFDCServiceImplTest {
+
+    private static SFDCServiceImpl sfdcServiceImpl;
+    private static boolean canRunTests = false;
+    private static SFDCConfiguration sfdcConfiguration;
+
+    @BeforeClass
+    public static void setUp() throws IOException {
+        sfdcServiceImpl = new SFDCServiceImpl();
+        // we must now configure it.
+        InputStream configPropertiesStream = SFDCServiceImplTest.class.getClassLoader().getResourceAsStream("org.apache.unomi.sfdc.cfg");
+        Properties properties = new Properties();
+        properties.load(configPropertiesStream);
+        sfdcConfiguration = new SFDCConfiguration();
+        sfdcConfiguration.setSfdcLoginEndpoint(properties.getProperty("sfdc.login.endpoint"));
+        sfdcConfiguration.setSfdcUserUsername(properties.getProperty("sfdc.user.username"));
+        sfdcConfiguration.setSfdcUserPassword(properties.getProperty("sfdc.user.password"));
+        sfdcConfiguration.setSfdcUserSecurityToken(properties.getProperty("sfdc.user.securityToken"));
+        sfdcConfiguration.setSfdcConsumerKey(properties.getProperty("sfdc.consumer.key"));
+        sfdcConfiguration.setSfdcConsumerSecret(properties.getProperty("sfdc.consumer.secret"));
+        sfdcConfiguration.setSfdcChannel(properties.getProperty("sfdc.channel"));
+        sfdcConfiguration.setSfdcFieldMappings(properties.getProperty("sfdc.fields.mappings"));
+        sfdcConfiguration.setSfdcFieldMappingsIdentifier(properties.getProperty("sfdc.fields.mappings.identifier"));
+        if (System.getProperty("sfdcProperties") != null) {
+            Properties testProperties = new Properties();
+            InputStream testPropertiesInputStream = new FileInputStream(System.getProperty("sfdcProperties"));
+            testProperties.load(testPropertiesInputStream);
+            sfdcConfiguration.setSfdcLoginEndpoint(testProperties.getProperty("sfdc.login.endpoint"));
+            sfdcConfiguration.setSfdcUserUsername(testProperties.getProperty("sfdc.user.username"));
+            sfdcConfiguration.setSfdcUserPassword(testProperties.getProperty("sfdc.user.password"));
+            sfdcConfiguration.setSfdcUserSecurityToken(testProperties.getProperty("sfdc.user.securityToken"));
+            sfdcConfiguration.setSfdcConsumerKey(testProperties.getProperty("sfdc.consumer.key"));
+            sfdcConfiguration.setSfdcConsumerSecret(testProperties.getProperty("sfdc.consumer.secret"));
+            canRunTests = true;
+            sfdcServiceImpl.setDefaultSFDCConfiguration(sfdcConfiguration);
+            sfdcServiceImpl.start();
+        } else {
+            System.out.println("CANNOT RUN TESTS, PLEASE PROVIDE A PROPERTIES FILE WITH SALESFORCE CREDENTIALS AND REFERENCING IT USING -DsfdcProperties=FILEPATH !!!!!!");
+        }
+    }
+
+    @AfterClass
+    public static void shutdown() {
+        if (canRunTests) {
+            sfdcServiceImpl.stop();
+            sfdcServiceImpl = null;
+        }
+    }
+
+    private boolean checkCanRunTests() {
+        if (!canRunTests) {
+            System.out.println("CANNOT RUN TESTS, PLEASE PROVIDE A PROPERTIES FILE WITH SALESFORCE CREDENTIALS AND REFERENCING IT USING -DsfdcProperties=FILEPATH !!!!!!");
+        }
+        return canRunTests;
+    }
+
+    @Test
+    public void testGetLeads() {
+        if (!checkCanRunTests()) return;
+        Set<String> recentLeadIds = sfdcServiceImpl.getRecentLeadIds();
+        if (recentLeadIds == null || recentLeadIds.size() == 0) {
+            return;
+        }
+        for (String recentLeadId : recentLeadIds) {
+            Map<String,Object> leadFields = sfdcServiceImpl.getLead(recentLeadId);
+            if (leadFields.containsKey(sfdcConfiguration.getSfdcIdentifierField())) {
+                String leadIdentifierFieldValue = (String) leadFields.get(sfdcConfiguration.getSfdcIdentifierField());
+                if (leadIdentifierFieldValue == null) {
+                    System.out.println("Skipping lead with null identifier field value for field: " + sfdcConfiguration.getSfdcIdentifierField());
+                    continue;
+                }
+                Set<String> foundLeadIds = sfdcServiceImpl.findLeadIdsByIdentifierValue(leadIdentifierFieldValue);
+                assertTrue("Should find a single lead for identifier value " + leadIdentifierFieldValue, foundLeadIds.size() == 1);
+                assertEquals("Expected Id to be the same", foundLeadIds.iterator().next(), leadFields.get("Id"));
+            }
+        }
+    }
+
+    @Test
+    public void testGetLimits() {
+        if (!checkCanRunTests()) return;
+        Map<String,Object> limits = sfdcServiceImpl.getLimits();
+        assertNotNull("Limits object is null, an error occurred !", limits);
+    }
+
+    @Test
+    public void testCreateOrUpdateAndSyncLead() {
+        if (!checkCanRunTests()) return;
+        Profile profile = new Profile();
+        profile.setItemId(UUID.randomUUID().toString());
+        profile.setProperty("email", "test2@jahia.com");
+        profile.setProperty("firstName", "Serge");
+        String leadId = sfdcServiceImpl.createOrUpdateLead(profile);
+        assertNull("The lead creation should fail since we are missing mandatory fields.", leadId);
+        profile.setProperty("lastName", "Huber");
+        profile.setProperty("company", "Jahia Solutions Group");
+        profile.setProperty("phoneNumber", "+41223613424");
+        profile.setProperty("jobTitle", "CTO");
+        leadId = sfdcServiceImpl.createOrUpdateLead(profile);
+        // now let's try to update it.
+        profile.setProperty("company", "Jahia Solutions Group SA");
+        sfdcServiceImpl.createOrUpdateLead(profile);
+        boolean profileUpdated = sfdcServiceImpl.updateProfileFromLead(profile);
+        assertTrue("Profile should have been updated since we are reading status field", profileUpdated);
+        profile.setProperty("company", "Another value");
+        profileUpdated = sfdcServiceImpl.updateProfileFromLead(profile);
+        assertTrue("Profile should have been updated since data is not equal", profileUpdated);
+        if (leadId != null) {
+            sfdcServiceImpl.deleteLead(leadId);
+        }
+    }
+
+    @Test
+    public void testStreaming() throws Exception {
+        if (!checkCanRunTests()) return;
+        System.out.println("Running streaming client example....");
+
+        sfdcServiceImpl.setupPushListener(sfdcConfiguration.getSfdcChannel(), new ClientSessionChannel.MessageListener() {
+            @Override
+            public void onMessage(ClientSessionChannel clientSessionChannel, Message message) {
+                System.out.println("Received message for channel" + sfdcConfiguration.getSfdcChannel() + ":"+ message);
+            }
+        });
+
+        System.out.println("Waiting 10 seconds for streamed data from your organization ...");
+        int i=0;
+        while (i < 10) {
+            Thread.sleep(1000);
+            i++;
+        }
+
+    }
+
+    @Test
+    public void testFailedLogin() throws IOException, HttpException {
+        if (!checkCanRunTests()) return;
+        InputStream configPropertiesStream = SFDCServiceImplTest.class.getClassLoader().getResourceAsStream("org.apache.unomi.sfdc.cfg");
+        Properties properties = new Properties();
+        properties.load(configPropertiesStream);
+        String loginEndpoint = properties.getProperty("sfdc.login.endpoint");
+        Properties testProperties = new Properties();
+        if (System.getProperty("sfdcProperties") != null) {
+            sfdcServiceImpl.logout();
+            InputStream testPropertiesInputStream = new FileInputStream(System.getProperty("sfdcProperties"));
+            testProperties.load(testPropertiesInputStream);
+            if (testProperties.getProperty("sfdc.login.endpoint") != null) {
+                loginEndpoint = testProperties.getProperty("sfdc.login.endpoint");
+            }
+            String userUserName = testProperties.getProperty("sfdc.user.username");
+            String userPassword = testProperties.getProperty("sfdc.user.password");
+            String userSecurityToken = testProperties.getProperty("sfdc.user.securityToken");
+            String consumerKey = testProperties.getProperty("sfdc.consumer.key");
+            String consumerSecret = testProperties.getProperty("sfdc.consumer.secret");
+            SFDCConfiguration sfdcConfiguration = new SFDCConfiguration();
+            sfdcConfiguration.setSfdcLoginEndpoint(loginEndpoint);
+            sfdcConfiguration.setSfdcUserUsername(userUserName);
+            sfdcConfiguration.setSfdcUserPassword(userPassword + "wrongpassword");
+            sfdcConfiguration.setSfdcUserSecurityToken(userSecurityToken);
+            sfdcConfiguration.setSfdcConsumerKey(consumerKey);
+            sfdcConfiguration.setSfdcConsumerSecret(consumerSecret);
+            boolean loginSuccessful = sfdcServiceImpl.login(sfdcConfiguration);
+            assertNull("Session should not be valid since we used a wrong password !", sfdcServiceImpl.getSFDCSession());
+
+            // we login properly for other tests to execute properly.
+            sfdcConfiguration.setSfdcUserPassword(userPassword);
+            loginSuccessful = sfdcServiceImpl.login(sfdcConfiguration);
+            assertTrue("Login with proper credentials should have worked !", loginSuccessful);
+        }
+    }
+}


[2/2] incubator-unomi git commit: UNOMI-116 Salesforce CRM Connector - Initial commit

Posted by sh...@apache.org.
UNOMI-116 Salesforce CRM Connector
- Initial commit

Signed-off-by: Serge Huber <sh...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/incubator-unomi/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-unomi/commit/a897ab84
Tree: http://git-wip-us.apache.org/repos/asf/incubator-unomi/tree/a897ab84
Diff: http://git-wip-us.apache.org/repos/asf/incubator-unomi/diff/a897ab84

Branch: refs/heads/master
Commit: a897ab8437966975a45082cb042c500540c8c0f8
Parents: 139e1b7
Author: Serge Huber <sh...@apache.org>
Authored: Wed Jul 19 14:28:54 2017 +0200
Committer: Serge Huber <sh...@apache.org>
Committed: Wed Jul 19 14:28:54 2017 +0200

----------------------------------------------------------------------
 .gitignore                                      |   1 +
 extensions/pom.xml                              |   1 +
 extensions/salesforce-connector/README.md       | 148 ++++
 extensions/salesforce-connector/actions/pom.xml |  49 ++
 .../sfdc/actions/CreateOrUpdateLeadAction.java  |  42 +
 .../actions/UpdateProfileFromLeadAction.java    |  43 +
 .../cxs/actions/sfdcCreateOrUpdateLead.json     |  15 +
 .../cxs/actions/sfdcUpdateProfileFromLead.json  |  15 +
 .../resources/OSGI-INF/blueprint/blueprint.xml  |  44 +
 .../salesforce-connector/compileWithTests.sh    |  20 +
 .../salesforce-connector/karaf-kar/pom.xml      |  93 +++
 .../karaf-kar/src/main/feature/feature.xml      |  28 +
 extensions/salesforce-connector/pom.xml         |  39 +
 extensions/salesforce-connector/rest/pom.xml    | 132 +++
 .../apache/unomi/sfdc/rest/SFDCEndPoint.java    |  81 ++
 .../resources/OSGI-INF/blueprint/blueprint.xml  |  67 ++
 .../salesforce-connector/services/pom.xml       | 164 ++++
 .../unomi/sfdc/services/FieldMapping.java       |  35 +
 .../apache/unomi/sfdc/services/MappedField.java |  30 +
 .../unomi/sfdc/services/SFDCConfiguration.java  | 178 ++++
 .../apache/unomi/sfdc/services/SFDCService.java |  81 ++
 .../apache/unomi/sfdc/services/SFDCSession.java |  71 ++
 .../sfdc/services/internal/SFDCServiceImpl.java | 815 +++++++++++++++++++
 .../resources/OSGI-INF/blueprint/blueprint.xml  |  65 ++
 .../main/resources/org.apache.unomi.sfdc.cfg    |  26 +
 .../services/internal/SFDCServiceImplTest.java  | 211 +++++
 26 files changed, 2494 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index bf90830..c3e5009 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ maven-metadata-local.xml
 GeoLite2-City.mmdb
 allCountries.zip
 rest/.miredot-offline.json
+/extensions/salesforce-connector/test.properties

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/pom.xml b/extensions/pom.xml
index dea4bc2..07608b3 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -35,6 +35,7 @@
         <module>privacy-extension</module>
         <module>geonames</module>
         <module>router</module>
+        <module>salesforce-connector</module>
     </modules>
 
 </project>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/README.md
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/README.md b/extensions/salesforce-connector/README.md
new file mode 100644
index 0000000..4bd7451
--- /dev/null
+++ b/extensions/salesforce-connector/README.md
@@ -0,0 +1,148 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+Unomi Salesforce Connector
+==========================
+
+## Getting started
+
+1. Create a new developer account here: 
+
+        https://developer.salesforce.com/signup
+        
+2. Create a new Connected App, by going into Setup -> App Manager and click "Create Connected App"
+ 
+3. In the settings, make sure you do the following:
+
+        Enable OAuth settings -> Activated
+        Enable for device flow -> Activated (no need for a callback URL)
+        Add all the selected OAuth scopes you want (or put all of them)
+        Make sure Require Secret for Web Server flow is activated
+        
+4. Make sure you retrieve the following information once you have created the app in the API (Enable OAuth Settings):
+
+        Consumer key
+        Consumer secret (click to see it)
+        
+5. You must also retrieve your user's security token, or create it if you don't have one already. To do this simply 
+click on your user at the top right, select "Settings", the click on "Reset my security token". You will receive an email
+with the security token.
+
+6. You are now ready to configure the Apache Unomi Salesforce Connector. In the etc/org.apache.unomi.sfdc.cfg file 
+change the following settings:
+
+        sfdc.user.username=YOUR_USER_NAME
+        sfdc.user.password=YOUR_PASSWORD
+        sfdc.user.securityToken=YOUR_USER_SECURITY_TOKEN
+        sfdc.consumer.key=CONNECTED_APP_CONSUMER_KEY
+        sfdc.consumer.secret=CONNECTED_APP_SECRET
+        
+7. Connected to the Apache Unomi Karaf Shell using : 
+
+        ssh -p 8102 karaf@localhost (default password is karaf)
+           
+7. Deploy into Apache Unomi using the following commands from the Apache Karaf shell:
+
+        feature:repo-add mvn:org.apache.unomi/unomi-salesforce-connector-karaf-kar/1.2.0-incubating-SNAPSHOT/xml/features
+        feature:install unomi-salesforce-connector-karaf-kar
+        
+8. You can then test the connection to Salesforce by accessing the following URLs:
+
+        https://localhost:9443/cxs/sfdc/version
+        https://localhost:9443/cxs/sfdc/limits
+        
+    The first URL will give you information about the version of the connector, so this makes it easy to check that the
+    plugin is properly deployed, started and the correct version. The second URL will actually make a request to the
+    Salesforce REST API to retrieve the limits of the Salesforce API.
+    
+    Both URLs are password protected by the Apache Unomi (Karaf) password. You can find this user and password information
+    in the etc/users.properties file.
+    
+## Upgrading the Salesforce connector
+
+If you followed all the steps in the Getting Started section, you can upgrade the Salesforce connector by using the following steps:
+
+1. Compile the connector using:
+
+        mvn clean install
+        
+2. Login to the Unomi Karaf Shell using:
+
+        ssh -p 8102 karaf@localhost (password by default is karaf)
+        
+3. Execute the following commands in the Karaf shell
+
+        feature:repo-refresh
+        feature:uninstall unomi-salesforce-connector-karaf-feature
+        feature:install unomi-salesforce-connector-karaf-feature
+        
+4. You can then check that the new version is properly deployed by accessing the following URL and checking the build date:
+
+        https://localhost:9443/cxs/sfdc/version
+        
+    (if asked for a password it's the same karaf/karaf default)
+   
+## Using the Salesforce Workbench for testing REST API
+   
+The Salesforce Workbench contains a REST API Explorer that is very useful to test requests. You may find it here : 
+
+    https://workbench.developerforce.com/restExplorer.php
+    
+## Setting up Streaming Push queries
+
+Using the Salesforce Workbench, you can setting streaming push queries (Queries->Streaming push topics) such as the 
+following example:
+
+    Name: LeadUpdates
+    Query : SELECT Id,FirstName,LastName,Email,Company FROM Lead
+
+## Executing the unit tests
+
+Before running the tests, make sure you have completed all the steps above, including the streaming push queries setup.
+
+By default the unit tests will not run as they need proper Salesforce credentials to run. To set this up create a 
+properties file like the following one:
+
+test.properties
+
+    #
+    # Licensed to the Apache Software Foundation (ASF) under one or more
+    # contributor license agreements.  See the NOTICE file distributed with
+    # this work for additional information regarding copyright ownership.
+    # The ASF licenses this file to You 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.
+    #
+    sfdc.user.username=YOUR_USER_NAME
+    sfdc.user.password=YOUR_PASSWORD
+    sfdc.user.securityToken=YOUR_USER_SECURITY_TOKEN
+    sfdc.consumer.key=CONNECTED_APP_CONSUMER_KEY
+    sfdc.consumer.secret=CONNECTED_APP_SECRET
+        
+and then use the following command line to reference the file:
+
+    mvn clean install -DsfdcProperties=../test.properties
+    
+(in case you're wondering the ../ is because the test is located in the services sub-directory)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/pom.xml b/extensions/salesforce-connector/actions/pom.xml
new file mode 100644
index 0000000..f0b47b9
--- /dev/null
+++ b/extensions/salesforce-connector/actions/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-salesforce-connector</artifactId>
+        <version>1.2.0-incubating-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>unomi-salesforce-connector-actions</artifactId>
+    <name>Apache Unomi :: Extensions :: Salesforce connector :: Rule Actions</name>
+    <description>Rule Actions for the Apache Unomi Context Server extension that integrates with Salesforce</description>
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-salesforce-connector-services</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/CreateOrUpdateLeadAction.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/CreateOrUpdateLeadAction.java b/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/CreateOrUpdateLeadAction.java
new file mode 100644
index 0000000..e46ec29
--- /dev/null
+++ b/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/CreateOrUpdateLeadAction.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.actions;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.actions.ActionExecutor;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.sfdc.services.SFDCService;
+
+/**
+ * Creates or updates a Salesforce lead from the corresponding Apache Unomi profile (using a common identifier field,
+ * usually the email address)
+ */
+public class CreateOrUpdateLeadAction implements ActionExecutor {
+
+    private SFDCService sfdcService;
+
+    public void setSfdcService(SFDCService sfdcService) {
+        this.sfdcService = sfdcService;
+    }
+
+    @Override
+    public int execute(Action action, Event event) {
+        sfdcService.createOrUpdateLead(event.getProfile());
+        return EventService.NO_CHANGE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/UpdateProfileFromLeadAction.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/UpdateProfileFromLeadAction.java b/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/UpdateProfileFromLeadAction.java
new file mode 100644
index 0000000..bbbd5c1
--- /dev/null
+++ b/extensions/salesforce-connector/actions/src/main/java/org/apache/unomi/sfdc/actions/UpdateProfileFromLeadAction.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.actions;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.actions.ActionExecutor;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.sfdc.services.SFDCService;
+
+/**
+ * Update an Apache Unomi profile from a corresponding Salesforce Lead with the same common identifier (usually the
+ * email address)
+ */
+public class UpdateProfileFromLeadAction implements ActionExecutor {
+
+    private SFDCService sfdcService;
+
+    public void setSfdcService(SFDCService sfdcService) {
+        this.sfdcService = sfdcService;
+    }
+
+    @Override
+    public int execute(Action action, Event event) {
+        boolean profileUpdated = sfdcService.updateProfileFromLead(event.getProfile());
+        if (profileUpdated) return EventService.PROFILE_UPDATED;
+        return EventService.NO_CHANGE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcCreateOrUpdateLead.json
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcCreateOrUpdateLead.json b/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcCreateOrUpdateLead.json
new file mode 100644
index 0000000..55c7da3
--- /dev/null
+++ b/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcCreateOrUpdateLead.json
@@ -0,0 +1,15 @@
+{
+  "metadata": {
+    "id": "sfdcCreateOrUpdateLeadAction",
+    "name": "sfdcCreateOrUpdateLeadAction",
+    "description": "",
+    "tags": [
+      "demographic",
+      "hidden.availableToEndUser"
+    ],
+    "readOnly": true
+  },
+  "actionExecutor": "sfdcCreateOrUpdateLead",
+  "parameters": [
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcUpdateProfileFromLead.json
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcUpdateProfileFromLead.json b/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcUpdateProfileFromLead.json
new file mode 100644
index 0000000..598edb7
--- /dev/null
+++ b/extensions/salesforce-connector/actions/src/main/resources/META-INF/cxs/actions/sfdcUpdateProfileFromLead.json
@@ -0,0 +1,15 @@
+{
+  "metadata": {
+    "id": "sfdcUpdateProfileFromLeadAction",
+    "name": "sfdcUpdateProfileFromLeadAction",
+    "description": "",
+    "tags": [
+      "demographic",
+      "hidden.availableToEndUser"
+    ],
+    "readOnly": true
+  },
+  "actionExecutor": "sfdcUpdateProfileFromLead",
+  "parameters": [
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/actions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/actions/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/salesforce-connector/actions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 0000000..24b61ff
--- /dev/null
+++ b/extensions/salesforce-connector/actions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
+
+    <reference id="sfdcService"
+               interface="org.apache.unomi.sfdc.services.SFDCService"/>
+
+    <service auto-export="interfaces">
+        <service-properties>
+            <entry key="actionExecutorId" value="sfdcCreateOrUpdateLead"/>
+        </service-properties>
+        <bean class="org.apache.unomi.sfdc.actions.CreateOrUpdateLeadAction">
+            <property name="sfdcService" ref="sfdcService"/>
+        </bean>
+    </service>
+
+    <service auto-export="interfaces">
+        <service-properties>
+            <entry key="actionExecutorId" value="sfdcUpdateProfileFromLead"/>
+        </service-properties>
+        <bean class="org.apache.unomi.sfdc.actions.UpdateProfileFromLeadAction">
+            <property name="sfdcService" ref="sfdcService"/>
+        </bean>
+    </service>
+
+</blueprint>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/compileWithTests.sh
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/compileWithTests.sh b/extensions/salesforce-connector/compileWithTests.sh
new file mode 100755
index 0000000..5295f7a
--- /dev/null
+++ b/extensions/salesforce-connector/compileWithTests.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+################################################################################
+#
+#    Licensed to the Apache Software Foundation (ASF) under one or more
+#    contributor license agreements.  See the NOTICE file distributed with
+#    this work for additional information regarding copyright ownership.
+#    The ASF licenses this file to You 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.
+#
+################################################################################
+mvn clean install -DsfdcProperties=../test.properties
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/karaf-kar/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/karaf-kar/pom.xml b/extensions/salesforce-connector/karaf-kar/pom.xml
new file mode 100644
index 0000000..577f4bf
--- /dev/null
+++ b/extensions/salesforce-connector/karaf-kar/pom.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-salesforce-connector</artifactId>
+        <version>1.2.0-incubating-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>unomi-salesforce-connector-karaf-kar</artifactId>
+    <name>Apache Unomi :: Extensions :: Salesforce connector :: Apache Karaf Feature and KAR archive</name>
+    <description>Apache Karaf Feature and KAR archive for the Apache Unomi Context Server extension that integrates with Salesforce</description>
+    <packaging>kar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore-osgi</artifactId>
+            <version>4.4.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient-osgi</artifactId>
+            <version>4.5.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-salesforce-connector-services</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-salesforce-connector-rest</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-salesforce-connector-actions</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.karaf.tooling</groupId>
+                    <artifactId>karaf-maven-plugin</artifactId>
+                    <extensions>true</extensions>
+                    <configuration>
+                        <includeTransitiveDependency>false</includeTransitiveDependency>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-maven-plugin</artifactId>
+                <configuration>
+                    <startLevel>85</startLevel>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/karaf-kar/src/main/feature/feature.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/karaf-kar/src/main/feature/feature.xml b/extensions/salesforce-connector/karaf-kar/src/main/feature/feature.xml
new file mode 100644
index 0000000..252f34e
--- /dev/null
+++ b/extensions/salesforce-connector/karaf-kar/src/main/feature/feature.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.1" name="unomi-salesforce-connector-karaf-feature">
+    <feature name="unomi-salesforce-connector-karaf-kar" version="1.0-SNAPSHOT" description="Apache Unomi :: Extensions :: Salesforce connector :: Apache Karaf Feature">
+        <details>Apache Karaf feature for the Apache Unomi Context Server extension that integrates with Salesforce</details>
+        <configfile finalname="/etc/org.apache.unomi.sfdc.cfg">mvn:org.apache.unomi/unomi-salesforce-connector-services/1.0-SNAPSHOT/cfg/sfdccfg</configfile>
+        <bundle start-level="85">mvn:org.apache.httpcomponents/httpcore-osgi/4.4.6</bundle>
+        <bundle start-level="85">mvn:org.apache.httpcomponents/httpclient-osgi/4.5.1</bundle>
+        <bundle start-level="85">mvn:org.apache.unomi/unomi-salesforce-connector-services/1.0-SNAPSHOT</bundle>
+        <bundle start-level="85">mvn:org.apache.unomi/unomi-salesforce-connector-rest/1.0-SNAPSHOT</bundle>
+        <bundle start-level="85">mvn:org.apache.unomi/unomi-salesforce-connector-actions/1.0-SNAPSHOT</bundle>
+    </feature>
+</features>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/pom.xml b/extensions/salesforce-connector/pom.xml
new file mode 100644
index 0000000..da00cf9
--- /dev/null
+++ b/extensions/salesforce-connector/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-extensions</artifactId>
+        <version>1.2.0-incubating-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>unomi-salesforce-connector</artifactId>
+    <name>Apache Unomi :: Extensions :: Salesforce connector</name>
+    <description>Apache Unomi Context Server extension that integrates with Salesforce</description>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>services</module>
+        <module>rest</module>
+        <module>actions</module>
+        <module>karaf-kar</module>
+    </modules>
+
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/rest/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/rest/pom.xml b/extensions/salesforce-connector/rest/pom.xml
new file mode 100644
index 0000000..2420cc5
--- /dev/null
+++ b/extensions/salesforce-connector/rest/pom.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-salesforce-connector</artifactId>
+        <version>1.2.0-incubating-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>unomi-salesforce-connector-rest</artifactId>
+    <name>Apache Unomi :: Extensions :: Salesforce connector :: REST API</name>
+    <description>REST API for the Apache Unomi Context Server extension that integrates with Salesforce</description>
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-salesforce-connector-services</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!--
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-core</artifactId>
+            <version>${cxf.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        -->
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxws</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-transports-http</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-rs-security-cors</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.jaxrs</groupId>
+            <artifactId>jackson-jaxrs-json-provider</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-spi</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>buildnumber-maven-plugin</artifactId>
+                <version>1.4</version>
+                <configuration>
+                    <format>{0}-{1,date,short}-{1,time,short}</format>
+                    <items>
+                        <item>scmVersion</item>
+                        <item>timestamp</item>
+                    </items>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>create</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Implementation-Build>${buildNumber}</Implementation-Build>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/rest/src/main/java/org/apache/unomi/sfdc/rest/SFDCEndPoint.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/rest/src/main/java/org/apache/unomi/sfdc/rest/SFDCEndPoint.java b/extensions/salesforce-connector/rest/src/main/java/org/apache/unomi/sfdc/rest/SFDCEndPoint.java
new file mode 100644
index 0000000..c7a5c3f
--- /dev/null
+++ b/extensions/salesforce-connector/rest/src/main/java/org/apache/unomi/sfdc/rest/SFDCEndPoint.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.rest;
+
+import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
+import org.apache.unomi.sfdc.services.SFDCService;
+import org.osgi.framework.BundleContext;
+
+import javax.jws.WebMethod;
+import javax.jws.WebService;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+@WebService
+@Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
+@CrossOriginResourceSharing(
+        allowAllOrigins = true,
+        allowCredentials = true
+)
+public class SFDCEndPoint {
+
+    private SFDCService sfdcService;
+    private BundleContext bundleContext;
+
+    public SFDCEndPoint() {
+        System.out.println("Initializing SFDC service endpoint...");
+    }
+
+    @WebMethod(exclude = true)
+    public void setSFDCService(SFDCService sfdcService) {
+        this.sfdcService = sfdcService;
+    }
+
+    public void setBundleContext(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    @GET
+    @Path("/version")
+    public Map<String,String> getVersion() {
+        Map<String,String> versionInfo = new HashMap<>();
+        Dictionary<String,String> bundleHeaders = bundleContext.getBundle().getHeaders();
+        Enumeration<String> bundleHeaderKeyEnum = bundleHeaders.keys();
+        while (bundleHeaderKeyEnum.hasMoreElements()) {
+            String bundleHeaderKey = bundleHeaderKeyEnum.nextElement();
+            versionInfo.put(bundleHeaderKey, bundleHeaders.get((bundleHeaderKey)));
+        }
+        return versionInfo;
+    }
+
+    @GET
+    @Path("/limits")
+    public Map<String,Object> getLimits() {
+        return sfdcService.getLimits();
+    }
+
+    public void setSfdcService(SFDCService sfdcService) {
+        this.sfdcService = sfdcService;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/salesforce-connector/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 0000000..30c8a81
--- /dev/null
+++ b/extensions/salesforce-connector/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xmlns:jaxrs="http://cxf.apache.org/blueprint/jaxrs"
+           xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd
+  http://cxf.apache.org/blueprint/jaxrs http://cxf.apache.org/schemas/blueprint/jaxrs.xsd">
+
+    <bean id="cors-filter" class="org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter"/>
+    <bean id="jacksonMapper" class="org.apache.unomi.persistence.spi.CustomObjectMapper"/>
+    <bean id="jaxb-provider" class="com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider">
+        <argument index="0" ref="jacksonMapper" type="com.fasterxml.jackson.databind.ObjectMapper "/>
+        <argument index="1" type="com.fasterxml.jackson.jaxrs.cfg.Annotations[]">
+            <array>
+                <value>JACKSON</value>
+                <value>JAXB</value>
+            </array>
+        </argument>
+    </bean>
+    <bean id="jaas-filter" class="org.apache.cxf.jaxrs.security.JAASAuthenticationFilter">
+        <!-- Name of the JAAS Context -->
+        <property name="contextName" value="karaf"/>
+        <!-- Hint to the filter on how to have Principals representing users and roles separated
+             while initializing a SecurityContext -->
+        <property name="rolePrefix" value="ROLE_"/>
+
+        <property name="realmName" value="cxs"/>
+        <!-- Activate this if you want to force a redirect if auth is missing, by default it will trigger a 403 which
+             is usually preferred -->
+        <!--property name="redirectURI" value="/login.jsp"/-->
+    </bean>
+
+    <jaxrs:server address="/sfdc" id="restSFDCService">
+        <jaxrs:providers>
+            <ref component-id="jaxb-provider"/>
+            <ref component-id="cors-filter"/>
+            <ref component-id="jaas-filter"/>
+        </jaxrs:providers>
+
+        <jaxrs:serviceBeans>
+            <ref component-id="sfdcServiceEndPoint"/>
+        </jaxrs:serviceBeans>
+    </jaxrs:server>
+
+    <reference id="sfdcService" interface="org.apache.unomi.sfdc.services.SFDCService"/>
+
+    <bean id="sfdcServiceEndPoint" class="org.apache.unomi.sfdc.rest.SFDCEndPoint">
+        <property name="sfdcService" ref="sfdcService"/>
+        <property name="bundleContext" ref="blueprintBundleContext" />
+    </bean>
+</blueprint>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/pom.xml
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/pom.xml b/extensions/salesforce-connector/services/pom.xml
new file mode 100644
index 0000000..6508f58
--- /dev/null
+++ b/extensions/salesforce-connector/services/pom.xml
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-salesforce-connector</artifactId>
+        <version>1.2.0-incubating-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>unomi-salesforce-connector-services</artifactId>
+    <name>Apache Unomi :: Extensions :: Salesforce connector :: Service</name>
+    <description>Service implementation for the Apache Unomi Context Server extension that integrates with Salesforce</description>
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-spi</artifactId>
+            <version>1.2.0-incubating-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-rs-security-cors</artifactId>
+            <version>${cxf.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient-osgi</artifactId>
+            <version>4.5.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-client</artifactId>
+            <version>7.4.4.v20110707</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-http</artifactId>
+            <version>7.4.4.v20110707</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-io</artifactId>
+            <version>7.4.4.v20110707</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-util</artifactId>
+            <version>7.4.4.v20110707</version>
+        </dependency>
+        <dependency>
+            <groupId>org.cometd.java</groupId>
+            <artifactId>bayeux-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.cometd.java</groupId>
+            <artifactId>cometd-java-common</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.cometd.java</groupId>
+            <artifactId>cometd-java-client</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Unit tests -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.6.6</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
+                        <Import-Package>
+                            sun.misc;resolution:=optional,
+                            net.sf.ehcache;resolution:=optional,
+                            net.spy.memcached;resolution:=optional,
+                            org.ietf.jgss;resolution:=optional,
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>attach-artifacts</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>attach-artifact</goal>
+                        </goals>
+                        <configuration>
+                            <artifacts>
+                                <artifact>
+                                    <file>
+                                        src/main/resources/org.apache.unomi.sfdc.cfg
+                                    </file>
+                                    <type>cfg</type>
+                                    <classifier>sfdccfg</classifier>
+                                </artifact>
+                            </artifacts>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/FieldMapping.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/FieldMapping.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/FieldMapping.java
new file mode 100644
index 0000000..68420ae
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/FieldMapping.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services;
+
+/**
+ * A mapping between two fields
+ */
+public class FieldMapping {
+
+    public enum MappingDirection {
+        LEFT_TO_RIGHT,
+        RIGHT_TO_LEFT,
+        BIDIRECTIONAL
+    }
+
+    private MappedField leftField;
+    private MappedField rightField;
+    private String fieldConverterIdentifier;
+    private MappingDirection mappingDirection;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/MappedField.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/MappedField.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/MappedField.java
new file mode 100644
index 0000000..872487d
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/MappedField.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services;
+
+/**
+ * A field used in a field mapping
+ */
+public class MappedField {
+
+    private String name;
+    private String type;
+    private boolean readOnly;
+    private String source;
+    private boolean identifier;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCConfiguration.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCConfiguration.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCConfiguration.java
new file mode 100644
index 0000000..fc84579
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCConfiguration.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services;
+
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.unomi.api.Item;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An persistence item that contains the configuration to the Salesforce service.
+ */
+public class SFDCConfiguration extends Item {
+
+    /**
+     * The ImportConfiguration ITEM_TYPE
+     *
+     * @see Item for a discussion of ITEM_TYPE
+     */
+    public static final String ITEM_TYPE = "sfdcConfiguration";
+
+    private String sfdcLoginEndpoint;
+    private String sfdcUserUsername;
+    private String sfdcUserPassword;
+    private String sfdcUserSecurityToken;
+    private String sfdcConsumerKey;
+    private String sfdcConsumerSecret;
+    private String sfdcChannel;
+    private String sfdcFieldMappings;
+    private String sfdcFieldMappingsIdentifier;
+    private long sfdcSessionTimeout = 15 * 60 * 1000L; // 15 minutes by default
+
+    private Map<String, String> unomiToSfdcFieldMappings = new HashMap<>();
+    private Map<String, String> sfdcToUnomiFieldMappings = new HashMap<>();
+
+    private String unomiIdentifierField;
+    private String sfdcIdentifierField;
+
+    public SFDCConfiguration() {
+    }
+
+    public String getSfdcLoginEndpoint() {
+        return sfdcLoginEndpoint;
+    }
+
+    public void setSfdcLoginEndpoint(String sfdcLoginEndpoint) {
+        this.sfdcLoginEndpoint = sfdcLoginEndpoint;
+    }
+
+    public String getSfdcUserUsername() {
+        return sfdcUserUsername;
+    }
+
+    public void setSfdcUserUsername(String sfdcUserUsername) {
+        this.sfdcUserUsername = sfdcUserUsername;
+    }
+
+    public String getSfdcUserPassword() {
+        return sfdcUserPassword;
+    }
+
+    public void setSfdcUserPassword(String sfdcUserPassword) {
+        this.sfdcUserPassword = sfdcUserPassword;
+    }
+
+    public String getSfdcUserSecurityToken() {
+        return sfdcUserSecurityToken;
+    }
+
+    public void setSfdcUserSecurityToken(String sfdcUserSecurityToken) {
+        this.sfdcUserSecurityToken = sfdcUserSecurityToken;
+    }
+
+    public String getSfdcConsumerKey() {
+        return sfdcConsumerKey;
+    }
+
+    public void setSfdcConsumerKey(String sfdcConsumerKey) {
+        this.sfdcConsumerKey = sfdcConsumerKey;
+    }
+
+    public String getSfdcConsumerSecret() {
+        return sfdcConsumerSecret;
+    }
+
+    public void setSfdcConsumerSecret(String sfdcConsumerSecret) {
+        this.sfdcConsumerSecret = sfdcConsumerSecret;
+    }
+
+    public String getSfdcChannel() {
+        return sfdcChannel;
+    }
+
+    public void setSfdcChannel(String sfdcChannel) {
+        this.sfdcChannel = sfdcChannel;
+    }
+
+    public String getSfdcFieldMappings() {
+        return sfdcFieldMappings;
+    }
+
+    public String getSfdcFieldMappingsIdentifier() {
+        return sfdcFieldMappingsIdentifier;
+    }
+
+    public long getSfdcSessionTimeout() {
+        return sfdcSessionTimeout;
+    }
+
+    public void setSfdcSessionTimeout(long sfdcSessionTimeout) {
+        this.sfdcSessionTimeout = sfdcSessionTimeout;
+    }
+
+    public void setSfdcFieldMappings(String sfdcFieldMappings) {
+        this.sfdcFieldMappings = sfdcFieldMappings;
+        String[] mappings = sfdcFieldMappings.split(",");
+        if (mappings != null && mappings.length > 0) {
+            for (String mapping : mappings) {
+                String[] parts = mapping.split("=");
+                if (parts != null && parts.length == 2) {
+                    unomiToSfdcFieldMappings.put(parts[0], parts[1]);
+                    sfdcToUnomiFieldMappings.put(parts[1], parts[0]);
+                }
+            }
+        }
+    }
+
+    public void setSfdcFieldMappingsIdentifier(String sfdcFieldMappingsIdentifier) {
+        this.sfdcFieldMappingsIdentifier = sfdcFieldMappingsIdentifier;
+        String[] sfdcFieldMappingsIdentifierParts = sfdcFieldMappingsIdentifier.split("=");
+        if (sfdcFieldMappingsIdentifierParts != null && sfdcFieldMappingsIdentifierParts.length == 2) {
+            unomiIdentifierField = sfdcFieldMappingsIdentifierParts[0];
+            sfdcIdentifierField = sfdcFieldMappingsIdentifierParts[1];
+        }
+    }
+
+    public Map<String, String> getUnomiToSfdcFieldMappings() {
+        return unomiToSfdcFieldMappings;
+    }
+
+    public Map<String, String> getSfdcToUnomiFieldMappings() {
+        return sfdcToUnomiFieldMappings;
+    }
+
+    public String getUnomiIdentifierField() {
+        return unomiIdentifierField;
+    }
+
+    public String getSfdcIdentifierField() {
+        return sfdcIdentifierField;
+    }
+
+    public boolean isComplete() {
+        return (!StringUtils.isEmpty(sfdcLoginEndpoint) &&
+                !StringUtils.isEmpty(sfdcUserUsername) &&
+                !StringUtils.isEmpty(sfdcUserPassword) &&
+                !StringUtils.isEmpty(sfdcUserSecurityToken) &&
+                !StringUtils.isEmpty(sfdcConsumerKey) &&
+                !StringUtils.isEmpty(sfdcConsumerSecret) &&
+                !StringUtils.isEmpty(sfdcFieldMappingsIdentifier));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCService.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCService.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCService.java
new file mode 100644
index 0000000..ab5041b
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCService.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services;
+
+import org.apache.http.HttpException;
+import org.apache.unomi.api.Profile;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Public interface for the Salesforce connector
+ */
+public interface SFDCService {
+
+    /**
+     * Load the configuration from the persistence service (if it exists)
+     * @return an instance of the configuration if it was found, null otherwise
+     */
+    SFDCConfiguration loadConfiguration();
+
+    /**
+     * Save a Salesforce configuration into the persistence service
+     * @param sfdcConfiguration the configuration to persist
+     * @return
+     */
+    boolean saveConfiguration(SFDCConfiguration sfdcConfiguration);
+
+    /**
+     * Login into Salesforce using the configuration passed in the methods arguments.
+     * @param sfdcConfiguration the configuration to use for the login
+     * @return true if the login was successful, false otherwise
+     * @throws HttpException
+     * @throws IOException
+     */
+    boolean login(SFDCConfiguration sfdcConfiguration) throws HttpException, IOException;
+
+    SFDCSession getSFDCSession();
+
+    void logout();
+
+    /**
+     * Create or update a lead based on a Unomi profile.
+     * @param profile
+     * @return a String containing the identifier of the corresponding SFDC lead
+     */
+    String createOrUpdateLead(Profile profile);
+
+    /**
+     * Updates a Unomi profile from a Salesforce lead
+     * @param profile
+     * @return true if the profile was updated, false otherwise.
+     */
+    boolean updateProfileFromLead(Profile profile);
+
+    Set<String> getRecentLeadIds();
+
+    Map<String,Object> getLead(String leadId);
+
+    Map<String,Object> query(String query);
+
+    Set<String> findLeadIdsByIdentifierValue(String identifierFieldValue);
+
+    Map<String,Object> getLimits();
+}

http://git-wip-us.apache.org/repos/asf/incubator-unomi/blob/a897ab84/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCSession.java
----------------------------------------------------------------------
diff --git a/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCSession.java b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCSession.java
new file mode 100644
index 0000000..d8938b3
--- /dev/null
+++ b/extensions/salesforce-connector/services/src/main/java/org/apache/unomi/sfdc/services/SFDCSession.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.unomi.sfdc.services;
+
+/**
+ * Model object that stores Salesforce session data
+ */
+public class SFDCSession {
+    private String sessionId;
+    private String endPoint;
+    private String signature;
+    private String id;
+    private String tokenType;
+    private Long issuedAt;
+    private Long timeout;
+
+    public SFDCSession(String sessionId, String endPoint, String signature, String id, String tokenType, String issuedAt, Long timeout) {
+        this.sessionId = sessionId;
+        this.endPoint = endPoint;
+        this.signature = signature;
+        this.id = id;
+        this.tokenType = tokenType;
+        this.issuedAt = Long.parseLong(issuedAt) * 1000; // value in in seconds, we convert it to milliseconds
+        this.timeout = timeout;
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public String getEndPoint() {
+        return endPoint;
+    }
+
+    public String getSignature() {
+        return signature;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getTokenType() {
+        return tokenType;
+    }
+
+    public Long getIssuedAt() {
+        return issuedAt;
+    }
+
+    public boolean isExpired() {
+        if (System.currentTimeMillis() < this.issuedAt + this.timeout) {
+            return false;
+        }
+        return true;
+    }
+}