//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 
=== Recipes

==== Introduction

In this section of the documentation we provide quick recipes focused on helping you achieve a specific result with
Apache Unomi.

==== Enabling debug mode

Although the examples provided in this documentation are correct (they will work "as-is"), 
you might be tempted to modify them to fit your use case, which might result in errors.

The best approach during development is to enable Apache Unomi debug mode, which will provide 
you with more detailed logs about events processing.

The debug mode can be activated via the karaf SSH console (default credentials are karaf/karaf):

[source]
----
ubuntu@ip-10-0-3-252:~/$ ssh -p 8102 karaf@localhost
Password authentication
Password:
        __ __                  ____
       / //_/____ __________ _/ __/
      / ,<  / __ `/ ___/ __ `/ /_
     / /| |/ /_/ / /  / /_/ / __/
    /_/ |_|\__,_/_/   \__,_/_/

  Apache Karaf (4.2.15)

Hit '<tab>' for a list of available commands
and '[cmd] --help' for help on a specific command.
Hit 'system:shutdown' to shutdown Karaf.
Hit '<ctrl-d>' or type 'logout' to disconnect shell from current session.

karaf@root()> log:set DEBUG org.apache.unomi.schema.impl.SchemaServiceImpl
----

You can then either watch the logs via your preferred logging mechanism (docker logs, log file, ...) or 
simply tail the logs to the terminal you used to enable debug mode.

[source]
----
karaf@root()> log:tail
08:55:28.128 DEBUG [qtp1422628821-128] Schema validation found 2 errors while validating against schema: https://unomi.apache.org/schemas/json/events/view/1-0-0
08:55:28.138 DEBUG [qtp1422628821-128] Validation error: There are unevaluated properties at following paths $.source.properties
08:55:28.140 DEBUG [qtp1422628821-128] Validation error: There are unevaluated properties at following paths $.source.itemId, $.source.itemType, $.source.scope, $.source.properties
08:55:28.142 ERROR [qtp1422628821-128] An event was rejected - switch to DEBUG log level for more information
----

The example above shows schema validation failure at the `$.source.properties` path. 
Note that the validation will output one log line for the exact failing path and a log line for its parent, 
therefore to find the source of a schema validation issue it's best to start from the top.

==== How to read a profile

The simplest way to retrieve profile data for the current profile is to simply send a request to the /cxs/context.json
endpoint. However you will need to send a body along with that request. Here's an example:

Here is an example that will retrieve all the session and profile properties, as well as the profile's segments and scores

[source]
----
curl -X POST http://localhost:8181/cxs/context.json?sessionId=1234 \
-H "Content-Type: application/json" \
--data-raw '{
    "source": {
        "itemId":"homepage",
        "itemType":"page",
        "scope":"example"
    },
    "requiredProfileProperties":["*"],
    "requiredSessionProperties":["*"],
    "requireSegments":true,
    "requireScores":true
}'
----

The `requiredProfileProperties` and `requiredSessionProperties` are properties that take an array of property names
that should be retrieved. In this case we use the wildcard character '*' to say we want to retrieve all the available
properties. The structure of the JSON object that you should send is a JSON-serialized version of the 
http://unomi.apache.org/unomi-api/apidocs/org/apache/unomi/api/ContextRequest.html[ContextRequest] Java class.

Note that it is also possible to access a profile's data through the /cxs/profiles/ endpoint but that really should be
reserved to administrative purposes. All public accesses should always use the /cxs/context.json endpoint for consistency
and security.

==== How to update a profile from the public internet

Before we get into how to update a profile directly from a request coming from the public internet, we'll quickly talk
first about how NOT to do it, because we often see users using the following anti-patterns.

===== How NOT to update a profile from the public internet

Please avoid using the /cxs/profile endpoint. This endpoint was initially the only way to update a profile but it has
multiple issues:

- it requires authenticated access. The temptation can be great to use this endpoint because it is simple to access
but the risk is that developers might include the credentials to access it in non-secure parts of code such as
client-side code. Since there is no difference between this endpoint and any other administration-focused endpoints,
attackers could easily re-use stolen credentials to wreak havock on the whole platform.
- No history of profile modifications is kept: this can be a problem for multiple reasons: you might want to keep an
trail of profile modifications, or even a history of profile values in case you want to understand how a profile
property was modified.
- Even when protected using some kind of proxy, potentially the whole profile properties might be modified, including
ones that you might not want to be overriden.

===== Recommended ways to update a profile

Instead you can use the following solutions to update profiles:

- (Preferred) Use you own custom event(s) to send data you want to be inserted in a profile, and use rules to map the
event data to the profile. This is simpler than it sounds, as usually all it requires is setting up a simple rule, 
defining the corresponding JSON schema and you're ready to update profiles using events. 

- Use the protected built-in "updateProperties" event. This event is designed to be used for administrative purposes
only. Again, prefer the custom events solution because as this is a protected event it will require sending the Unomi
key as a request header, and as Unomi only supports a single key for the moment it could be problematic if the key is
intercepted. But at least by using an event you will get the benefits of auditing and historical property modification
tracing.

Let's go into more detail about the preferred way to update a profile. Let's consider the following example of a rule:

[source]
----
curl -X POST http://localhost:8181/cxs/rules \
--user karaf:karaf \
-H "Content-Type: application/json" \
--data-raw '{
  "metadata": {
    "id": "setContactInfo",
    "name": "Copy the received contact info to the current profile",
    "description": "Copies the contact info received in a custom event called 'contactInfoSubmitted' to the current profile"
  },
  "raiseEventOnlyOnceForSession": false,
  "condition": {
    "type": "eventTypeCondition",
    "parameterValues": {
      "eventTypeId": "contactInfoSubmitted"
    }
  },
  "actions": [
    {
      "type": "setPropertyAction",
      "parameterValues": {
        "setPropertyName": "properties(firstName)",
        "setPropertyValue": "eventProperty::properties(firstName)",
        "setPropertyStrategy": "alwaysSet"
      }
    },
    {
      "type": "setPropertyAction",
      "parameterValues": {
        "setPropertyName": "properties(lastName)",
        "setPropertyValue": "eventProperty::properties(lastName)",
        "setPropertyStrategy": "alwaysSet"
      }
    },
    {
      "type": "setPropertyAction",
      "parameterValues": {
        "setPropertyName": "properties(email)",
        "setPropertyValue": "eventProperty::properties(email)",
        "setPropertyStrategy": "alwaysSet"
      }
    }
  ]
}'
----

What this rule does is that it listen for a custom event (events don't need any registration, you can simply start
sending them to Apache Unomi whenever you like) of type 'contactInfoSubmitted' and it will search for properties called
'firstName', 'lastName' and 'email' and copy them over to the profile with corresponding property names. You could of
course change any of the property names to find your needs. For example you might want to prefix the profile properties
with the source of the event, such as 'mobileApp:firstName'.

Now that our rule is defined, the next step is to create a scope and a JSON Schema corresponding to the event to be submitted.

We will start by creating a scope called "example" scope:
[source]
----
curl --location --request POST 'http://localhost:8181/cxs/scopes' \
-u 'karaf:karaf' \
--header 'Content-Type: application/json' \
--data-raw '{
"itemId": "example",
"itemType": "scope"
}'
----

The next step consist in creating a JSON Schema to validate our event.

[source]
----
curl --location --request POST 'http://localhost:8181/cxs/jsonSchema' \
-u 'karaf:karaf' \
--header 'Content-Type: application/json' \
--data-raw '{
    "$id": "https://unomi.apache.org/schemas/json/events/contactInfoSubmitted/1-0-0",
    "$schema": "https://json-schema.org/draft/2019-09/schema",
    "self": {
        "vendor": "org.apache.unomi",
        "name": "contactInfoSubmitted",
        "format": "jsonschema",
        "target": "events",
        "version": "1-0-0"
    },
    "title": "contactInfoSubmittedEvent",
    "type": "object",
    "allOf": [{ "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }],
    "properties": {
        "source" : {
          "$ref" : "https://unomi.apache.org/schemas/json/item/1-0-0"
        },
        "target" : {
          "$ref" : "https://unomi.apache.org/schemas/json/item/1-0-0"
        },
        "properties": {
          "type": "object",
          "properties": {
            "firstName": {
              "type": ["null", "string"]
            },
            "lastName": {
              "type": ["null", "string"]
            },
            "email": {
              "type": ["null", "string"]
            }
          }
        }
    },
    "unevaluatedProperties": false
}'
----

You can notice the following in the above schema:

* We are creating a schema of type "events" ("self.target" equals "events")
* The name of this schema is "contactInfoSubmitted", this MUST match the value of the "eventType" field in the event itself (below)
* To simplify our schema declaration, we're referring to an already existing schema (https://unomi.apache.org/schemas/json/item/1-0-0) to validate the "source" and "target" properties. Apache Unomi ships with a set of predefined JSON Schemas, detailed here: https://github.com/apache/unomi/tree/master/extensions/json-schema/services/src/main/resources/META-INF/cxs/schemas.
* `"unevaluatedProperties": false` indicates that the event should be rejected if it contains any additional metadata.

Finally, send the `contactInfoSubmitted` event using a request similar to this one:

[source]
----
curl -X POST http://localhost:8181/cxs/eventcollector \
-H "Content-Type: application/json" \
--data-raw '{
    "sessionId" : "1234",
    "events":[
        {
            "eventType":"contactInfoSubmitted",
            "scope": "example",
            "source":{
                "itemType": "site",
                "scope": "example",
                "itemId": "mysite"
            },
            "target":{
                "itemType": "form",
                "scope": "example",
                "itemId": "contactForm"
            },
            "properties" : {
              "firstName": "John",
              "lastName": "Doe",
              "email": "john.doe@acme.com"
            }
        }
    ]
}'
----

The event we just submitted can be retrieved using the following request:

[source]
----
curl -X POST http://localhost:8181/cxs/events/search \
--user karaf:karaf \
-H "Content-Type: application/json" \
--data-raw '{
  "offset" : 0,
  "limit" : 20,
  "condition" : {
    "type": "eventPropertyCondition",
    "parameterValues" : {
      "propertyName" : "properties.firstName",
      "comparisonOperator" : "equals",
      "propertyValue" : "John"
    }
  }
}'
----

===== Troubleshooting common errors

There could be two types of common errors while customizing the above requests:
* The schema is invalid
* The event is invalid

While first submitting the schema during its creation, Apache Unomi will validate it is syntaxically correct (JSON) 
but will not perform any further validation. Since the schema will be processed for the first time when events are submitted, 
errors might be noticeable at that time. 

Those errors are usually self-explanatory, such as this one pointing to an incorrect lcoation for the "firstName" keyword:
[source]
----
09:35:56.573 WARN [qtp1421852915-83] Unknown keyword firstName - you should define your own Meta Schema. If the keyword is irrelevant for validation, just use a NonValidationKeyword
----

If an event is invalid, the logs will contain details about the part of the event that did not validate against the schema. 
In the example below, an extra property "abcd" was added to the event:
[source]
----
12:27:04.269 DEBUG [qtp1421852915-481] Schema validation found 1 errors while validating against schema: https://unomi.apache.org/schemas/json/events/contactInfoSubmitted/1-0-0
12:27:04.272 DEBUG [qtp1421852915-481] Validation error: There are unevaluated properties at following paths $.properties.abcd
12:27:04.273 ERROR [qtp1421852915-481] An event was rejected - switch to DEBUG log level for more information
----

==== How to search for profile events

Sometimes you want to retrieve events for a known profile. You will need to provide a query in the body of the request
that looks something like this (and https://unomi.apache.org/rest-api-doc/#1768188821[documentation is available in the REST API]) :

[source]
----
curl -X POST http://localhost:8181/cxs/events/search \
--user karaf:karaf \
-H "Content-Type: application/json" \
--data-raw '{
  "offset" : 0,
  "limit" : 20,
  "condition" : {
    "type": "eventPropertyCondition",
    "parameterValues" : {
      "propertyName" : "profileId",
      "comparisonOperator" : "equals",
      "propertyValue" : "PROFILE_ID"
    }
  }
}'
----

where PROFILE_ID is a profile identifier. This will indeed retrieve all the events for a given profile.

==== How to create a new rule

There are basically two ways to create a new rule :

- Using the REST API
- Packaging it as a predefined rule in a plugin

In both cases the JSON structure for the rule will be exactly the same, and in most scenarios it will be more
interesting to use the REST API to create and manipulate rules, as they don't require any development or deployments
on the Apache Unomi server.

[source]
----
curl -X POST http://localhost:8181/cxs/rules \
--user karaf:karaf \
-H "Content-Type: application/json" \
--data-raw '{
  "metadata": {
    "id": "exampleEventCopy",
    "name": "Example Copy Event to Profile",
    "description": "Copy event properties to profile properties"
  },
  "condition": {
      "type": "eventTypeCondition",
      "parameterValues": {
        "eventTypeId" : "myEvent"
      }
  },
  "actions": [
    {
      "parameterValues": {
      },
      "type": "allEventToProfilePropertiesAction"
    }
  ]
}'
----

The above rule will be executed if the incoming event is of type `myEvent` and will simply copy all the properties
contained in the event to the current profile.

==== How to search for profiles

In order to search for profiles you will have to use the /cxs/profiles/search endpoint that requires a Query JSON
structure. Here's an example of a profile search with a Query object:

[source]
----
curl -X POST http://localhost:8181/cxs/profiles/search \
--user karaf:karaf \
-H "Content-Type: application/json" \
--data-raw '{
  "text" : "unomi",
  "offset" : 0,
  "limit" : 10,
  "sortby" : "properties.lastName:asc,properties.firstName:desc",
  "condition" : {
    "type" : "booleanCondition",
    "parameterValues" : {
      "operator" : "and",
      "subConditions" : [
        {
          "type": "profilePropertyCondition",
          "parameterValues": {
            "propertyName": "properties.leadAssignedTo",
            "comparisonOperator": "exists"
          }
        },
        {
          "type": "profilePropertyCondition",
          "parameterValues": {
            "propertyName": "properties.lastName",
            "comparisonOperator": "exists"
          }
        }
      ]
    }
  }
}'
----

In the above example, you search for all the profiles that have the `leadAssignedTo` and `lastName` properties and that
have the `unomi` value anywhere in their profile property values. You are also specifying that you only want 10 results
beginning at offset 0. The results will be also sorted in alphabetical order for the `lastName` property value, and then
by reverse alphabetical order for the `firstName` property value.

As you can see, queries can be quite complex. Please remember that the more complex the more resources it will consume
on the server and potentially this could affect performance.

==== Getting / updating consents

You can find information on how to retrieve or create/update consents in the <<Consent API>> section.

==== How to send a login event to Unomi

Tracking logins must be done carefully with Unomi. A login event is considered a "privileged" event and therefore for
not be initiated from the public internet. Ideally user authentication should always be validated by a trusted third-
party even if it is a well-known social platform such as Facebook or Twitter. Basically what should NEVER be done:

1. Login to a social platform
2. Call back to the originating page
3. Send a login event to Unomi from the page originating the login in step 1

The problem with this, is that any attacker could simply directly call step 3 without any kind of security. Instead the
flow should look something like this:

1. Login to a social platform
2. Call back to a special secured system that performs an server-to-server call to send the login event to Apache
Unomi using the Unomi key.

For simplicity reasons, in our login example, the first method is used, but it really should never be done like this
in production because of the aforementioned security issues. The second method, although a little more involved, is
much preferred.

When sending a login event, you can setup a rule that can check a profile property to see if profiles can be merged on an
universal identifier such as an email address.

In our login sample we provide an example of such a rule. You can find it here:

https://github.com/apache/unomi/blob/master/samples/login-integration/src/main/resources/META-INF/cxs/rules/exampleLogin.json

As you can see in this rule, we call an action called :

    mergeProfilesOnPropertyAction

with as a parameter value the name of the property on which to perform the merge (the email). What this means is that
upon successful login using an email, Unomi will look for other profiles that have the same email and merge them into
a single profile. Because of the merge, this should only be done for authenticated profiles, otherwise this could be a
security issue since it could be a way to load data from other profiles by merging their data !

==== What profile aliases are and how to use them

Profile aliases make it possible to reference profiles using multiple identifiers.
The profile alias object basically contains a link between the alias ID and the profile ID. The `itemId` of a profile alias is the actual alias ID, which the `profileID` field contains the reference to the aliased profile.

===== What they are

image::profile-alias-overview.png[Profile alias overview]

Profile aliases:

- Make it possible to lookup profiles by main (Unomi) ID or by any other alias ID
- Aliases are just IDs stored in a dedicated index
- A profile may have an unlimited number of aliases attached to it.

===== How to use them

image::profile-alias-external-ids.png[Profile with external IDs]

Here are different use cases for profile aliases:

- Connect different systems to Unomi such as a CRM, CMS and native mobile app that all have their own iD for a single customer
- Merging profiles when a visitor is identified
- Adding new IDs at a later time

===== Example

Here is an example of multiple external aliases pointing to a single Unomi profile

image::profile-alias-example.png[Profile alias example]

===== Interactions with merging

Profile merges have been modified to use aliases starting Unomi 2

Upon merge:

- Properties are copied to the master profile as before
- An alias is created for the "master" profile with the ID of the merged profile
- Merged profiles are now deleted
- "mergedWith" property is no longer used since we deleted the merged profiles

===== API

/context.json and /eventcollector will now look up profiles by profile ID or aliases from the same cookie (`context-profile-id`) or body parameters (`profileId`)

|===
| *Verb* | *Path* | *Description*

| GET | /cxs/profiles/PROFILE_ID_OR_ALIAS | Retrieves a profile by ID or Alias ID (useful if an external system wants to get a profile)

| GET | /cxs/profiles/PROFILE_ID/aliases | Get all the aliases for a profile

| POST | /cxs/profiles/PROFILE_ID/aliases/ALIAS_ID | Add an alias to a profile

| DELETE | /cxs/profiles/PROFILE_ID/aliases/ALIAS_ID | Remove an alias from a profile

|===
