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 2026/06/02 09:27:10 UTC
(unomi) branch master updated: UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new 956440cd4 UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)
956440cd4 is described below
commit 956440cd45976afb6ba6b485a5605197eb8b6c12
Author: Serge Huber <sh...@apache.org>
AuthorDate: Tue Jun 2 11:27:04 2026 +0200
UNOMI-879 Unified CRUD Karaf shell developer commands (includes UNOMI-920 YAML toString() debugging) (#755)
---
.../apache/unomi/api/services/EventService.java | 7 +
.../apache/unomi/api/services/ProfileService.java | 9 +
.../apache/unomi/itests/graphql/GraphQLListIT.java | 7 +-
.../services/impl/events/EventServiceImpl.java | 5 +
.../services/impl/profiles/ProfileServiceImpl.java | 5 +
tools/shell-dev-commands/pom.xml | 14 +
.../apache/unomi/shell/commands/ActionList.java | 64 ---
.../apache/unomi/shell/commands/ActionView.java | 48 --
.../apache/unomi/shell/commands/ConditionList.java | 64 ---
.../apache/unomi/shell/commands/ConditionView.java | 50 --
.../unomi/shell/commands/DeployDefinition.java | 113 ----
.../org/apache/unomi/shell/commands/EventList.java | 90 ---
.../apache/unomi/shell/commands/EventSearch.java | 99 ----
.../org/apache/unomi/shell/commands/EventView.java | 60 --
.../apache/unomi/shell/commands/ProfileList.java | 84 ---
.../apache/unomi/shell/commands/ProfileView.java | 48 --
.../org/apache/unomi/shell/commands/RuleList.java | 108 ----
.../apache/unomi/shell/commands/SegmentList.java | 73 ---
.../apache/unomi/shell/commands/SegmentRemove.java | 56 --
.../apache/unomi/shell/commands/SegmentView.java | 48 --
.../apache/unomi/shell/commands/SessionList.java | 79 ---
.../unomi/shell/commands/UndeployDefinition.java | 111 ----
.../unomi/shell/dev/actions/UnomiCrudCommand.java | 609 +++++++++++++++++++++
.../unomi/shell/dev/commands/BaseCommand.java | 53 ++
.../unomi/shell/dev/commands/BaseListCommand.java | 74 +++
.../shell/dev/commands/BaseSimpleCommand.java | 61 +++
.../unomi/shell/dev/commands/CommandUtils.java | 80 +++
.../unomi/shell/dev/commands/DeployDefinition.java | 137 +++++
.../commands/DeploymentCommandSupport.java | 120 +++-
.../unomi/shell/{ => dev}/commands/EventTail.java | 16 +-
.../{ => dev}/commands/ListCommandSupport.java | 15 +-
.../shell/dev/commands/RemoveCommandSupport.java | 51 ++
.../shell/{ => dev}/commands/RuleResetStats.java | 7 +-
.../unomi/shell/{ => dev}/commands/RuleTail.java | 16 +-
.../unomi/shell/{ => dev}/commands/RuleWatch.java | 18 +-
.../{ => dev}/commands/TailCommandSupport.java | 12 +-
.../unomi/shell/dev/commands/TailCommandUtils.java | 91 +++
.../shell/dev/commands/UndeployDefinition.java | 95 ++++
.../commands/actions/ActionTypeCrudCommand.java | 157 ++++++
.../commands/campaigns/CampaignCrudCommand.java | 151 +++++
.../campaigns/CampaignEventCrudCommand.java | 147 +++++
.../conditions/ConditionTypeCrudCommand.java | 153 ++++++
.../dev/commands/consents/ConsentCrudCommand.java | 255 +++++++++
.../dev/commands/events/EventCrudCommand.java | 167 ++++++
.../shell/dev/commands/goals/GoalCrudCommand.java | 143 +++++
.../dev/commands/personas/PersonaCrudCommand.java | 153 ++++++
.../commands/profiles/ProfileAliasCrudCommand.java | 159 ++++++
.../dev/commands/profiles/ProfileCrudCommand.java | 186 +++++++
.../properties/PropertyTypeCrudCommand.java | 174 ++++++
.../shell/dev/commands/rules/RuleCrudCommand.java | 247 +++++++++
.../commands/rules/RuleStatisticsCrudCommand.java | 133 +++++
.../dev/commands/scopes/ScopeCrudCommand.java | 139 +++++
.../dev/commands/scoring/ScoringCrudCommand.java | 142 +++++
.../dev/commands/segments/SegmentCrudCommand.java | 195 +++++++
.../dev/commands/sessions/SessionCrudCommand.java | 128 +++++
.../dev/commands/topics/TopicCrudCommand.java | 120 ++++
.../unomi/shell/dev/completers/BaseCompleter.java | 86 +++
.../unomi/shell/dev/completers/IdCompleter.java | 156 ++++++
.../completers/OperationCompleter.java} | 27 +-
.../unomi/shell/dev/completers/TypeCompleter.java | 66 +++
.../unomi/shell/dev/services/BaseCrudCommand.java | 399 ++++++++++++++
.../unomi/shell/dev/services/CrudCommand.java | 206 +++++++
62 files changed, 5300 insertions(+), 1286 deletions(-)
diff --git a/api/src/main/java/org/apache/unomi/api/services/EventService.java b/api/src/main/java/org/apache/unomi/api/services/EventService.java
index fb6d60d70..64ca1beeb 100644
--- a/api/src/main/java/org/apache/unomi/api/services/EventService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/EventService.java
@@ -153,4 +153,11 @@ public interface EventService {
* @param profileId identifier of the profile that we want to remove it's events
*/
void removeProfileEvents(String profileId);
+
+ /**
+ * Deletes the event identified by the given identifier from persistence.
+ *
+ * @param eventIdentifier the unique identifier for the event
+ */
+ void deleteEvent(String eventIdentifier);
}
diff --git a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
index 03da6ce9b..1895e9dbf 100644
--- a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
@@ -217,6 +217,15 @@ public interface ProfileService {
*/
void removeProfileSessions(String profileId);
+ /**
+ * Deletes the session identified by the given identifier from persistence.
+ * Note: events belonging to this session are NOT removed; they remain in persistence
+ * with a dangling sessionId reference.
+ *
+ * @param sessionIdentifier the unique identifier for the session
+ */
+ void deleteSession(String sessionIdentifier);
+
/**
* Checks whether the specified profile and/or session satisfy the specified condition.
*
diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
index 60a9a8507..66553e742 100644
--- a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java
@@ -21,14 +21,10 @@ import org.apache.unomi.api.Profile;
import org.apache.unomi.lists.UserList;
import org.junit.Assert;
import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.util.Objects;
public class GraphQLListIT extends BaseGraphQLIT {
- private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLListIT.class);
-
@Test
public void testCRUD() throws Exception {
Profile persistedProfile = null;
@@ -86,7 +82,6 @@ public class GraphQLListIT extends BaseGraphQLIT {
try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) {
return ResponseContext.parse(response.getEntity());
} catch (Exception e) {
- LOGGER.warn("find-lists poll attempt failed: {}", e.getMessage());
return null;
}
},
@@ -108,7 +103,7 @@ public class GraphQLListIT extends BaseGraphQLIT {
try (CloseableHttpResponse response = post("graphql/list/delete-list.json")) {
final ResponseContext context = ResponseContext.parse(response.getEntity());
- Assert.assertTrue("testListId", context.getValue("data.cdp.deleteList"));
+ Assert.assertTrue("deleteList should return true", Boolean.TRUE.equals(context.getValue("data.cdp.deleteList")));
}
} finally {
if (persistedProfile != null) {
diff --git a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
index f345466c7..60680924e 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java
@@ -336,4 +336,9 @@ public class EventServiceImpl implements EventService {
persistenceService.removeByQuery(profileCondition,Event.class);
}
+
+ @Override
+ public void deleteEvent(String eventIdentifier) {
+ persistenceService.remove(eventIdentifier, Event.class);
+ }
}
diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index 9673a707c..e6b74c917 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -1015,6 +1015,11 @@ public class ProfileServiceImpl implements ProfileService, SynchronousBundleList
persistenceService.removeByQuery(profileCondition, Session.class);
}
+ @Override
+ public void deleteSession(String sessionIdentifier) {
+ persistenceService.remove(sessionIdentifier, Session.class);
+ }
+
@Override
public boolean matchCondition(Condition condition, Profile profile, Session session) {
ParserHelper.resolveConditionType(definitionsService, condition, "profile " + profile.getItemId() + " matching");
diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml
index 27b0b641f..807677a63 100644
--- a/tools/shell-dev-commands/pom.xml
+++ b/tools/shell-dev-commands/pom.xml
@@ -92,6 +92,8 @@
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient-osgi</artifactId>
+ <version>${httpclient-osgi.version}</version>
+ <type>bundle</type>
<scope>provided</scope>
</dependency>
<dependency>
@@ -99,6 +101,10 @@
<artifactId>commons-lang3</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-csv</artifactId>
+ </dependency>
<dependency>
<groupId>junit</groupId>
@@ -115,6 +121,14 @@
<configuration>
<instructions>
<Karaf-Commands>*</Karaf-Commands>
+ <Export-Package>
+ org.apache.unomi.shell.dev.services,
+ org.apache.unomi.shell.dev.commands
+ </Export-Package>
+ <_dsannotations>*</_dsannotations>
+ <_dsannotations-options>inherit</_dsannotations-options>
+ <_metatypeannotations>*</_metatypeannotations>
+ <_metatypeannotations-options>version;nested</_metatypeannotations-options>
</instructions>
</configuration>
</plugin>
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java
deleted file mode 100644
index 7839cb7c8..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-@Command(scope = "unomi", name = "action-list", description = "This will list all the actions deployed in the Apache Unomi Context Server")
-@Service
-public class ActionList extends ListCommandSupport {
-
- @Reference
- DefinitionsService definitionsService;
-
- @Override
- protected String[] getHeaders() {
- return new String[] {
- "Id",
- "Name",
- "System tags"
- };
- }
-
- @Override
- protected DataTable buildDataTable() {
- Collection<ActionType> allActions = definitionsService.getAllActionTypes();
-
- DataTable dataTable = new DataTable();
-
- for (ActionType actionType : allActions) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(actionType.getItemId());
- rowData.add(actionType.getMetadata().getName());
- rowData.add(StringUtils.join(actionType.getMetadata().getSystemTags(), ","));
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
-
- dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING));
- return dataTable;
- }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java
deleted file mode 100644
index 939e7f9d1..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "action-view", description = "This will display a single action deployed in the Apache Unomi Context Server")
-@Service
-public class ActionView implements Action {
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "actionId", description = "The identifier for the action", required = true, multiValued = false)
- String actionTypeIdentifier;
-
- public Object execute() throws Exception {
- ActionType actionType = definitionsService.getActionType(actionTypeIdentifier);
- if (actionType == null) {
- System.out.println("Couldn't find an action with id=" + actionTypeIdentifier);
- return null;
- }
- String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(actionType);
- System.out.println(jsonRule);
- return null;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java
deleted file mode 100644
index 83f6debc5..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-@Command(scope = "unomi", name = "condition-list", description = "This will list all the conditions deployed in the Apache Unomi Context Server")
-@Service
-public class ConditionList extends ListCommandSupport {
-
- @Reference
- DefinitionsService definitionsService;
-
- @Override
- protected String[] getHeaders() {
- return new String[] {
- "Id",
- "Name",
- "System tags"
- };
- }
-
- @Override
- protected DataTable buildDataTable() {
- Collection<ConditionType> allConditionTypes = definitionsService.getAllConditionTypes();
-
- DataTable dataTable = new DataTable();
-
- for (ConditionType conditionType : allConditionTypes) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(conditionType.getItemId());
- rowData.add(conditionType.getMetadata().getName());
- rowData.add(StringUtils.join(conditionType.getMetadata().getSystemTags(), ","));
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
-
- dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING));
- return dataTable;
- }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java
deleted file mode 100644
index 03efe9d48..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "condition-view", description = "This will display a single condition deployed in the Apache Unomi Context Server")
-@Service
-public class ConditionView implements Action {
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "conditionId", description = "The identifier for the condition", required = true, multiValued = false)
- String conditionTypeIdentifier;
-
- public Object execute() throws Exception {
- ConditionType conditionType = definitionsService.getConditionType(conditionTypeIdentifier);
- if (conditionType == null) {
- System.out.println("Couldn't find an action with id=" + conditionTypeIdentifier);
- return null;
- }
- String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(conditionType);
- System.out.println(jsonRule);
- return null;
- }
-
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java
deleted file mode 100644
index 9365de8a9..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Patch;
-import org.apache.unomi.api.PersonaWithSessions;
-import org.apache.unomi.api.PropertyType;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.campaigns.Campaign;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.goals.Goal;
-import org.apache.unomi.api.rules.Rule;
-import org.apache.unomi.api.segments.Scoring;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-import java.io.IOException;
-import java.net.URL;
-
-@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles")
-@Service
-public class DeployDefinition extends DeploymentCommandSupport {
-
- public void processDefinition(String definitionType, URL definitionURL) {
- try {
- if (ALL_OPTION_LABEL.equals(definitionType)) {
- String definitionURLString = definitionURL.toString();
- for (String possibleDefinitionType : definitionTypes) {
- if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
- definitionType = possibleDefinitionType;
- break;
- }
- }
- if (ALL_OPTION_LABEL.equals(definitionType)) {
- System.out.println("Couldn't resolve definition type for definition URL " + definitionURL);
- return;
- }
- }
- boolean successful = true;
- switch (definitionType) {
- case CONDITION_DEFINITION_TYPE:
- ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class);
- definitionsService.setConditionType(conditionType);
- break;
- case ACTION_DEFINITION_TYPE:
- ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class);
- definitionsService.setActionType(actionType);
- break;
- case GOAL_DEFINITION_TYPE:
- Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class);
- goalsService.setGoal(goal);
- break;
- case CAMPAIGN_DEFINITION_TYPE:
- Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class);
- goalsService.setCampaign(campaign);
- break;
- case PERSONA_DEFINITION_TYPE:
- PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class);
- profileService.savePersonaWithSessions(persona);
- break;
- case PROPERTY_DEFINITION_TYPE:
- PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class);
- profileService.setPropertyTypeTarget(definitionURL, propertyType);
- profileService.setPropertyType(propertyType);
- break;
- case RULE_DEFINITION_TYPE:
- Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class);
- rulesService.setRule(rule);
- break;
- case SEGMENT_DEFINITION_TYPE:
- Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class);
- segmentService.setSegmentDefinition(segment);
- break;
- case SCORING_DEFINITION_TYPE:
- Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class);
- segmentService.setScoringDefinition(scoring);
- break;
- case PATCH_DEFINITION_TYPE:
- Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class);
- patchService.patch(patch);
- break;
- default:
- System.out.println("Unrecognized definition type:" + definitionType);
- successful = false;
- break;
- }
- if (successful) {
- System.out.println("Predefined definition registered : " + definitionURL.getFile());
- }
- } catch (IOException e) {
- System.out.println("Error while saving definition " + definitionURL);
- System.out.println(e.getMessage());
- }
- }
-
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java
deleted file mode 100644
index b996a7085..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "event-list", description = "This commands lists the latest events updated in the Apache Unomi Context Server")
-@Service
-public class EventList extends ListCommandSupport {
-
- @Reference
- private EventService eventService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- @Argument(index = 1, name = "eventType", description = "If specified, will filter the event list by the given event type", required = false, multiValued = false)
- String eventType = null;
-
- String[] columnHeaders = new String[] {
- "ID",
- "Type",
- "Session",
- "Profile",
- "Timestamp",
- "Scope",
- "Persistent"
- };
-
- @Override
- protected String[] getHeaders() {
- return columnHeaders;
- }
-
- @Override
- protected DataTable buildDataTable() {
- Condition condition = new Condition(definitionsService.getConditionType("matchAllCondition"));
- if (eventType != null) {
- condition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
- condition.setParameter("eventTypeId", eventType);
- }
- Query query = new Query();
- query.setLimit(maxEntries);
- query.setCondition(condition);
- query.setSortby("timeStamp:desc");
- PartialList<Event> lastEvents = eventService.search(query);
- DataTable dataTable = new DataTable();
- for (Event event : lastEvents.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(event.getItemId());
- rowData.add(event.getEventType());
- rowData.add(event.getSessionId());
- rowData.add(event.getProfileId());
- rowData.add(event.getTimeStamp().toString());
- rowData.add(event.getScope());
- rowData.add(Boolean.toString(event.isPersistent()));
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
- return dataTable;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java
deleted file mode 100644
index 082f1d3d3..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Command(scope = "unomi", name = "event-search", description = "This commands search for profile events of a certain type by last timestamp in the Apache Unomi Context Server")
-@Service
-public class EventSearch extends ListCommandSupport {
- @Reference
- private EventService eventService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false)
- String profileIdentifier;
-
- @Argument(index = 1, name = "eventType", description = "The type of the event", required = false, multiValued = false)
- String eventTypeId;
-
- @Argument(index = 2, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- String[] columnHeaders = new String[] {
- "ID",
- "Type",
- "Session",
- "Profile",
- "Timestamp",
- "Scope",
- "Persistent"
- };
-
- @Override
- protected String[] getHeaders() {
- return columnHeaders;
- }
-
- @Override
- protected DataTable buildDataTable() {
- Condition booleanCondition = new Condition(definitionsService.getConditionType("booleanCondition"));
- booleanCondition.setParameter("operator", "and");
- List<Condition> subConditions = new ArrayList<>();
- if (profileIdentifier != null) {
- Condition eventProfileIdCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition"));
- eventProfileIdCondition.setParameter("propertyName", "profileId");
- eventProfileIdCondition.setParameter("comparisonOperator", "equals");
- eventProfileIdCondition.setParameter("propertyValue", profileIdentifier);
- subConditions.add(eventProfileIdCondition);
- }
- if (eventTypeId != null) {
- Condition eventTypeIdCondition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
- eventTypeIdCondition.setParameter("eventTypeId", eventTypeId);
- subConditions.add(eventTypeIdCondition);
- }
- booleanCondition.setParameter("subConditions", subConditions);
- PartialList<Event> lastEvents = eventService.searchEvents(booleanCondition, 0, maxEntries);
- DataTable dataTable = new DataTable();
- for (Event event : lastEvents.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(event.getItemId());
- rowData.add(event.getEventType());
- rowData.add(event.getSessionId());
- rowData.add(event.getProfileId());
- rowData.add(event.getTimeStamp().toString());
- rowData.add(event.getScope());
- rowData.add(Boolean.toString(event.isPersistent()));
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
- return dataTable;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java
deleted file mode 100644
index 10054ddd4..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "event-view", description = "This command will dump an Event as a JSON object")
-@Service
-public class EventView implements Action {
-
- @Reference
- EventService eventService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "event", description = "The identifier for the event", required = true, multiValued = false)
- String eventIdentifier;
-
- public Object execute() throws Exception {
-
- Condition eventCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition"));
- eventCondition.setParameter("propertyName", "itemId");
- eventCondition.setParameter("comparisonOperator", "equals");
- eventCondition.setParameter("propertyValue", eventIdentifier);
-
- PartialList<Event> matchingEvents = eventService.searchEvents(eventCondition, 0, 10);
- if (matchingEvents == null || matchingEvents.getTotalSize() != 1) {
- System.out.println("Couldn't find a single event with id=" + eventIdentifier + ". Maybe it wasn't a persistent event ?");
- return null;
- }
- String jsonEvent = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(matchingEvents.get(0));
- System.out.println(jsonEvent);
- return null;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java
deleted file mode 100644
index 24f3a2c97..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "profile-list", description = "This commands lists the latest profiles updated in the Apache Unomi Context Server")
-@Service
-public class ProfileList extends ListCommandSupport {
-
- @Reference
- ProfileService profileService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- @java.lang.Override
- protected String[] getHeaders() {
- return new String[] {
- "ID",
- "Scope",
- "Segments",
- "Consents",
- "Last visit",
- "Last update"
- };
- }
-
- @java.lang.Override
- protected DataTable buildDataTable() {
- Query query = new Query();
- query.setSortby("systemProperties.lastUpdated:desc,properties.lastVisit:desc");
- query.setLimit(maxEntries);
- Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
- query.setCondition(matchAllCondition);
- PartialList<Profile> lastModifiedProfiles = profileService.search(query, Profile.class);
- DataTable dataTable = new DataTable();
- for (Profile profile : lastModifiedProfiles.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(profile.getItemId());
- rowData.add(profile.getScope());
- rowData.add(StringUtils.join(profile.getSegments(), ","));
- rowData.add(StringUtils.join(profile.getConsents().keySet(), ","));
- rowData.add((String) profile.getProperty("lastVisit"));
- if (profile.getSystemProperties() != null && profile.getSystemProperties().get("lastUpdated") != null) {
- rowData.add((String) profile.getSystemProperties().get("lastUpdated"));
- } else {
- rowData.add("");
- }
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
- return dataTable;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java
deleted file mode 100644
index 89705790f..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "profile-view", description = "This command will dump a profile as a JSON string")
-@Service
-public class ProfileView implements Action {
-
- @Reference
- ProfileService profileService;
-
- @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false)
- String profileIdentifier;
-
- public Object execute() throws Exception {
- Profile profile = profileService.load(profileIdentifier);
- if (profile == null) {
- System.out.println("Couldn't find a profile with id=" + profileIdentifier);
- return null;
- }
- String jsonProfile = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(profile);
- System.out.println(jsonProfile);
- return null;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java
deleted file mode 100644
index de5a4d8c6..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.rules.RuleStatistics;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.RulesService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-import java.util.Map;
-
-@Command(scope = "unomi", name = "rule-list", description = "This will list all the rules deployed in the Apache Unomi Context Server")
-@Service
-public class RuleList extends ListCommandSupport {
-
- @Reference
- RulesService rulesService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- @Override
- protected String[] getHeaders() {
- return new String[] {
- "Activated",
- "Hidden",
- "Read-only",
- "Identifier",
- "Scope",
- "Name",
- "Tags",
- "System tags",
- "Executions",
- "Conditions [ms]",
- "Actions [ms]"
- };
- }
-
- @Override
- protected DataTable buildDataTable() {
- Query query = new Query();
- Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
- query.setCondition(matchAllCondition);
- query.setLimit(maxEntries);
- PartialList<Metadata> ruleMetadatas = rulesService.getRuleMetadatas(query);
- if (ruleMetadatas.getList().size() != ruleMetadatas.getTotalSize()) {
- System.out.println("WARNING : Only the first " + ruleMetadatas.getPageSize() + " rules have been retrieved, there are " + ruleMetadatas.getTotalSize() + " rules registered in total. Use the maxEntries parameter to retrieve more rules");
- }
- Map<String,RuleStatistics> allRuleStatistics = rulesService.getAllRuleStatistics();
-
- DataTable dataTable = new DataTable();
- for (Metadata ruleMetadata : ruleMetadatas.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- String ruleId = ruleMetadata.getId();
- rowData.add(ruleMetadata.isEnabled() ? "x" : "");
- rowData.add(ruleMetadata.isHidden() ? "x" : "");
- rowData.add(ruleMetadata.isReadOnly() ? "x" : "");
- rowData.add(ruleId);
- rowData.add(ruleMetadata.getScope());
- rowData.add(ruleMetadata.getName());
- rowData.add(StringUtils.join(ruleMetadata.getTags(), ","));
- rowData.add(StringUtils.join(ruleMetadata.getSystemTags(), ","));
- RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId);
- if (ruleStatistics != null) {
- rowData.add(ruleStatistics.getExecutionCount());
- rowData.add(ruleStatistics.getConditionsTime());
- rowData.add(ruleStatistics.getActionsTime());
- } else {
- rowData.add(0L);
- rowData.add(0L);
- rowData.add(0L);
- }
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
- dataTable.sort(new DataTable.SortCriteria(9, DataTable.SortOrder.DESCENDING),
- new DataTable.SortCriteria(10, DataTable.SortOrder.DESCENDING),
- new DataTable.SortCriteria(5, DataTable.SortOrder.ASCENDING));
- return dataTable;
- }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java
deleted file mode 100644
index 5f8395137..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.services.SegmentService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "segment-list", description = "This will list all the segments present in the Apache Unomi Context Server")
-@Service
-public class SegmentList extends ListCommandSupport {
-
- @Reference
- SegmentService segmentService;
-
- @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- @Override
- protected String[] getHeaders() {
- return new String[] {
- "Enabled",
- "Hidden",
- "Id",
- "Scope",
- "Name",
- "System tags"
- };
- }
-
- @Override
- protected DataTable buildDataTable() {
- PartialList<Metadata> segmentMetadatas = segmentService.getSegmentMetadatas(0, maxEntries, null);
-
- DataTable dataTable = new DataTable();
- for (Metadata metadata : segmentMetadatas.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(metadata.isEnabled() ? "x" : "");
- rowData.add(metadata.isHidden() ? "x" : "");
- rowData.add(metadata.getId());
- rowData.add(metadata.getScope());
- rowData.add(metadata.getName());
- rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
-
- dataTable.sort(new DataTable.SortCriteria(4, DataTable.SortOrder.ASCENDING));
- return dataTable;
- }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java
deleted file mode 100644
index 14b4efdf1..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.segments.DependentMetadata;
-import org.apache.unomi.api.services.SegmentService;
-
-@Command(scope = "unomi", name = "segment-remove", description = "Remove segments in the Apache Unomi Context Server")
-@Service
-public class SegmentRemove implements Action {
-
- @Reference
- SegmentService segmentService;
-
- @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false)
- String segmentIdentifier;
-
- @Argument(index = 1, name = "validate", description = "Check if the segment is used in goals or other segments", required = false, multiValued = false)
- Boolean validate = true;
-
-
- public Object execute() throws Exception {
- DependentMetadata dependantMetadata = segmentService.removeSegmentDefinition(segmentIdentifier, validate);
- if (!validate || (dependantMetadata.getSegments().isEmpty() && dependantMetadata.getScorings().isEmpty())) {
- System.out.println("Segment " + segmentIdentifier + " successfully deleted");
- } else if (validate) {
- System.out.print("Segment " + segmentIdentifier + " could not be deleted because of the following dependents:");
- if (!dependantMetadata.getScorings().isEmpty()) {
- System.out.print(" scoring:" + dependantMetadata.getScorings());
- }
- if (!dependantMetadata.getSegments().isEmpty()) {
- System.out.println(" segments:" + dependantMetadata.getSegments());
- }
- }
- return null;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java
deleted file mode 100644
index 9016aa9ed..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.api.services.SegmentService;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-@Command(scope = "unomi", name = "segment-view", description = "This will allows to view a segment in the Apache Unomi Context Server")
-@Service
-public class SegmentView implements Action {
-
- @Reference
- SegmentService segmentService;
-
- @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false)
- String segmentIdentifier;
-
- public Object execute() throws Exception {
- Segment segment = segmentService.getSegmentDefinition(segmentIdentifier);
- if (segment == null) {
- System.out.println("Couldn't find a segment with id=" + segmentIdentifier);
- return null;
- }
- String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(segment);
- System.out.println(jsonRule);
- return null;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java
deleted file mode 100644
index a3d0fda98..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Argument;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.Session;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.common.DataTable;
-
-import java.util.ArrayList;
-
-@Command(scope = "unomi", name = "session-list", description = "This commands lists the latest sessions updated in the Apache Unomi Context Server")
-@Service
-public class SessionList extends ListCommandSupport {
-
- @Reference
- ProfileService profileService;
-
- @Reference
- DefinitionsService definitionsService;
-
- @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
- int maxEntries = 100;
-
- @java.lang.Override
- protected String[] getHeaders() {
- return new String[] {
- "ID",
- "Scope",
- "Last event",
- "Duration",
- "Profile",
- "Timestamp"
- };
- }
-
- @java.lang.Override
- protected DataTable buildDataTable() {
- Query query = new Query();
- query.setSortby("lastEventDate:desc");
- query.setLimit(maxEntries);
- Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
- query.setCondition(matchAllCondition);
- PartialList<Session> lastModifiedProfiles = profileService.searchSessions(query);
- DataTable dataTable = new DataTable();
- for (Session session : lastModifiedProfiles.getList()) {
- ArrayList<Comparable> rowData = new ArrayList<>();
- rowData.add(session.getItemId());
- rowData.add(session.getScope());
- rowData.add(session.getLastEventDate());
- rowData.add(session.getDuration());
- rowData.add(session.getProfileId());
- rowData.add(session.getTimeStamp());
- dataTable.addRow(rowData.toArray(new Comparable[rowData.size()]));
- }
- return dataTable;
- }
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java
deleted file mode 100644
index e672bff14..000000000
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.shell.commands;
-
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Patch;
-import org.apache.unomi.api.PersonaWithSessions;
-import org.apache.unomi.api.PropertyType;
-import org.apache.unomi.api.actions.ActionType;
-import org.apache.unomi.api.campaigns.Campaign;
-import org.apache.unomi.api.conditions.ConditionType;
-import org.apache.unomi.api.goals.Goal;
-import org.apache.unomi.api.rules.Rule;
-import org.apache.unomi.api.segments.Scoring;
-import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-
-import java.io.IOException;
-import java.net.URL;
-
-@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles")
-@Service
-public class UndeployDefinition extends DeploymentCommandSupport {
-
- public void processDefinition(String definitionType, URL definitionURL) {
- try {
- if (ALL_OPTION_LABEL.equals(definitionType)) {
- String definitionURLString = definitionURL.toString();
- for (String possibleDefinitionType : definitionTypes) {
- if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
- definitionType = possibleDefinitionType;
- break;
- }
- }
- if (ALL_OPTION_LABEL.equals(definitionType)) {
- System.out.println("Couldn't resolve definition type for definition URL " + definitionURL);
- return;
- }
- }
- boolean successful = true;
- switch (definitionType) {
- case CONDITION_DEFINITION_TYPE:
- ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class);
- definitionsService.removeActionType(conditionType.getItemId());
- break;
- case ACTION_DEFINITION_TYPE:
- ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class);
- definitionsService.removeActionType(actionType.getItemId());
- break;
- case GOAL_DEFINITION_TYPE:
- Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class);
- goalsService.removeGoal(goal.getItemId());
- break;
- case CAMPAIGN_DEFINITION_TYPE:
- Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class);
- goalsService.removeCampaign(campaign.getItemId());
- break;
- case PERSONA_DEFINITION_TYPE:
- PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class);
- profileService.delete(persona.getPersona().getItemId(), true);
- break;
- case PROPERTY_DEFINITION_TYPE:
- PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class);
- profileService.deletePropertyType(propertyType.getItemId());
- break;
- case RULE_DEFINITION_TYPE:
- Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class);
- rulesService.removeRule(rule.getItemId());
- break;
- case SEGMENT_DEFINITION_TYPE:
- Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class);
- segmentService.removeSegmentDefinition(segment.getItemId(), false);
- break;
- case SCORING_DEFINITION_TYPE:
- Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class);
- segmentService.removeScoringDefinition(scoring.getItemId(), false);
- break;
- case PATCH_DEFINITION_TYPE:
- Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class);
- // patchService.patch(patch);
- break;
- default:
- System.out.println("Unrecognized definition type: " + definitionType);
- successful = false;
- break;
- }
- if (successful) {
- System.out.println("Predefined definition unregistered : " + definitionURL.getFile());
- }
- } catch (IOException e) {
- System.out.println("Error while removing definition " + definitionURL);
- System.out.println(e.getMessage());
- }
- }
-
-}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
new file mode 100644
index 000000000..67b00d159
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
@@ -0,0 +1,609 @@
+/*
+ * 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.shell.dev.actions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.karaf.shell.api.action.*;
+import org.apache.karaf.shell.api.action.lifecycle.Init;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import org.apache.unomi.shell.dev.completers.IdCompleter;
+import org.apache.unomi.shell.dev.completers.OperationCompleter;
+import org.apache.unomi.shell.dev.completers.TypeCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+@Command(scope = "unomi", name = "crud", description = "Perform CRUD operations on Unomi objects")
+@Service
+public class UnomiCrudCommand implements Action {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(UnomiCrudCommand.class.getName());
+
+ private static final ObjectMapper OBJECT_MAPPER = CustomObjectMapper.getObjectMapper();
+
+ @Reference
+ private BundleContext bundleContext;
+
+ @Reference
+ private Session session;
+
+ @Argument(index = 0, name = "operation", description = "Operation to perform (create/read/update/delete/list/help)", required = true)
+ @Completion(OperationCompleter.class)
+ private String operation;
+
+ @Argument(index = 1, name = "type", description = "Object type", required = true)
+ @Completion(TypeCompleter.class)
+ private String type;
+
+ // Multi-valued argument that captures all remaining tokens after type
+ // ⚠️ IMPORTANT: Only the last argument (highest index) can be multi-valued in Karaf
+ // Since this is at index 2 (last argument), it can safely be multi-valued
+ // For create: remaining[0] = JSON/URL
+ // For read/delete: remaining[0] = ID
+ // For update: remaining[0] = ID, remaining[1] = JSON/URL
+ // For list: remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing
+ @Argument(index = 2, name = "remaining", description = "ID/JSON/URL (for create/read/update/delete) or remaining tokens (for list)", required = false, multiValued = true)
+ @Completion(IdCompleter.class) // Could be enhanced to detect context
+ private List<String> remaining;
+
+ // Option fields for list operation (populated via manual parsing from remaining)
+ @Option(name = "--csv", description = "Output list in CSV format", required = false, multiValued = false)
+ private boolean csv;
+
+ @Option(name = "-n", aliases = "--max-entries", description = "Maximum number of entries to list", required = false)
+ private Integer maxEntries;
+
+ @Init
+ public void init() {
+ LOGGER.debug("UnomiCrudCommand init");
+ }
+
+ /**
+ * Check if a token is a max-entries option flag.
+ *
+ * @param token the token to check
+ * @return true if the token is -n or --max-entries
+ */
+ private boolean isMaxEntriesOption(String token) {
+ return "-n".equals(token) || "--max-entries".equals(token);
+ }
+
+ /**
+ * Parse max-entries option value from the remaining list.
+ * Validates that the value is a positive integer.
+ *
+ * @param remaining the remaining argument list
+ * @param index the index of the option flag
+ * @return the parsed integer value (must be > 0), or null if invalid/missing
+ */
+ private Integer parseMaxEntriesValue(List<String> remaining, int index) {
+ if (index + 1 >= remaining.size()) {
+ return null;
+ }
+ try {
+ int value = Integer.parseInt(remaining.get(index + 1));
+ // Only accept positive values
+ if (value <= 0) {
+ LOGGER.warn("Invalid max-entries value (must be positive): " + value);
+ return null;
+ }
+ return value;
+ } catch (NumberFormatException e) {
+ LOGGER.warn("Invalid number for max-entries option: " + remaining.get(index + 1));
+ return null;
+ }
+ }
+
+ /**
+ * Parse list-specific options from the remaining argument list.
+ * This implements Option 1: Simple Manual Parsing from the redesign proposal.
+ *
+ * Note: If --csv was already set by Karaf's option parser (when placed before arguments),
+ * we preserve that value. Otherwise, we parse it from the remaining list.
+ *
+ * @param remaining List of remaining tokens after type (e.g., ["--csv", "-n", "50"])
+ */
+ private void parseListOptions(List<String> remaining) {
+ // Preserve csv value if already set by Karaf's option parser (when --csv comes before arguments)
+ boolean csv = this.csv;
+ Integer maxEntries = this.maxEntries;
+
+ if (remaining == null || remaining.isEmpty()) {
+ // Keep existing values if already set by Karaf
+ return;
+ }
+
+ for (int i = 0; i < remaining.size(); i++) {
+ String token = remaining.get(i);
+
+ if ("--csv".equals(token)) {
+ csv = true;
+ } else if (isMaxEntriesOption(token)) {
+ Integer value = parseMaxEntriesValue(remaining, i);
+ if (value != null) {
+ maxEntries = value;
+ i++; // Skip the next token as it's the value
+ }
+ }
+ // Ignore unknown tokens (could log warning)
+ }
+
+ // Populate option fields
+ this.csv = csv;
+ this.maxEntries = maxEntries;
+ }
+
+ /**
+ * Check if remaining argument list has at least the specified number of non-empty elements.
+ *
+ * @param remaining the remaining argument list
+ * @param minSize minimum number of elements required
+ * @return true if valid, false otherwise
+ */
+ private boolean hasMinimumRemainingArgs(List<String> remaining, int minSize) {
+ if (remaining == null || remaining.size() < minSize) {
+ return false;
+ }
+ for (int i = 0; i < minSize; i++) {
+ if (StringUtils.isBlank(remaining.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parse JSON properties from remaining argument or URL with error handling.
+ * This method wraps parseProperties() and handles exceptions, providing consistent error messages.
+ *
+ * @param jsonOrUrl JSON string or URL from remaining argument
+ * @param console Console for error output
+ * @return Map of properties, or null if parsing failed or error occurred
+ */
+ private Map<String, Object> parsePropertiesWithErrorHandling(String jsonOrUrl, PrintStream console) {
+ final String errorMsg = "Error: Failed to parse JSON or URL: " + jsonOrUrl;
+ try {
+ Map<String, Object> props = parseProperties(jsonOrUrl);
+ if (props == null) {
+ console.println(errorMsg);
+ }
+ return props;
+ } catch (Exception e) {
+ console.println(errorMsg);
+ console.println("Error details: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Strip surrounding quotes from a string if present.
+ *
+ * @param str the string to process
+ * @return the string with quotes removed, or original if no quotes
+ */
+ private String stripQuotes(String str) {
+ if (StringUtils.isEmpty(str) || str.length() < 2) {
+ return str;
+ }
+ // Check for single quotes
+ if (str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'') {
+ return str.substring(1, str.length() - 1);
+ }
+ // Check for double quotes
+ if (str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') {
+ return str.substring(1, str.length() - 1);
+ }
+ return str;
+ }
+
+ /**
+ * Check if a string is a valid URL by attempting to parse it as a URI.
+ * This method supports all URL schemes that Pax URL supports:
+ * - file:// (file protocol)
+ * - http://, https:// (HTTP/HTTPS protocols)
+ * - mvn: (Maven protocol)
+ * - war: (War protocol)
+ * - Any other valid URI scheme
+ *
+ * The method uses Java's URI class to validate the scheme, which is more
+ * robust than simple string matching and supports all standard and custom schemes.
+ *
+ * @param str the string to check
+ * @return true if the string is a valid URI with a scheme, false otherwise
+ */
+ private boolean isUrl(String str) {
+ if (StringUtils.isBlank(str)) {
+ return false;
+ }
+
+ // JSON strings typically start with { or [, so they're not URLs
+ String trimmed = str.trim();
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+ return false;
+ }
+
+ try {
+ URI uri = new URI(trimmed);
+ // A valid URI with a scheme is considered a URL
+ // getScheme() returns null for relative URIs, which are not URLs
+ return uri.getScheme() != null;
+ } catch (URISyntaxException e) {
+ // Not a valid URI, so not a URL
+ return false;
+ }
+ }
+
+ /**
+ * Parse JSON from a file URL.
+ *
+ * @param fileUrl the file:// URL
+ * @return the parsed JSON as a Map
+ * @throws Exception if there's an error reading or parsing the file
+ */
+ private Map<String, Object> parseFileUrl(String fileUrl) throws Exception {
+ URI uri = new URI(fileUrl);
+ String scheme = uri.getScheme();
+
+ if (!"file".equals(scheme)) {
+ throw new IllegalArgumentException("Expected file:// URL, got: " + fileUrl);
+ }
+
+ // Handle file:// URLs - getPath() handles both file:///path and file://path
+ String filePath = uri.getPath();
+ if (filePath == null || filePath.isEmpty()) {
+ throw new IllegalArgumentException("Invalid file URL: " + fileUrl);
+ }
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> result = OBJECT_MAPPER.readValue(Files.readString(Paths.get(filePath)), Map.class);
+ return result;
+ }
+
+ /**
+ * Parse JSON properties from remaining argument or URL.
+ * Supports:
+ * - Inline JSON string: {"itemId":"test"} (quoted or unquoted)
+ * - File URL: file:///path/to/file.json
+ * - HTTP/HTTPS URL: http://example.com/data.json (not yet implemented)
+ * - Maven URL: mvn:groupId/artifactId/version (not yet implemented)
+ * - War URL: war:file://path/to.war (not yet implemented)
+ * - Any other Pax URL supported scheme (not yet implemented)
+ *
+ * Note: If JSON is quoted in the command (e.g., '{"itemId":"test"}'),
+ * the Gogo parser will strip the quotes before passing to this method.
+ * This method handles both quoted and unquoted JSON strings.
+ *
+ * @param jsonOrUrl JSON string or URL from remaining argument
+ * @return Map of properties, or null if invalid
+ * @throws Exception if there's an error parsing the JSON or reading the URL
+ */
+ private Map<String, Object> parseProperties(String jsonOrUrl) throws Exception {
+ if (StringUtils.isBlank(jsonOrUrl)) {
+ return null;
+ }
+
+ String trimmed = stripQuotes(StringUtils.trim(jsonOrUrl));
+
+ if (isUrl(trimmed)) {
+ URI uri = new URI(trimmed);
+ String scheme = uri.getScheme();
+
+ if ("file".equals(scheme)) {
+ return parseFileUrl(trimmed);
+ } else {
+ // Other URL schemes (http, https, mvn, war, etc.) are not yet supported
+ // In the future, we could use Pax URL's URLStreamHandler to resolve these
+ throw new UnsupportedOperationException(
+ "URL scheme '" + scheme + "' is not yet supported. " +
+ "Currently only file:// URLs are supported. Use file:// or inline JSON.");
+ }
+ }
+
+ // Treat as inline JSON
+ @SuppressWarnings("unchecked")
+ Map<String, Object> result = OBJECT_MAPPER.readValue(trimmed, Map.class);
+ return result;
+ }
+
+ /**
+ * Validate that operation and type are provided.
+ *
+ * @param console console for error output
+ * @return true if valid, false otherwise
+ */
+ private boolean validateOperationAndType(PrintStream console) {
+ if (StringUtils.isBlank(operation)) {
+ console.println("Error: Operation is required");
+ console.println("Usage: unomi:crud <operation> <type> [remaining...]");
+ console.println("Available operations: create, read, update, delete, list, help");
+ return false;
+ }
+
+ if (StringUtils.isBlank(type)) {
+ console.println("Error: Type is required");
+ console.println("Usage: unomi:crud <operation> <type> [remaining...]");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Execute the appropriate handler for the given operation.
+ *
+ * @param cmd the CrudCommand instance
+ * @param operationLower the lowercase operation name
+ * @param console console for output
+ * @return the result of the operation
+ * @throws Exception if the operation fails
+ */
+ private Object executeOperation(CrudCommand cmd, String operationLower, PrintStream console) throws Exception {
+ switch (operationLower) {
+ case "create":
+ return handleCreate(cmd, console);
+
+ case "read":
+ return handleRead(cmd, console);
+
+ case "update":
+ return handleUpdate(cmd, console);
+
+ case "delete":
+ return handleDelete(cmd, console);
+
+ case "list":
+ return handleList(cmd, console);
+
+ case "help":
+ console.println("Properties for " + type + ":");
+ console.println(cmd.getPropertiesHelp());
+ return null;
+
+ default:
+ console.println("Unknown operation: " + operation);
+ console.println("Available operations: create, read, update, delete, list, help");
+ return null;
+ }
+ }
+
+ /**
+ * Find and execute the CrudCommand for the given type.
+ *
+ * @param console console for output
+ * @return true if a handler was found and executed, false otherwise
+ * @throws Exception if the operation fails
+ */
+ private boolean findAndExecuteCommand(PrintStream console) throws Exception {
+ ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+ if (refs == null) {
+ return false;
+ }
+
+ String operationLower = operation.toLowerCase();
+ for (ServiceReference<?> ref : refs) {
+ CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+ if (cmd == null) continue;
+ try {
+ if (cmd.getObjectType().equals(type)) {
+ executeOperation(cmd, operationLower, console);
+ return true;
+ }
+ } finally {
+ bundleContext.ungetService(ref);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Object execute() throws Exception {
+ PrintStream console = session.getConsole();
+
+ if (!validateOperationAndType(console)) {
+ return null;
+ }
+
+ boolean handlerFound = findAndExecuteCommand(console);
+ if (!handlerFound) {
+ console.println("No handler found for object type: " + type);
+ }
+ return null;
+ }
+
+ /**
+ * Validate that remaining argument list has at least one non-empty element for create operation.
+ *
+ * @param remaining the remaining argument list
+ * @param console console for error output
+ * @return true if valid, false otherwise
+ */
+ private boolean validateCreateRemaining(List<String> remaining, PrintStream console) {
+ if (!hasMinimumRemainingArgs(remaining, 1)) {
+ console.println("Error: JSON string or URL is required for create operation");
+ console.println("Usage: unomi:crud create <type> <json|url>");
+ console.println("Example: unomi:crud create goal '{\"itemId\":\"test\",\"enabled\":true}'");
+ console.println("Example: unomi:crud create goal file:///path/to/file.json");
+ console.println("Note: Quote JSON strings to ensure they're treated as a single argument");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle create operation.
+ * Syntax: unomi:crud create <type> <json|url>
+ * remaining[0] = JSON string or URL
+ */
+ private Object handleCreate(CrudCommand cmd, PrintStream console) throws Exception {
+ if (!validateCreateRemaining(remaining, console)) {
+ return null;
+ }
+
+ String jsonOrUrl = remaining.get(0);
+ Map<String, Object> createProps = parsePropertiesWithErrorHandling(jsonOrUrl, console);
+ if (createProps == null) {
+ return null;
+ }
+
+ // Validate that we have at least some properties (empty JSON {} is not valid)
+ if (createProps.isEmpty()) {
+ console.println("Error: Empty JSON object is not valid. Please provide required properties.");
+ console.println("Usage: unomi:crud create <type> <json|url>");
+ return null;
+ }
+
+ String newId = cmd.create(createProps);
+ if (newId == null) {
+ console.println("Error: Failed to create " + type + ". The create operation returned null.");
+ return null;
+ }
+ console.println("Created " + type + " with ID: " + newId);
+ return null;
+ }
+
+ /**
+ * Validate that remaining argument list has at least one non-empty element.
+ *
+ * @param remaining the remaining argument list
+ * @param operation the operation name (for error messages)
+ * @param console console for error output
+ * @return true if valid, false otherwise
+ */
+ private boolean validateRemainingNotEmpty(List<String> remaining, String operation, PrintStream console) {
+ if (!hasMinimumRemainingArgs(remaining, 1)) {
+ console.println("Error: ID is required for " + operation + " operation");
+ console.println("Usage: unomi:crud " + operation + " <type> <id>");
+ console.println("Example: unomi:crud " + operation + " goal test-goal-123");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle read operation.
+ * Syntax: unomi:crud read <type> <id>
+ * remaining[0] = Object ID
+ */
+ private Object handleRead(CrudCommand cmd, PrintStream console) throws Exception {
+ if (!validateRemainingNotEmpty(remaining, "read", console)) {
+ return null;
+ }
+
+ String id = remaining.get(0);
+ Map<String, Object> obj = cmd.read(id);
+ if (obj != null) {
+ console.println(OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj));
+ } else {
+ console.println(type + " not found with ID: " + id);
+ }
+ return null;
+ }
+
+ /**
+ * Handle update operation.
+ * Syntax: unomi:crud update <type> <id> <json|url>
+ * remaining[0] = Object ID
+ * remaining[1] = JSON string or URL
+ */
+ private Object handleUpdate(CrudCommand cmd, PrintStream console) throws Exception {
+ if (!hasMinimumRemainingArgs(remaining, 2)) {
+ console.println("Error: ID and JSON/URL are required for update operation");
+ console.println("Usage: unomi:crud update <type> <id> <json|url>");
+ console.println("Example: unomi:crud update goal test-goal-123 '{\"itemId\":\"test-goal-123\",\"enabled\":false}'");
+ console.println("Note: Quote JSON strings to ensure they're treated as a single argument");
+ return null;
+ }
+
+ String id = remaining.get(0);
+ String jsonOrUrl = remaining.get(1);
+
+ // hasMinimumRemainingArgs already ensures both id and jsonOrUrl are non-blank
+ Map<String, Object> updateProps = parsePropertiesWithErrorHandling(jsonOrUrl, console);
+ if (updateProps == null) {
+ return null;
+ }
+
+ cmd.update(id, updateProps);
+ console.println("Updated " + type + " with ID: " + id);
+ return null;
+ }
+
+ /**
+ * Handle delete operation.
+ * Syntax: unomi:crud delete <type> <id>
+ * remaining[0] = Object ID
+ */
+ private Object handleDelete(CrudCommand cmd, PrintStream console) throws Exception {
+ if (!validateRemainingNotEmpty(remaining, "delete", console)) {
+ return null;
+ }
+
+ String id = remaining.get(0);
+ cmd.delete(id);
+ console.println("Deleted " + type + " with ID: " + id);
+ return null;
+ }
+
+ /**
+ * Handle list operation.
+ * Syntax: unomi:crud list <type> [--csv] [-n <number>]
+ * remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing
+ */
+ private Object handleList(CrudCommand cmd, PrintStream console) throws Exception {
+ // Parse list-specific options from remaining argument
+ parseListOptions(remaining);
+
+ String[] headers = cmd.getHeaders();
+ if (headers == null || headers.length == 0) {
+ console.println("Error: No headers available for " + type);
+ return null;
+ }
+
+ // Ensure limit is positive (default to 100 if null or invalid)
+ int limit = (maxEntries != null && maxEntries > 0) ? maxEntries : 100;
+
+ if (csv) {
+ // Generate proper CSV output using Apache Commons CSV
+ cmd.buildCsvOutput(console, headers, limit);
+ } else {
+ // Generate table output
+ ShellTable table = new ShellTable();
+ for (String header : headers) {
+ table.column(header);
+ }
+ cmd.buildRows(table, limit);
+ table.print(console, true);
+ }
+ return null;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java
new file mode 100644
index 000000000..a968aaff1
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java
@@ -0,0 +1,53 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+/**
+ * Base class for Unomi shell commands
+ */
+public abstract class BaseCommand implements Action {
+ protected static final int DEFAULT_ENTRIES = 100;
+
+ @Reference
+ protected PersistenceService persistenceService;
+
+ protected ShellTable buildTable() {
+ ShellTable table = new ShellTable();
+ table.column("ID");
+ table.column("Name");
+ table.column("Description");
+ return table;
+ }
+
+ protected void printTable(ShellTable table, Session session) {
+ table.print(session.getConsole());
+ }
+
+ protected boolean confirm(Session session, String message) throws IOException {
+ String response = session.readLine(message + " (y/n): ", null);
+ return response != null && response.toLowerCase().startsWith("y");
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java
new file mode 100644
index 000000000..f40b51a35
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java
@@ -0,0 +1,74 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+
+/**
+ * Base class for list commands
+ */
+public abstract class BaseListCommand<T extends Item> extends BaseCommand {
+
+ @Reference
+ protected DefinitionsService definitionsService;
+
+ @Reference
+ protected Session session;
+
+ @Option(name = "--max-entries", description = "Maximum number of entries to display", required = false)
+ protected int maxEntries = DEFAULT_ENTRIES;
+
+ @Option(name = "--sort-by", description = "Sort by field name", required = false)
+ protected String sortBy;
+
+ protected abstract Class<T> getItemType();
+
+ protected abstract void printItem(ShellTable table, T item);
+
+ @Override
+ public Object execute() throws Exception {
+ Query query = new Query();
+ query.setLimit(maxEntries);
+ query.setSortby(sortBy);
+
+ Condition condition = new Condition();
+ condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(condition);
+
+ PartialList<T> items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit());
+
+ ShellTable table = buildTable();
+ for (T item : items.getList()) {
+ printItem(table, item);
+ }
+ printTable(table, session);
+
+ return null;
+ }
+
+ public void setDefinitionsService(DefinitionsService definitionsService) {
+ this.definitionsService = definitionsService;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java
new file mode 100644
index 000000000..1ef8ee99b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java
@@ -0,0 +1,61 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
+
+import java.io.PrintStream;
+
+/**
+ * Base class for simple shell commands that provides common functionality
+ * for accessing Session and console output.
+ */
+public abstract class BaseSimpleCommand implements Action {
+
+ @Reference
+ protected Session session;
+
+ /**
+ * Get the console PrintStream from the session.
+ *
+ * @return the console PrintStream
+ */
+ protected PrintStream getConsole() {
+ return session.getConsole();
+ }
+
+ /**
+ * Print a message to the console.
+ *
+ * @param message the message to print
+ */
+ protected void println(String message) {
+ getConsole().println(message);
+ }
+
+ /**
+ * Print a formatted message to the console.
+ *
+ * @param format the format string
+ * @param args the arguments
+ */
+ protected void printf(String format, Object... args) {
+ getConsole().printf(format, args);
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java
new file mode 100644
index 000000000..44dfcbf82
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java
@@ -0,0 +1,80 @@
+/*
+ * 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.shell.dev.commands;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Utility class for common command functionality.
+ */
+public final class CommandUtils {
+
+ /**
+ * Standard date format used across commands: "yyyy-MM-dd HH:mm:ss"
+ */
+ public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+ /**
+ * Thread-local SimpleDateFormat instance for date formatting.
+ * SimpleDateFormat is not thread-safe, so we use ThreadLocal to ensure thread safety.
+ */
+ private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
+ ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_FORMAT_PATTERN));
+
+ private CommandUtils() {
+ // Utility class - prevent instantiation
+ }
+
+ /**
+ * Format a date using the standard date format pattern.
+ *
+ * @param date the date to format
+ * @return the formatted date string, or "-" if date is null
+ */
+ public static String formatDate(Date date) {
+ if (date == null) {
+ return "-";
+ }
+ return DATE_FORMAT.get().format(date);
+ }
+
+ /**
+ * Format a date using the standard date format pattern.
+ *
+ * @param date the date to format
+ * @param nullValue the value to return if date is null
+ * @return the formatted date string, or nullValue if date is null
+ */
+ public static String formatDate(Date date, String nullValue) {
+ if (date == null) {
+ return nullValue;
+ }
+ return DATE_FORMAT.get().format(date);
+ }
+
+ /**
+ * Get a SimpleDateFormat instance for the standard pattern.
+ * Note: This returns a new instance each time. For thread-safe usage,
+ * prefer using formatDate() methods.
+ *
+ * @return a SimpleDateFormat instance
+ */
+ public static SimpleDateFormat getDateFormat() {
+ return new SimpleDateFormat(DATE_FORMAT_PATTERN);
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java
new file mode 100644
index 000000000..243e04e51
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java
@@ -0,0 +1,137 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.unomi.api.Patch;
+import org.apache.unomi.api.PersonaWithSessions;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.segments.Segment;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URL;
+
+@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles")
+@Service
+public class DeployDefinition extends DeploymentCommandSupport {
+
+ public void processDefinition(String definitionType, URL definitionURL) {
+ try {
+ processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition registered");
+ } catch (IOException e) {
+ handleDefinitionError(definitionURL, "saving", e);
+ }
+ }
+
+ protected void deployConditionType(URL definitionURL) throws IOException {
+ ConditionType conditionType = readDefinition(definitionURL, ConditionType.class);
+ definitionsService.setConditionType(conditionType);
+ }
+
+ protected void deployActionType(URL definitionURL) throws IOException {
+ ActionType actionType = readDefinition(definitionURL, ActionType.class);
+ definitionsService.setActionType(actionType);
+ }
+
+ protected void deployGoal(URL definitionURL) throws IOException {
+ Goal goal = readDefinition(definitionURL, Goal.class);
+ goalsService.setGoal(goal);
+ }
+
+ protected void deployCampaign(URL definitionURL) throws IOException {
+ Campaign campaign = readDefinition(definitionURL, Campaign.class);
+ goalsService.setCampaign(campaign);
+ }
+
+ protected void deployPersona(URL definitionURL) throws IOException {
+ PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class);
+ profileService.savePersonaWithSessions(persona);
+ }
+
+ protected void deployPropertyType(URL definitionURL) throws IOException {
+ PropertyType propertyType = readDefinition(definitionURL, PropertyType.class);
+ profileService.setPropertyTypeTarget(definitionURL, propertyType);
+ profileService.setPropertyType(propertyType);
+ }
+
+ protected void deployRule(URL definitionURL) throws IOException {
+ Rule rule = readDefinition(definitionURL, Rule.class);
+ rulesService.setRule(rule);
+ }
+
+ protected void deploySegment(URL definitionURL) throws IOException {
+ Segment segment = readDefinition(definitionURL, Segment.class);
+ segmentService.setSegmentDefinition(segment);
+ }
+
+ protected void deployScoring(URL definitionURL) throws IOException {
+ Scoring scoring = readDefinition(definitionURL, Scoring.class);
+ segmentService.setScoringDefinition(scoring);
+ }
+
+ protected void deployPatch(URL definitionURL) throws IOException {
+ Patch patch = readDefinition(definitionURL, Patch.class);
+ patchService.patch(patch);
+ }
+
+ @Override
+ protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+ switch (definitionType) {
+ case CONDITION_DEFINITION_TYPE:
+ deployConditionType(definitionURL);
+ return true;
+ case ACTION_DEFINITION_TYPE:
+ deployActionType(definitionURL);
+ return true;
+ case GOAL_DEFINITION_TYPE:
+ deployGoal(definitionURL);
+ return true;
+ case CAMPAIGN_DEFINITION_TYPE:
+ deployCampaign(definitionURL);
+ return true;
+ case PERSONA_DEFINITION_TYPE:
+ deployPersona(definitionURL);
+ return true;
+ case PROPERTY_DEFINITION_TYPE:
+ deployPropertyType(definitionURL);
+ return true;
+ case RULE_DEFINITION_TYPE:
+ deployRule(definitionURL);
+ return true;
+ case SEGMENT_DEFINITION_TYPE:
+ deploySegment(definitionURL);
+ return true;
+ case SCORING_DEFINITION_TYPE:
+ deployScoring(definitionURL);
+ return true;
+ case PATCH_DEFINITION_TYPE:
+ deployPatch(definitionURL);
+ return true;
+ default:
+ console.println("Unrecognized definition type:" + definitionType);
+ return false;
+ }
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
similarity index 65%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
index 56db909ab..5081e3f26 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java
@@ -14,14 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.commons.lang3.StringUtils;
import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.Argument;
import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.console.Session;
+
+import java.io.PrintStream;
import org.apache.unomi.api.services.*;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
import org.jline.reader.LineReader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
@@ -33,7 +36,7 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
-public abstract class DeploymentCommandSupport implements Action {
+public abstract class DeploymentCommandSupport extends BaseSimpleCommand {
public static final String ALL_OPTION_LABEL = "* (All)";
@Reference
@@ -57,8 +60,6 @@ public abstract class DeploymentCommandSupport implements Action {
@Reference
BundleContext bundleContext;
- @Reference
- Session session;
public static final String CONDITION_DEFINITION_TYPE = "conditions";
public static final String ACTION_DEFINITION_TYPE = "actions";
@@ -102,6 +103,74 @@ public abstract class DeploymentCommandSupport implements Action {
public abstract void processDefinition(String definitionType, URL definitionURL);
+ /**
+ * Process definition by type. Override in subclasses to provide specific implementation.
+ *
+ * @param definitionType the type of definition
+ * @param definitionURL the URL of the definition
+ * @param console the console for output
+ * @return true if successful, false otherwise
+ * @throws IOException if there's an error reading the definition
+ */
+ protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+ return false;
+ }
+
+ /**
+ * Read a definition object from a URL using CustomObjectMapper.
+ *
+ * @param definitionURL the URL to read from
+ * @param clazz the class of the object to read
+ * @param <T> the type of the object
+ * @return the read object
+ * @throws IOException if there's an error reading the definition
+ */
+ protected <T> T readDefinition(URL definitionURL, Class<T> clazz) throws IOException {
+ return CustomObjectMapper.getObjectMapper().readValue(definitionURL, clazz);
+ }
+
+ /**
+ * Handle errors that occur during definition processing.
+ *
+ * @param definitionURL the URL of the definition that caused the error
+ * @param operation the operation being performed (e.g., "saving", "removing")
+ * @param e the exception that occurred
+ */
+ protected void handleDefinitionError(URL definitionURL, String operation, IOException e) {
+ PrintStream console = getConsole();
+ console.println("Error while " + operation + " definition " + definitionURL);
+ console.println(e.getMessage());
+ }
+
+ /**
+ * Internal method to process a definition. Handles common logic like resolving definition type.
+ *
+ * @param definitionType the type of definition
+ * @param definitionURL the URL of the definition
+ * @param console the console for output
+ * @param successMessage the message to display on success
+ * @throws IOException if there's an error processing the definition
+ */
+ protected void processDefinitionInternal(String definitionType, URL definitionURL, PrintStream console, String successMessage) throws IOException {
+ if (ALL_OPTION_LABEL.equals(definitionType)) {
+ String definitionURLString = definitionURL.toString();
+ for (String possibleDefinitionType : definitionTypes) {
+ if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) {
+ definitionType = possibleDefinitionType;
+ break;
+ }
+ }
+ if (ALL_OPTION_LABEL.equals(definitionType)) {
+ console.println("Couldn't resolve definition type for definition URL " + definitionURL);
+ return;
+ }
+ }
+ boolean successful = processDefinitionByType(definitionType, definitionURL, console);
+ if (successful) {
+ console.println(successMessage + " : " + definitionURL.getFile());
+ }
+ }
+
public Object execute() throws Exception {
List<Bundle> bundlesToUpdate;
if ("*".equals(definitionType)) {
@@ -127,17 +196,23 @@ public abstract class DeploymentCommandSupport implements Action {
String bundleAnswer = askUserWithAuthorizedAnswer(session, "Which bundle ?" + getValuesWithNumber(bundleSymbolicNames) + "\n",
IntStream.range(1,bundleSymbolicNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
- String selectedBundle = bundleSymbolicNames.get(new Integer(bundleAnswer)-1);
- if (selectedBundle.equals(ALL_OPTION_LABEL)) {
- bundlesToUpdate = bundles;
- } else {
- bundlesToUpdate = Collections.singletonList(bundles.get(new Integer(bundleAnswer) - 1));
+ try {
+ int bundleIndex = Integer.parseInt(bundleAnswer) - 1;
+ String selectedBundle = bundleSymbolicNames.get(bundleIndex);
+ if (selectedBundle.equals(ALL_OPTION_LABEL)) {
+ bundlesToUpdate = bundles;
+ } else {
+ bundlesToUpdate = Collections.singletonList(bundles.get(bundleIndex));
+ }
+ } catch (NumberFormatException e) {
+ println("Invalid selection: " + bundleAnswer);
+ return null;
}
} else {
Bundle bundle = bundleContext.getBundle(bundleIdentifier);
if (bundle == null) {
- System.out.println("Couldn't find a bundle with id: " + bundleIdentifier);
+ println("Couldn't find a bundle with id: " + bundleIdentifier);
return null;
}
@@ -146,27 +221,33 @@ public abstract class DeploymentCommandSupport implements Action {
if (definitionType == null) {
List<String> possibleDefinitionNames = definitionTypes.stream().filter((t) -> bundlesToUpdate.stream().anyMatch(b->b.findEntries(getDefinitionTypePath(t), "*.json", true) != null)).collect(Collectors.toList());
- possibleDefinitionNames.add(ALL_OPTION_LABEL);
if (possibleDefinitionNames.isEmpty()) {
- System.out.println("Couldn't find definitions in bundle : " + bundlesToUpdate);
+ println("Couldn't find definitions in bundle : " + bundlesToUpdate);
return null;
}
+ possibleDefinitionNames.add(ALL_OPTION_LABEL);
+
String definitionTypeAnswer = askUserWithAuthorizedAnswer(session, "Which kind of definition do you want to load?" + getValuesWithNumber(possibleDefinitionNames) + "\n",
IntStream.range(1,possibleDefinitionNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
- definitionType = possibleDefinitionNames.get(new Integer(definitionTypeAnswer)-1);
+ try {
+ definitionType = possibleDefinitionNames.get(Integer.parseInt(definitionTypeAnswer) - 1);
+ } catch (NumberFormatException e) {
+ println("Invalid selection: " + definitionTypeAnswer);
+ return null;
+ }
}
if (!definitionTypes.contains(definitionType) && !ALL_OPTION_LABEL.equals(definitionType)) {
- System.out.println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes);
+ println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes);
return null;
}
String definitionTypePath = getDefinitionTypePath(definitionType);
List<URL> definitionTypeURLs = bundlesToUpdate.stream().flatMap(b->b.findEntries(definitionTypePath, "*.json", true) != null ? Collections.list(b.findEntries(definitionTypePath, "*.json", true)).stream() : Stream.empty()).collect(Collectors.toList());
if (definitionTypeURLs.isEmpty()) {
- System.out.println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath);
+ println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath);
return null;
}
@@ -175,7 +256,12 @@ public abstract class DeploymentCommandSupport implements Action {
definitionTypeFileNames.add(ALL_OPTION_LABEL);
String fileNameAnswer = askUserWithAuthorizedAnswer(session, "Which file do you want to load ?" + getValuesWithNumber(definitionTypeFileNames) + "\n",
IntStream.range(1,definitionTypeFileNames.size()+1).mapToObj(Integer::toString).collect(Collectors.toList()));
- fileName = definitionTypeFileNames.get(new Integer(fileNameAnswer)-1);
+ try {
+ fileName = definitionTypeFileNames.get(Integer.parseInt(fileNameAnswer) - 1);
+ } catch (NumberFormatException e) {
+ println("Invalid selection: " + fileNameAnswer);
+ return null;
+ }
}
if (ALL_OPTION_LABEL.equals(fileName)) {
for (URL url : definitionTypeURLs) {
@@ -194,7 +280,7 @@ public abstract class DeploymentCommandSupport implements Action {
URL url = optionalURL.get();
processDefinition(definitionType, url);
} else {
- System.out.println("Couldn't find file " + fileName);
+ println("Couldn't find file " + fileName);
return null;
}
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
similarity index 81%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
index 36ecdeceb..c4fd462c9 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.karaf.shell.api.action.Argument;
import org.apache.karaf.shell.api.action.Command;
@@ -24,7 +24,6 @@ import org.apache.unomi.api.services.EventListenerService;
import org.apache.unomi.api.services.EventService;
import java.io.PrintStream;
-import java.util.ArrayList;
import java.util.List;
@Command(scope = "unomi", name = "event-tail", description = "This will tail all the events coming into the Apache Unomi Context Server")
@@ -57,12 +56,12 @@ public class EventTail extends TailCommandSupport {
@Override
public Object getListener() {
- return new TailEventListener(session.getConsole());
+ return new TailEventListener(getConsole());
}
class TailEventListener implements EventListenerService {
- PrintStream out;
+ private final PrintStream out;
public TailEventListener(PrintStream out) {
this.out = out;
@@ -78,14 +77,7 @@ public class EventTail extends TailCommandSupport {
if (!event.isPersistent() && !withInternal) {
return EventService.NO_CHANGE;
}
- List<String> eventInfo = new ArrayList<>();
- eventInfo.add(event.getItemId());
- eventInfo.add(event.getEventType());
- eventInfo.add(event.getSessionId());
- eventInfo.add(event.getProfileId());
- eventInfo.add(event.getTimeStamp().toString());
- eventInfo.add(event.getScope());
- eventInfo.add(Boolean.toString(event.isPersistent()));
+ List<String> eventInfo = TailCommandUtils.extractEventInfo(event);
outputLine(out, eventInfo);
return EventService.NO_CHANGE;
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
similarity index 87%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
index dad45f10b..154c3c96d 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java
@@ -14,12 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.support.table.Row;
import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
import org.apache.unomi.common.DataTable;
import java.util.ArrayList;
@@ -29,6 +33,9 @@ import java.util.ArrayList;
*/
public abstract class ListCommandSupport implements Action {
+ @Reference
+ protected Session session;
+
@Option(name = "--csv", description = "Output table in CSV format", required = false, multiValued = false)
boolean csv;
@@ -47,13 +54,13 @@ public abstract class ListCommandSupport implements Action {
protected abstract DataTable buildDataTable();
public Object execute() throws Exception {
-
DataTable dataTable = buildDataTable();
String[] headers = getHeaders();
+ PrintStream console = session.getConsole();
if (csv) {
- System.out.println(dataTable.toCSV(headers));
+ console.println(dataTable.toCSV(headers));
return null;
}
@@ -70,7 +77,7 @@ public abstract class ListCommandSupport implements Action {
row.addContent(rowData);
}
- shellTable.print(System.out);
+ shellTable.print(console);
return null;
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java
new file mode 100644
index 000000000..58009cca6
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java
@@ -0,0 +1,51 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Option;
+
+import java.io.IOException;
+
+public abstract class RemoveCommandSupport extends BaseSimpleCommand {
+
+ @Option(name = "--force", description = "Force deletion without confirmation", required = false, multiValued = false)
+ boolean force;
+
+ public abstract Object doRemove() throws Exception;
+
+ public abstract String getResourceDescription();
+
+ @Override
+ public Object execute() throws Exception {
+ Object result = null;
+ // Prompt for confirmation
+ if (force || askForConfirmation("Are you sure you want to delete "+getResourceDescription()+" ? (yes/no): ")) {
+ result = doRemove();
+ println("Resource deleted successfully.");
+ } else {
+ println("Operation cancelled.");
+ }
+ return result;
+ }
+
+ private boolean askForConfirmation(String prompt) throws IOException {
+ String input = session.readLine(prompt, null);
+ return "yes".equals(input);
+ }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
similarity index 86%
copy from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
copy to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
index 7a228be9f..ad5c11aba 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java
@@ -14,9 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
-import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.action.lifecycle.Service;
@@ -24,7 +23,7 @@ import org.apache.unomi.api.services.RulesService;
@Command(scope = "unomi", name = "rule-reset-stats", description = "This command will reset the rule statistics")
@Service
-public class RuleResetStats implements Action {
+public class RuleResetStats extends BaseSimpleCommand {
@Reference
RulesService rulesService;
@@ -32,7 +31,7 @@ public class RuleResetStats implements Action {
@Override
public Object execute() throws Exception {
rulesService.resetAllRuleStatistics();
- System.out.println("Rule statistics successfully reset.");
+ println("Rule statistics successfully reset.");
return null;
}
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
similarity index 80%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
index 6cf66b219..e6de275a9 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.lifecycle.Service;
@@ -23,7 +23,6 @@ import org.apache.unomi.api.rules.Rule;
import org.apache.unomi.api.services.RuleListenerService;
import java.io.PrintStream;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -56,12 +55,12 @@ public class RuleTail extends TailCommandSupport {
@Override
public Object getListener() {
- return new TailRuleListener(session.getConsole());
+ return new TailRuleListener(getConsole());
}
class TailRuleListener implements RuleListenerService {
- PrintStream out;
+ private final PrintStream out;
public TailRuleListener(PrintStream out) {
this.out = out;
@@ -79,14 +78,7 @@ public class RuleTail extends TailCommandSupport {
@Override
public void onExecuteActions(Rule rule, Event event) {
- List<String> ruleExecutionInfo = new ArrayList<>();
- ruleExecutionInfo.add(rule.getItemId());
- ruleExecutionInfo.add(rule.getMetadata().getName());
- ruleExecutionInfo.add(event.getEventType());
- ruleExecutionInfo.add(event.getSessionId());
- ruleExecutionInfo.add(event.getProfileId());
- ruleExecutionInfo.add(event.getTimeStamp().toString());
- ruleExecutionInfo.add(event.getScope());
+ List<String> ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfo(rule, event);
outputLine(out, ruleExecutionInfo);
}
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
similarity index 82%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
index c94394455..63ba069e5 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.karaf.shell.api.action.Argument;
@@ -25,7 +25,6 @@ import org.apache.unomi.api.rules.Rule;
import org.apache.unomi.api.services.RuleListenerService;
import java.io.PrintStream;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -62,12 +61,12 @@ public class RuleWatch extends TailCommandSupport {
@Override
public Object getListener() {
- return new RuleWatchListener(session.getConsole());
+ return new RuleWatchListener(getConsole());
}
class RuleWatchListener implements RuleListenerService {
- PrintStream out;
+ private final PrintStream out;
public RuleWatchListener(PrintStream out) {
this.out = out;
@@ -78,7 +77,6 @@ public class RuleWatch extends TailCommandSupport {
populateRuleInfo(rule, event, "EVALUATE");
}
-
@Override
public void onAlreadyRaised(AlreadyRaisedFor alreadyRaisedFor, Rule rule, Event event) {
populateRuleInfo(rule, event, "AR " + alreadyRaisedFor.toString());
@@ -93,15 +91,7 @@ public class RuleWatch extends TailCommandSupport {
if (!ArrayUtils.contains(ruleIds, rule.getItemId())) {
return;
}
- List<String> ruleExecutionInfo = new ArrayList<>();
- ruleExecutionInfo.add(status);
- ruleExecutionInfo.add(rule.getItemId());
- ruleExecutionInfo.add(rule.getMetadata().getName());
- ruleExecutionInfo.add(event.getEventType());
- ruleExecutionInfo.add(event.getSessionId());
- ruleExecutionInfo.add(event.getProfileId());
- ruleExecutionInfo.add(event.getTimeStamp().toString());
- ruleExecutionInfo.add(event.getScope());
+ List<String> ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfoWithStatus(rule, event, status);
outputLine(out, ruleExecutionInfo);
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
similarity index 93%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
index f6af0328a..7de0d4451 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java
@@ -14,12 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.commands;
import org.apache.commons.lang3.StringUtils;
import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.console.Session;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
@@ -28,15 +27,12 @@ import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
-public abstract class TailCommandSupport implements Action {
+public abstract class TailCommandSupport extends BaseSimpleCommand {
public abstract int[] getColumnSizes();
public abstract String[] getColumnHeaders();
- @Reference
- Session session;
-
@Reference
BundleContext bundleContext;
@@ -74,12 +70,12 @@ public abstract class TailCommandSupport implements Action {
}
out.println(eventLine.toString());
}
-
+
public abstract Object getListener();
public Object execute() throws Exception {
// Do not use System.out as it may write to the wrong console depending on the thread that calls our log handler
- PrintStream out = session.getConsole();
+ PrintStream out = getConsole();
out.flush();
outputHeaders(out);
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java
new file mode 100644
index 000000000..725fa6a5e
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.rules.Rule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for common tail command functionality.
+ */
+public final class TailCommandUtils {
+
+ private TailCommandUtils() {
+ // Utility class - prevent instantiation
+ }
+
+ /**
+ * Extract event information into a list of strings for display.
+ *
+ * @param event the event to extract information from
+ * @return list of event information strings
+ */
+ public static List<String> extractEventInfo(Event event) {
+ List<String> eventInfo = new ArrayList<>();
+ eventInfo.add(event.getItemId());
+ eventInfo.add(event.getEventType());
+ eventInfo.add(event.getSessionId());
+ eventInfo.add(event.getProfileId());
+ eventInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+ eventInfo.add(event.getScope());
+ eventInfo.add(Boolean.toString(event.isPersistent()));
+ return eventInfo;
+ }
+
+ /**
+ * Extract rule execution information into a list of strings for display.
+ *
+ * @param rule the rule to extract information from
+ * @param event the event associated with the rule execution
+ * @return list of rule execution information strings
+ */
+ public static List<String> extractRuleExecutionInfo(Rule rule, Event event) {
+ List<String> ruleExecutionInfo = new ArrayList<>();
+ ruleExecutionInfo.add(rule.getItemId());
+ ruleExecutionInfo.add(rule.getMetadata().getName());
+ ruleExecutionInfo.add(event.getEventType());
+ ruleExecutionInfo.add(event.getSessionId());
+ ruleExecutionInfo.add(event.getProfileId());
+ ruleExecutionInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+ ruleExecutionInfo.add(event.getScope());
+ return ruleExecutionInfo;
+ }
+
+ /**
+ * Extract rule execution information with status into a list of strings for display.
+ *
+ * @param rule the rule to extract information from
+ * @param event the event associated with the rule execution
+ * @param status the status of the rule execution (e.g., "EVALUATE", "EXECUTE", "AR ...")
+ * @return list of rule execution information strings with status as first element
+ */
+ public static List<String> extractRuleExecutionInfoWithStatus(Rule rule, Event event, String status) {
+ List<String> ruleExecutionInfo = new ArrayList<>();
+ ruleExecutionInfo.add(status);
+ ruleExecutionInfo.add(rule.getItemId());
+ ruleExecutionInfo.add(rule.getMetadata().getName());
+ ruleExecutionInfo.add(event.getEventType());
+ ruleExecutionInfo.add(event.getSessionId());
+ ruleExecutionInfo.add(event.getProfileId());
+ ruleExecutionInfo.add(event.getTimeStamp() != null ? event.getTimeStamp().toString() : "-");
+ ruleExecutionInfo.add(event.getScope());
+ return ruleExecutionInfo;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java
new file mode 100644
index 000000000..530d003ff
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java
@@ -0,0 +1,95 @@
+/*
+ * 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.shell.dev.commands;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.unomi.api.PersonaWithSessions;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.segments.Segment;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URL;
+
+@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles")
+@Service
+public class UndeployDefinition extends DeploymentCommandSupport {
+
+ public void processDefinition(String definitionType, URL definitionURL) {
+ try {
+ processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition unregistered");
+ } catch (IOException e) {
+ handleDefinitionError(definitionURL, "removing", e);
+ }
+ }
+
+ @Override
+ protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException {
+ switch (definitionType) {
+ case CONDITION_DEFINITION_TYPE:
+ ConditionType conditionType = readDefinition(definitionURL, ConditionType.class);
+ definitionsService.removeConditionType(conditionType.getItemId());
+ return true;
+ case ACTION_DEFINITION_TYPE:
+ ActionType actionType = readDefinition(definitionURL, ActionType.class);
+ definitionsService.removeActionType(actionType.getItemId());
+ return true;
+ case GOAL_DEFINITION_TYPE:
+ Goal goal = readDefinition(definitionURL, Goal.class);
+ goalsService.removeGoal(goal.getItemId());
+ return true;
+ case CAMPAIGN_DEFINITION_TYPE:
+ Campaign campaign = readDefinition(definitionURL, Campaign.class);
+ goalsService.removeCampaign(campaign.getItemId());
+ return true;
+ case PERSONA_DEFINITION_TYPE:
+ PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class);
+ profileService.delete(persona.getPersona().getItemId(), true);
+ return true;
+ case PROPERTY_DEFINITION_TYPE:
+ PropertyType propertyType = readDefinition(definitionURL, PropertyType.class);
+ profileService.deletePropertyType(propertyType.getItemId());
+ return true;
+ case RULE_DEFINITION_TYPE:
+ Rule rule = readDefinition(definitionURL, Rule.class);
+ rulesService.removeRule(rule.getItemId());
+ return true;
+ case SEGMENT_DEFINITION_TYPE:
+ Segment segment = readDefinition(definitionURL, Segment.class);
+ segmentService.removeSegmentDefinition(segment.getItemId(), false);
+ return true;
+ case SCORING_DEFINITION_TYPE:
+ Scoring scoring = readDefinition(definitionURL, Scoring.class);
+ segmentService.removeScoringDefinition(scoring.getItemId(), false);
+ return true;
+ case PATCH_DEFINITION_TYPE:
+ console.println("Patch undeployment is not supported.");
+ return false;
+ default:
+ console.println("Unrecognized definition type: " + definitionType);
+ return false;
+ }
+ }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
new file mode 100644
index 000000000..319464aef
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
@@ -0,0 +1,157 @@
+/*
+ * 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.shell.dev.commands.actions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Parameter;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Scope;
+import org.apache.unomi.api.actions.ActionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ScopeService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ActionTypeCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "tags", "systemTags", "parameters"
+ );
+
+ @Reference
+ private ScopeService scopeService;
+
+ @Override
+ public String getObjectType() {
+ return "actiontype";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Identifier",
+ "Name",
+ "Description",
+ "Scope",
+ "Tags",
+ "System Tags",
+ "Parameters",
+ "Action Executor"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ List<ActionType> actionTypes = new ArrayList<>(definitionsService.getAllActionTypes());
+ return paginateList(actionTypes, query);
+ }
+
+ @Override
+ protected String[] buildRow(Object item) {
+ ActionType actionType = (ActionType) item;
+ return new String[] {
+ actionType.getItemId(),
+ actionType.getMetadata().getName(),
+ actionType.getMetadata().getDescription(),
+ actionType.getMetadata().getScope(),
+ String.join(",", actionType.getMetadata().getTags()),
+ String.join(",", actionType.getMetadata().getSystemTags()),
+ String.join(",", actionType.getParameters().stream().map(Parameter::getId).collect(Collectors.toList())),
+ actionType.getActionExecutor()
+ };
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ ActionType actionType = OBJECT_MAPPER.convertValue(properties, ActionType.class);
+ definitionsService.setActionType(actionType);
+ return actionType.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ ActionType actionType = definitionsService.getActionType(id);
+ if (actionType != null) {
+ return OBJECT_MAPPER.convertValue(actionType, Map.class);
+ }
+ return null;
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ ActionType existing = definitionsService.getActionType(id);
+ if (existing == null) {
+ throw new IllegalArgumentException("Action type not found: " + id);
+ }
+
+ ActionType updated = OBJECT_MAPPER.convertValue(properties, ActionType.class);
+ updated.setItemId(id);
+ definitionsService.setActionType(updated);
+ }
+
+ @Override
+ public void delete(String id) {
+ definitionsService.removeActionType(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Identifier for the action type",
+ "- name: Name of the action type",
+ "",
+ "Optional properties:",
+ "- description: Description of the action type",
+ "- scope: Scope of the action type",
+ "- tags: List of tags",
+ "- systemTags: List of system tags",
+ "- parameters: Map of parameter definitions"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ if ("scope".equals(propertyName)) {
+ return scopeService.getScopes().stream()
+ .map(Scope::getItemId)
+ .filter(id -> id.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+ return Collections.emptyList();
+ }
+
+ public void setScopeService(ScopeService scopeService) {
+ this.scopeService = scopeService;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
new file mode 100644
index 000000000..c908f5320
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
@@ -0,0 +1,151 @@
+/*
+ * 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.shell.dev.commands.campaigns;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.campaigns.Campaign;
+import org.apache.unomi.api.campaigns.CampaignDetail;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on campaigns
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class CampaignCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "startDate", "endDate", "cost", "currency", "primaryGoal", "goals", "entryCondition", "enabled"
+ );
+
+ @Reference
+ private GoalsService goalsService;
+
+ @Override
+ public String getObjectType() {
+ return "campaign";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Name", "Description", "Start Date", "End Date", "Cost", "Currency", "Primary Goal", "Enabled"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return goalsService.getCampaignDetails(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ CampaignDetail detail = (CampaignDetail) item;
+ Campaign campaign = detail.getCampaign();
+ String primaryGoalName = "";
+ if (campaign.getPrimaryGoal() != null) {
+ // Get the goal details to get its name
+ Goal primaryGoal = goalsService.getGoal(campaign.getPrimaryGoal());
+ if (primaryGoal != null) {
+ primaryGoalName = primaryGoal.getMetadata().getName();
+ }
+ }
+ return new Comparable[]{
+ campaign.getItemId(),
+ campaign.getMetadata().getName(),
+ campaign.getMetadata().getDescription(),
+ campaign.getStartDate() != null ? campaign.getStartDate().toString() : "",
+ campaign.getEndDate() != null ? campaign.getEndDate().toString() : "",
+ campaign.getCost() != null ? campaign.getCost().toString() : "",
+ campaign.getCurrency(),
+ primaryGoalName,
+ campaign.getMetadata().isEnabled()
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Campaign campaign = goalsService.getCampaign(id);
+ if (campaign == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(campaign, Map.class);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Campaign campaign = OBJECT_MAPPER.convertValue(properties, Campaign.class);
+ goalsService.setCampaign(campaign);
+ return campaign.getItemId();
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Campaign existingCampaign = goalsService.getCampaign(id);
+ if (existingCampaign == null) {
+ throw new IllegalArgumentException("Campaign not found with ID: " + id);
+ }
+
+ Campaign updatedCampaign = OBJECT_MAPPER.convertValue(properties, Campaign.class);
+ updatedCampaign.setItemId(id);
+ goalsService.setCampaign(updatedCampaign);
+ }
+
+ @Override
+ public void delete(String id) {
+ Campaign campaign = goalsService.getCampaign(id);
+ if (campaign != null) {
+ goalsService.removeCampaign(id);
+ }
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: The unique identifier of the campaign",
+ "- name: The name of the campaign",
+ "- description: The description of the campaign",
+ "",
+ "Optional properties:",
+ "- scope: The scope of the campaign (defaults to systemscope)",
+ "- startDate: The start date of the campaign (ISO-8601 format)",
+ "- endDate: The end date of the campaign (ISO-8601 format)",
+ "- cost: The cost of the campaign",
+ "- currency: The currency for the campaign cost",
+ "- primaryGoal: The primary goal of the campaign",
+ "- goals: List of goals associated with the campaign",
+ "- entryCondition: The condition that determines when a visitor enters the campaign",
+ "- enabled: Whether the campaign is enabled (true/false)"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
new file mode 100644
index 000000000..28ddb190a
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
@@ -0,0 +1,147 @@
+/*
+ * 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.shell.dev.commands.campaigns;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.campaigns.events.CampaignEvent;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on campaign events
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class CampaignEventCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "campaignId", "eventDate", "cost", "currency", "timezone"
+ );
+
+ @Reference
+ private GoalsService goalsService;
+
+ @Override
+ public String getObjectType() {
+ return "campaignevent";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Name", "Description", "Campaign ID", "Event Date", "Cost", "Currency", "Timezone"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return goalsService.getEvents(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ CampaignEvent event = (CampaignEvent) item;
+ return new Comparable[]{
+ event.getItemId(),
+ event.getMetadata().getName(),
+ event.getMetadata().getDescription(),
+ event.getCampaignId(),
+ event.getEventDate() != null ? event.getEventDate().toString() : "",
+ event.getCost() != null ? event.getCost().toString() : "",
+ event.getCurrency(),
+ event.getTimezone()
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ int offset = 0;
+ final int pageSize = 200;
+ while (true) {
+ Query query = new Query();
+ Condition condition = new Condition();
+ condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(condition);
+ query.setOffset(offset);
+ query.setLimit(pageSize);
+
+ PartialList<CampaignEvent> events = goalsService.getEvents(query);
+ CampaignEvent event = events.getList().stream()
+ .filter(e -> e.getItemId().equals(id))
+ .findFirst()
+ .orElse(null);
+ if (event != null) {
+ return OBJECT_MAPPER.convertValue(event, Map.class);
+ }
+ if (offset + events.getList().size() >= events.getTotalSize()) {
+ return null;
+ }
+ offset += pageSize;
+ }
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ CampaignEvent event = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class);
+ goalsService.setCampaignEvent(event);
+ return event.getItemId();
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ CampaignEvent updatedEvent = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class);
+ updatedEvent.setItemId(id);
+ goalsService.setCampaignEvent(updatedEvent);
+ }
+
+ @Override
+ public void delete(String id) {
+ goalsService.removeCampaignEvent(id);
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: The unique identifier of the campaign event",
+ "- name: The name of the campaign event",
+ "- description: The description of the campaign event",
+ "- campaignId: The ID of the campaign this event belongs to",
+ "",
+ "Optional properties:",
+ "- scope: The scope of the campaign event (defaults to systemscope)",
+ "- eventDate: The date of the event (ISO-8601 format)",
+ "- cost: The cost associated with this event",
+ "- currency: The currency for the event cost",
+ "- timezone: The timezone for the event"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
new file mode 100644
index 000000000..b98560290
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
@@ -0,0 +1,153 @@
+/*
+ * 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.shell.dev.commands.conditions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ConditionTypeCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "scope", "name", "description", "conditionEvaluator", "queryBuilder", "parameters", "parentCondition"
+ );
+
+ @Override
+ public String getObjectType() {
+ return "conditiontype";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Activated",
+ "Hidden",
+ "Read-only",
+ "Identifier",
+ "Scope",
+ "Name",
+ "Tags",
+ "System tags"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ List<ConditionType> allTypes = new ArrayList<>(definitionsService.getAllConditionTypes());
+ int offset = query.getOffset();
+ int pageSize = query.getLimit();
+ int totalSize = allTypes.size();
+
+ List<ConditionType> pageTypes = allTypes.subList(
+ Math.min(offset, totalSize),
+ Math.min(offset + pageSize, totalSize)
+ );
+
+ return new PartialList<ConditionType>(pageTypes, offset, pageSize, totalSize, PartialList.Relation.EQUAL);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ ConditionType type = (ConditionType) item;
+ Metadata metadata = type.getMetadata();
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(metadata.isEnabled() ? "x" : "");
+ rowData.add(metadata.isHidden() ? "x" : "");
+ rowData.add(metadata.isReadOnly() ? "x" : "");
+ rowData.add(metadata.getId());
+ rowData.add(metadata.getScope());
+ rowData.add(metadata.getName());
+ rowData.add(StringUtils.join(metadata.getTags(), ","));
+ rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ ConditionType conditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class);
+ definitionsService.setConditionType(conditionType);
+ return conditionType.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ ConditionType conditionType = definitionsService.getConditionType(id);
+ if (conditionType == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(conditionType, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ ConditionType conditionType = definitionsService.getConditionType(id);
+ if (conditionType == null) {
+ throw new IllegalArgumentException("Condition type with id '" + id + "' not found");
+ }
+ ConditionType updatedConditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class);
+ updatedConditionType.setItemId(id);
+ definitionsService.setConditionType(updatedConditionType);
+ }
+
+ @Override
+ public void delete(String id) {
+ definitionsService.removeConditionType(id);
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return "Required properties:\n" +
+ "- itemId (string): Unique identifier for the condition type\n" +
+ "- scope (string): Scope of the condition type\n" +
+ "- name (string): Human-readable name\n" +
+ "\n" +
+ "Optional properties:\n" +
+ "- description (string): Description of the condition type\n" +
+ "- conditionEvaluator (string): Name of the condition evaluator implementation\n" +
+ "- queryBuilder (string): Name of the query builder implementation\n" +
+ "- parameters (array): List of parameters, each containing:\n" +
+ " - id (string): Parameter identifier\n" +
+ " - type (string): Parameter type\n" +
+ " - multivalued (boolean): Whether the parameter accepts multiple values\n" +
+ " - defaultValue (any): Default value for the parameter\n" +
+ "- parentCondition (object): Parent condition definition\n" +
+ "- enabled (boolean): Whether the condition type is enabled (default: true)\n" +
+ "- hidden (boolean): Whether the condition type is hidden (default: false)\n" +
+ "- readOnly (boolean): Whether the condition type is read-only (default: false)\n" +
+ "- tags (array): List of tags for the condition type\n" +
+ "- systemTags (array): List of system tags for the condition type";
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
new file mode 100644
index 000000000..bb3d5c195
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
@@ -0,0 +1,255 @@
+/*
+ * 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.shell.dev.commands.consents;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Consent;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+/**
+ * A command to perform CRUD operations on consents
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class ConsentCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final String CONSENT_DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ private static final ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(
+ () -> new SimpleDateFormat(CONSENT_DATE_FORMAT_PATTERN));
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "profileId", "scope", "typeIdentifier", "status", "statusDate", "revokeDate"
+ );
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return "consent";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"Profile ID", "Scope", "Type", "Status", "Status Date", "Revoke Date"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ // Consents are embedded in profiles. We fetch consentLimit*20 profiles from offset 0,
+ // then paginate the extracted consent list with the original query parameters.
+ // LIMITATION: always starts from profile offset 0, so consents on profiles outside
+ // the first consentLimit*20 results may not appear for high-offset pages.
+ int consentLimit = query.getLimit() > 0 ? query.getLimit() : 100;
+ Query profileQuery = new Query();
+ profileQuery.setLimit(consentLimit * 20);
+ profileQuery.setOffset(0);
+ profileQuery.setSortby(query.getSortby());
+ profileQuery.setCondition(query.getCondition());
+ PartialList<Profile> profiles = profileService.search(profileQuery, Profile.class);
+ List<Map<String, Object>> consents = new ArrayList<>();
+
+ DateFormat dateFormat = DATE_FORMAT.get();
+ for (Profile profile : profiles.getList()) {
+ Map<String, Object> profileProperties = profile.getProperties();
+ if (profileProperties.containsKey("consents")) {
+ @SuppressWarnings("unchecked")
+ Map<String, Consent> profileConsents = (Map<String, Consent>) profileProperties.get("consents");
+ for (Map.Entry<String, Consent> entry : profileConsents.entrySet()) {
+ Map<String, Object> consentMap = entry.getValue().toMap(dateFormat);
+ consentMap.put("profileId", profile.getItemId());
+ consents.add(consentMap);
+ }
+ }
+ }
+
+ return paginateList(consents, query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> consentMap = (Map<String, Object>) item;
+ return new Comparable[]{
+ String.valueOf(consentMap.get("profileId")),
+ String.valueOf(consentMap.get("scope")),
+ String.valueOf(consentMap.get("typeIdentifier")),
+ String.valueOf(consentMap.get("status")),
+ String.valueOf(consentMap.get("statusDate")),
+ String.valueOf(consentMap.get("revokeDate"))
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ // The ID format is expected to be "profileId:typeIdentifier"
+ String[] parts = id.split(":");
+ if (parts.length != 2) {
+ return null;
+ }
+
+ String profileId = parts[0];
+ String typeIdentifier = parts[1];
+
+ Profile profile = profileService.load(profileId);
+ if (profile == null) {
+ return null;
+ }
+
+ Map<String, Object> profileProperties = profile.getProperties();
+ if (!profileProperties.containsKey("consents")) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ Map<String, Consent> consents = (Map<String, Consent>) profileProperties.get("consents");
+ Consent consent = consents.get(typeIdentifier);
+
+ if (consent == null) {
+ return null;
+ }
+
+ Map<String, Object> consentMap = consent.toMap(DATE_FORMAT.get());
+ consentMap.put("profileId", profileId);
+ return consentMap;
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ String profileId = (String) properties.remove("profileId");
+ if (profileId == null) {
+ return null;
+ }
+
+ Profile profile = profileService.load(profileId);
+ if (profile == null) {
+ return null;
+ }
+
+ try {
+ Consent consent = new Consent(properties, DATE_FORMAT.get());
+
+ Map<String, Object> profileProperties = profile.getProperties();
+ @SuppressWarnings("unchecked")
+ Map<String, Consent> consents = profileProperties.containsKey("consents") ?
+ (Map<String, Consent>) profileProperties.get("consents") : new HashMap<>();
+
+ consents.put(consent.getTypeIdentifier(), consent);
+ profileProperties.put("consents", consents);
+ profile.setProperties(profileProperties);
+
+ profileService.save(profile);
+ return profileId + ":" + consent.getTypeIdentifier();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ // The ID format is expected to be "profileId:typeIdentifier"
+ String[] parts = id.split(":");
+ if (parts.length != 2) {
+ return;
+ }
+
+ String profileId = parts[0];
+ String typeIdentifier = parts[1];
+
+ Profile profile = profileService.load(profileId);
+ if (profile == null) {
+ return;
+ }
+
+ try {
+ Consent consent = new Consent(properties, DATE_FORMAT.get());
+
+ Map<String, Object> profileProperties = profile.getProperties();
+ @SuppressWarnings("unchecked")
+ Map<String, Consent> consents = profileProperties.containsKey("consents") ?
+ (Map<String, Consent>) profileProperties.get("consents") : new HashMap<>();
+
+ consents.put(typeIdentifier, consent);
+ profileProperties.put("consents", consents);
+ profile.setProperties(profileProperties);
+
+ profileService.save(profile);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to update consent: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void delete(String id) {
+ // The ID format is expected to be "profileId:typeIdentifier"
+ String[] parts = id.split(":");
+ if (parts.length != 2) {
+ return;
+ }
+
+ String profileId = parts[0];
+ String typeIdentifier = parts[1];
+
+ Profile profile = profileService.load(profileId);
+ if (profile == null) {
+ return;
+ }
+
+ Map<String, Object> profileProperties = profile.getProperties();
+ if (profileProperties.containsKey("consents")) {
+ @SuppressWarnings("unchecked")
+ Map<String, Consent> consents = (Map<String, Consent>) profileProperties.get("consents");
+ consents.remove(typeIdentifier);
+ profileProperties.put("consents", consents);
+ profile.setProperties(profileProperties);
+ profileService.save(profile);
+ }
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- profileId: The identifier of the profile this consent belongs to",
+ "- typeIdentifier: The unique identifier for the consent type",
+ "- scope: The scope for this consent",
+ "- status: The consent status (GRANTED, DENIED, or REVOKED)",
+ "",
+ "Optional properties:",
+ "- statusDate: The date from which this consent applies (ISO-8601 format)",
+ "- revokeDate: The date at which this consent will expire (ISO-8601 format)"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
new file mode 100644
index 000000000..697867bd3
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
@@ -0,0 +1,167 @@
+/*
+ * 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.shell.dev.commands.events;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class EventCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "eventType", "scope", "source", "target", "properties", "persistent"
+ );
+ private static final List<String> EVENT_TYPES = List.of(
+ "view", "form", "login", "sessionCreated", "sessionReassigned", "profileUpdated", "incrementTrait",
+ "modifyConsent", "updateProperties", "identify", "impersonate", "matching"
+ );
+
+ @Reference
+ private EventService eventService;
+
+ @Override
+ public String getObjectType() {
+ return "event";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Identifier",
+ "Event Type",
+ "Scope",
+ "Source ID",
+ "Target ID",
+ "Profile ID",
+ "Session ID",
+ "Timestamp"
+ };
+ }
+
+ @Override
+ protected String getSortBy() {
+ return "timeStamp:desc";
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return eventService.search(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Event event = (Event) item;
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(event.getItemId());
+ rowData.add(event.getEventType());
+ rowData.add(event.getScope());
+ rowData.add(event.getSource() != null ? event.getSource().getItemId() : "");
+ rowData.add(event.getTarget() != null ? event.getTarget().getItemId() : "");
+ rowData.add(event.getProfileId());
+ rowData.add(event.getSessionId());
+ rowData.add(event.getTimeStamp());
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Event event = OBJECT_MAPPER.convertValue(properties, Event.class);
+ eventService.send(event);
+ return event.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Event event = eventService.getEvent(id);
+ if (event == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(event, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ properties.put("itemId", id);
+ Event event = OBJECT_MAPPER.convertValue(properties, Event.class);
+ eventService.send(event);
+ }
+
+ @Override
+ public void delete(String id) {
+ eventService.deleteEvent(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Event ID (string)",
+ "- eventType: Type of event",
+ "- scope: Event scope",
+ "- source: Source object (Item)",
+ "- target: Target object (Item)",
+ "",
+ "Optional properties:",
+ "- properties: Map of event properties",
+ "- persistent: Whether event should be persisted (boolean)",
+ "",
+ "Common event types:",
+ "- view: Page/content view",
+ "- form: Form submission",
+ "- login: User login",
+ "- sessionCreated: New session",
+ "- sessionReassigned: Session reassigned",
+ "- profileUpdated: Profile update",
+ "- incrementTrait: Increment profile trait",
+ "- modifyConsent: Consent change",
+ "- updateProperties: Property update",
+ "- identify: Profile identification",
+ "- impersonate: Profile impersonation",
+ "- matching: Profile matching"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ if ("eventType".equals(propertyName)) {
+ return EVENT_TYPES.stream()
+ .filter(type -> type.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+ return List.of();
+ }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
new file mode 100644
index 000000000..7f03aba9b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
@@ -0,0 +1,143 @@
+/*
+ * 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.shell.dev.commands.goals;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.goals.Goal;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.GoalsService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on goals
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class GoalCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "campaignId", "enabled"
+ );
+
+ @Reference
+ private GoalsService goalsService;
+
+ @Override
+ public String getObjectType() {
+ return "goal";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Name", "Description", "Scope", "Campaign ID", "Enabled"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ // Convert Set<Metadata> to PartialList for consistency
+ Set<Metadata> metadatas = goalsService.getGoalMetadatas(query);
+ List<Goal> goals = metadatas.stream()
+ .map(metadata -> goalsService.getGoal(metadata.getId()))
+ .filter(goal -> goal != null)
+ .collect(Collectors.toList());
+ // getGoalMetadatas returns Set<Metadata> with no totalSize information; we cannot distinguish
+ // "hit limit" from "exactly that many items exist", so we report goals.size() as totalSize.
+ return new PartialList<>(goals, query.getOffset(), goals.size(), goals.size(), PartialList.Relation.EQUAL);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Goal goal = (Goal) item;
+ return new Comparable[]{
+ goal.getItemId(),
+ goal.getMetadata().getName(),
+ goal.getMetadata().getDescription(),
+ goal.getMetadata().getScope(),
+ goal.getCampaignId(),
+ goal.getMetadata().isEnabled()
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Goal goal = goalsService.getGoal(id);
+ if (goal == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(goal, Map.class);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Goal goal = OBJECT_MAPPER.convertValue(properties, Goal.class);
+ goalsService.setGoal(goal);
+ return goal.getItemId();
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Goal existingGoal = goalsService.getGoal(id);
+ if (existingGoal == null) {
+ throw new IllegalArgumentException("Goal not found with ID: " + id);
+ }
+
+ Goal updatedGoal = OBJECT_MAPPER.convertValue(properties, Goal.class);
+ updatedGoal.setItemId(id);
+ goalsService.setGoal(updatedGoal);
+ }
+
+ @Override
+ public void delete(String id) {
+ Goal goal = goalsService.getGoal(id);
+ if (goal != null) {
+ goalsService.removeGoal(id);
+ }
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: The unique identifier of the goal",
+ "- name: The name of the goal",
+ "- description: The description of the goal",
+ "",
+ "Optional properties:",
+ "- scope: The scope of the goal (defaults to systemscope)",
+ "- campaignId: The ID of the associated campaign",
+ "- enabled: Whether the goal is enabled (true/false)",
+ "- startEvent: The condition that triggers the start of the goal",
+ "- targetEvent: The condition that marks the goal as achieved"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
new file mode 100644
index 000000000..5633e89c4
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
@@ -0,0 +1,153 @@
+/*
+ * 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.shell.dev.commands.personas;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Persona;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class PersonaCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = Arrays.asList(
+ "firstName",
+ "lastName",
+ "email",
+ "description",
+ "properties"
+ );
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return Persona.ITEM_TYPE;
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Identifier",
+ "First Name",
+ "Last Name",
+ "Email",
+ "Description",
+ "Last Updated"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return profileService.search(query, Persona.class);
+ }
+
+ @Override
+ protected String[] buildRow(Object item) {
+ Persona persona = (Persona) item;
+ Object lastUpdated = persona.getSystemProperties() != null
+ ? persona.getSystemProperties().get("lastUpdated")
+ : null;
+ return new String[] {
+ persona.getItemId(),
+ (String) persona.getProperty("firstName"),
+ (String) persona.getProperty("lastName"),
+ (String) persona.getProperty("email"),
+ (String) persona.getProperty("description"),
+ lastUpdated != null ? lastUpdated.toString() : ""
+ };
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ String personaId = (String) properties.get("itemId");
+ if (personaId == null) {
+ throw new IllegalArgumentException("itemId is required");
+ }
+
+ Persona persona = new Persona(personaId);
+ for (Map.Entry<String, Object> entry : properties.entrySet()) {
+ if (!entry.getKey().equals("itemId")) {
+ persona.setProperty(entry.getKey(), entry.getValue());
+ }
+ }
+
+ Persona saved = profileService.savePersona(persona);
+ return saved.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Persona persona = profileService.loadPersona(id);
+ return persona != null ? OBJECT_MAPPER.convertValue(persona, Map.class) : null;
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Persona persona = profileService.loadPersona(id);
+ if (persona == null) {
+ throw new IllegalArgumentException("Persona not found: " + id);
+ }
+
+ for (Map.Entry<String, Object> entry : properties.entrySet()) {
+ persona.setProperty(entry.getKey(), entry.getValue());
+ }
+
+ profileService.savePersona(persona);
+ }
+
+ @Override
+ public void delete(String id) {
+ profileService.delete(id, true);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return "Required properties:\n" +
+ "- itemId: The identifier for the persona\n" +
+ "\n" +
+ "Optional properties:\n" +
+ "- firstName: The persona's first name\n" +
+ "- lastName: The persona's last name\n" +
+ "- email: The persona's email address\n" +
+ "- description: A description of what this persona represents\n" +
+ "- properties: Additional properties as a JSON object";
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ return List.of();
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
new file mode 100644
index 000000000..fb5f38391
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
@@ -0,0 +1,159 @@
+/*
+ * 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.shell.dev.commands.profiles;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on profile aliases
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class ProfileAliasCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "profileID", "clientID", "creationTime", "modifiedTime"
+ );
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return "profilealias";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Profile ID", "Client ID", "Created", "Modified"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ // Since there's no direct method to search all aliases, we'll use findProfileAliases with null profileId
+ return profileService.findProfileAliases(null, query.getOffset(), query.getLimit(), query.getSortby());
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ ProfileAlias alias = (ProfileAlias) item;
+ return new Comparable[]{
+ alias.getItemId(),
+ alias.getProfileID(),
+ alias.getClientID(),
+ alias.getCreationTime() != null ? alias.getCreationTime().toString() : "",
+ alias.getModifiedTime() != null ? alias.getModifiedTime().toString() : ""
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ int offset = 0;
+ final int pageSize = 200;
+ while (true) {
+ PartialList<ProfileAlias> page = profileService.findProfileAliases(null, offset, pageSize, null);
+ ProfileAlias alias = page.getList().stream()
+ .filter(a -> a.getItemId().equals(id))
+ .findFirst()
+ .orElse(null);
+ if (alias != null) {
+ return OBJECT_MAPPER.convertValue(alias, Map.class);
+ }
+ if (offset + page.getList().size() >= page.getTotalSize()) {
+ return null;
+ }
+ offset += pageSize;
+ }
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ ProfileAlias alias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class);
+ // Set creation and modification time if not provided
+ if (alias.getCreationTime() == null) {
+ alias.setCreationTime(new Date());
+ }
+ if (alias.getModifiedTime() == null) {
+ alias.setModifiedTime(alias.getCreationTime());
+ }
+
+ // Add the alias to the profile
+ profileService.addAliasToProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID());
+ return alias.getItemId();
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ // Get the existing alias (check if it exists and get its data in one call)
+ Map<String, Object> aliasData = read(id);
+ if (aliasData == null) {
+ return;
+ }
+
+ // Remove the old alias and add the new one
+ ProfileAlias oldAlias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class);
+ profileService.removeAliasFromProfile(oldAlias.getProfileID(), oldAlias.getItemId(), oldAlias.getClientID());
+
+ ProfileAlias updatedAlias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class);
+ updatedAlias.setItemId(id);
+ updatedAlias.setModifiedTime(new Date());
+ profileService.addAliasToProfile(updatedAlias.getProfileID(), updatedAlias.getItemId(), updatedAlias.getClientID());
+ }
+
+ @Override
+ public void delete(String id) {
+ // First check if the alias exists
+ Map<String, Object> aliasData = read(id);
+ if (aliasData != null) {
+ ProfileAlias alias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class);
+ profileService.removeAliasFromProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID());
+ }
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: The unique identifier of the alias",
+ "- profileID: The identifier of the profile this alias belongs to",
+ "- clientID: The identifier of the client that created this alias",
+ "",
+ "Optional properties:",
+ "- creationTime: The creation timestamp (ISO-8601 format)",
+ "- modifiedTime: The last modification timestamp (ISO-8601 format)"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
new file mode 100644
index 000000000..1ac66997b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
@@ -0,0 +1,186 @@
+/*
+ * 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.shell.dev.commands.profiles;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.apache.unomi.api.conditions.Condition;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ProfileCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = Arrays.asList(
+ "itemId",
+ "properties.firstName",
+ "properties.lastName",
+ "properties.email",
+ "segments",
+ "scores",
+ "consents",
+ "systemProperties.lastUpdated"
+ );
+
+ private static final String[] TABLE_HEADERS = {
+ "Identifier",
+ "First Name",
+ "Last Name",
+ "Email",
+ "Segments",
+ "Last Updated",
+ "Scope"
+ };
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return "profile";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return TABLE_HEADERS;
+ }
+
+ @Override
+ protected String getSortBy() {
+ return "systemProperties.lastUpdated:desc,properties.lastVisit:desc";
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return profileService.search(query, Profile.class);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Profile profile = (Profile) item;
+ Map<String, Object> systemProperties = profile.getSystemProperties();
+ return new Comparable[] {
+ profile.getItemId(),
+ (String) profile.getProperty("firstName"),
+ (String) profile.getProperty("lastName"),
+ (String) profile.getProperty("email"),
+ profile.getSegments().size(),
+ systemProperties != null ? (Comparable) systemProperties.get("lastUpdated") : null,
+ profile.getScope()
+ };
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ String itemId = (String) properties.get("itemId");
+ Profile profile = itemId != null ? new Profile(itemId) : new Profile();
+ Map<String, Object> profileProps = new java.util.HashMap<>(properties);
+ profileProps.remove("itemId");
+ profile.setProperties(profileProps);
+ profileService.save(profile);
+ return profile.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Profile profile = profileService.load(id);
+ return profile != null ? OBJECT_MAPPER.convertValue(profile, Map.class) : null;
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Profile profile = profileService.load(id);
+ if (profile == null) {
+ throw new IllegalArgumentException("Profile not found with ID: " + id);
+ }
+ profile.setProperties(properties);
+ profileService.save(profile);
+ }
+
+ @Override
+ public void delete(String id) {
+ profileService.delete(id, false);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return "Required properties:\n" +
+ " - itemId: Unique identifier for the profile\n" +
+ "\n" +
+ "Optional properties:\n" +
+ " - properties.firstName: First name\n" +
+ " - properties.lastName: Last name\n" +
+ " - properties.email: Email address\n" +
+ " - properties.phoneNumber: Phone number\n" +
+ " - properties.address: Address\n" +
+ " - properties.company: Company name\n" +
+ " - properties.jobTitle: Job title\n" +
+ " - consents: Map of consent IDs to consent status\n" +
+ " - scores: Map of scoring plan IDs to scores";
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completeId(String prefix) {
+ // Create a query with filter by ID prefix if provided
+ Query query = new Query();
+ query.setLimit(20); // Reasonable limit for auto-completion
+
+ try {
+ // If prefix is not empty, filter by it
+ if (!prefix.isEmpty()) {
+ Condition condition = new Condition(definitionsService.getConditionType("profilePropertyCondition"));
+ condition.setParameter("propertyName", "itemId");
+ condition.setParameter("comparisonOperator", "startsWith");
+ condition.setParameter("propertyValue", prefix);
+ query.setCondition(condition);
+ } else {
+ // Otherwise, match all
+ Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(matchAllCondition);
+ }
+
+ query.setSortby("properties.lastVisit:desc"); // Sort by last visit to show most recent profiles first
+
+ // Execute the query
+ PartialList<Profile> profiles = profileService.search(query, Profile.class);
+
+ // Extract IDs
+ return profiles.getList().stream()
+ .map(Profile::getItemId)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ return List.of(); // Return empty list on error
+ }
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
new file mode 100644
index 000000000..55b31301f
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
@@ -0,0 +1,174 @@
+/*
+ * 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.shell.dev.commands.properties;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class PropertyTypeCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "tags", "systemTags", "target", "valueTypeId",
+ "defaultValue", "rank", "mergeStrategy", "multivalued", "protected"
+ );
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return "propertytype";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Identifier",
+ "Name",
+ "Description",
+ "Target",
+ "Value Type",
+ "Default Value",
+ "Multivalued",
+ "Protected",
+ "Rank",
+ "Merge Strategy",
+ "Scope",
+ "Tags",
+ "System Tags"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ Map<String, Collection<PropertyType>> propertyTypesByTarget = profileService.getTargetPropertyTypes();
+ List<PropertyType> propertyTypes = new ArrayList<>();
+
+ // Combine all property types from all targets
+ for (Collection<PropertyType> typeCollection : propertyTypesByTarget.values()) {
+ propertyTypes.addAll(typeCollection);
+ }
+
+ return paginateList(propertyTypes, query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ PropertyType propertyType = (PropertyType) item;
+ return new Comparable[] {
+ propertyType.getItemId(),
+ propertyType.getMetadata().getName(),
+ propertyType.getMetadata().getDescription(),
+ propertyType.getTarget(),
+ propertyType.getValueTypeId(),
+ propertyType.getDefaultValue(),
+ String.valueOf(propertyType.isMultivalued()),
+ String.valueOf(propertyType.isProtected()),
+ propertyType.getRank() != null ? propertyType.getRank().toString() : "",
+ propertyType.getMergeStrategy(),
+ propertyType.getMetadata().getScope(),
+ String.join(",", propertyType.getMetadata().getTags()),
+ String.join(",", propertyType.getMetadata().getSystemTags())
+ };
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class);
+ profileService.setPropertyType(propertyType);
+ return propertyType.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ PropertyType propertyType = profileService.getPropertyType(id);
+ if (propertyType != null) {
+ return OBJECT_MAPPER.convertValue(propertyType, Map.class);
+ }
+ return null;
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class);
+ propertyType.setItemId(id);
+ profileService.setPropertyType(propertyType);
+ }
+
+ @Override
+ public void delete(String id) {
+ profileService.deletePropertyType(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Identifier for the property type",
+ "- name: Name of the property type",
+ "- valueTypeId: Type of value (string, integer, long, date, etc.)",
+ "",
+ "Optional properties:",
+ "- description: Description of the property type",
+ "- target: Target type (e.g., 'profiles')",
+ "- defaultValue: Default value for the property",
+ "- rank: Numeric rank for ordering",
+ "- mergeStrategy: Strategy for merging property values",
+ "- multivalued: Whether the property can have multiple values (true/false)",
+ "- protected: Whether the property is read-only (true/false)",
+ "- scope: Scope of the property type",
+ "- tags: List of tags",
+ "- systemTags: List of system tags"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ if ("valueTypeId".equals(propertyName)) {
+ return List.of("string", "integer", "long", "date", "boolean", "json")
+ .stream()
+ .filter(type -> type.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+ return List.of();
+ }
+
+ public void setProfileService(ProfileService profileService) {
+ this.profileService = profileService;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
new file mode 100644
index 000000000..f99e5ad94
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
@@ -0,0 +1,247 @@
+/*
+ * 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.shell.dev.commands.rules;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.karaf.shell.support.table.ShellTable;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.rules.RuleStatistics;
+import org.apache.unomi.api.services.RulesService;
+import org.apache.unomi.common.DataTable;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.apache.unomi.api.conditions.Condition;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class RuleCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "priority", "condition", "actions", "metadata"
+ );
+ private static final List<String> CONDITION_TYPES = List.of(
+ "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition",
+ "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition"
+ );
+
+ @Reference
+ private RulesService rulesService;
+
+ /** Cached for the duration of a single list/query operation (see {@link #buildQuery}). */
+ private Map<String, RuleStatistics> statisticsSnapshot;
+
+ @Override
+ public String getObjectType() {
+ return "rule";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Activated",
+ "Hidden",
+ "Read-only",
+ "Identifier",
+ "Scope",
+ "Name",
+ "Tags",
+ "System tags",
+ "Executions",
+ "Conditions [ms]",
+ "Actions [ms]"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return rulesService.getRuleDetails(query);
+ }
+
+ @Override
+ protected DataTable buildDataTable() {
+ try {
+ statisticsSnapshot = rulesService.getAllRuleStatistics();
+ return super.buildDataTable();
+ } finally {
+ statisticsSnapshot = null;
+ }
+ }
+
+ @Override
+ public void buildRows(ShellTable table, int maxEntries) {
+ try {
+ statisticsSnapshot = rulesService.getAllRuleStatistics();
+ super.buildRows(table, maxEntries);
+ } finally {
+ statisticsSnapshot = null;
+ }
+ }
+
+ @Override
+ public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception {
+ try {
+ statisticsSnapshot = rulesService.getAllRuleStatistics();
+ super.buildCsvOutput(console, headers, limit);
+ } finally {
+ statisticsSnapshot = null;
+ }
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Rule rule = (Rule) item;
+ String ruleId = rule.getItemId();
+ Map<String, RuleStatistics> allRuleStatistics = statisticsSnapshot != null
+ ? statisticsSnapshot
+ : rulesService.getAllRuleStatistics();
+
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(rule.getMetadata().isEnabled() ? "x" : "");
+ rowData.add(rule.getMetadata().isHidden() ? "x" : "");
+ rowData.add(rule.getMetadata().isReadOnly() ? "x" : "");
+ rowData.add(ruleId);
+ rowData.add(rule.getMetadata().getScope());
+ rowData.add(rule.getMetadata().getName());
+ rowData.add(StringUtils.join(rule.getMetadata().getTags(), ","));
+ rowData.add(StringUtils.join(rule.getMetadata().getSystemTags(), ","));
+ RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId);
+ if (ruleStatistics != null) {
+ rowData.add(ruleStatistics.getExecutionCount());
+ rowData.add(ruleStatistics.getConditionsTime());
+ rowData.add(ruleStatistics.getActionsTime());
+ } else {
+ rowData.add(0L);
+ rowData.add(0L);
+ rowData.add(0L);
+ }
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class);
+ rulesService.setRule(rule);
+ return rule.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Rule rule = rulesService.getRule(id);
+ if (rule == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(rule, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ properties.put("itemId", id);
+ Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class);
+ rulesService.setRule(rule);
+ }
+
+ @Override
+ public void delete(String id) {
+ rulesService.removeRule(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Rule ID (string)",
+ "- name: Rule name",
+ "- condition: Rule condition object",
+ "- actions: List of rule actions",
+ "",
+ "Optional properties:",
+ "- description: Rule description",
+ "- priority: Rule priority (integer)",
+ "- metadata: Rule metadata",
+ "",
+ "Condition types:",
+ "- booleanCondition: Simple true/false condition",
+ "- profilePropertyCondition: Match profile property",
+ "- sessionPropertyCondition: Match session property",
+ "- eventPropertyCondition: Match event property",
+ "- pastEventCondition: Match past events",
+ "- matchAllCondition: Match all sub-conditions",
+ "- notCondition: Negate sub-condition",
+ "- orCondition: Match any sub-condition",
+ "- andCondition: Match all sub-conditions"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ if ("condition.type".equals(propertyName)) {
+ return CONDITION_TYPES.stream()
+ .filter(type -> type.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+ return List.of();
+ }
+
+ @Override
+ public List<String> completeId(String prefix) {
+ // Create a query to find rules that match the prefix
+ Query query = new Query();
+ query.setLimit(20); // Reasonable limit for auto-completion
+
+ // If prefix is not empty, use it to filter results
+ if (!prefix.isEmpty()) {
+ Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
+ condition.setParameter("comparisonOperator", "startsWith");
+ condition.setParameter("propertyName", "itemId");
+ condition.setParameter("propertyValue", prefix);
+ query.setCondition(condition);
+ } else {
+ // Otherwise, match all
+ Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(matchAllCondition);
+ }
+
+ // Execute the query and extract rule IDs
+ try {
+ PartialList<Metadata> metadatas = rulesService.getRuleMetadatas(query);
+ return metadatas.getList().stream()
+ .map(Metadata::getId)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ return List.of(); // Return empty list on error
+ }
+ }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
new file mode 100644
index 000000000..84baa0e08
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
@@ -0,0 +1,133 @@
+/*
+ * 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.shell.dev.commands.rules;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.rules.RuleStatistics;
+import org.apache.unomi.api.services.RulesService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A command to perform CRUD operations on rule statistics
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class RuleStatisticsCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "executionCount", "localExecutionCount", "conditionsTime", "localConditionsTime", "actionsTime", "localActionsTime", "lastSyncDate"
+ );
+
+ @Reference
+ private RulesService rulesService;
+
+ @Override
+ public String getObjectType() {
+ return "rulestats";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Executions", "Local Executions", "Conditions Time", "Local Conditions Time", "Actions Time", "Local Actions Time", "Last Sync"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ // Get all rules and their statistics
+ Map<String, RuleStatistics> statisticsMap = rulesService.getAllRuleStatistics();
+ List<RuleStatistics> statistics = new ArrayList<>(statisticsMap.values());
+ return new PartialList<>(statistics, 0, statistics.size(), statistics.size(), PartialList.Relation.EQUAL);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ RuleStatistics stats = (RuleStatistics) item;
+ return new Comparable[]{
+ stats.getItemId(),
+ stats.getExecutionCount(),
+ stats.getLocalExecutionCount(),
+ stats.getConditionsTime(),
+ stats.getLocalConditionsTime(),
+ stats.getActionsTime(),
+ stats.getLocalActionsTime(),
+ stats.getLastSyncDate() != null ? stats.getLastSyncDate().toString() : ""
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ RuleStatistics stats = rulesService.getRuleStatistics(id);
+ if (stats == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(stats, Map.class);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ throw new UnsupportedOperationException(
+ "Rule statistics are managed automatically by the rules engine and cannot be created via the shell.");
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ throw new UnsupportedOperationException(
+ "Rule statistics are managed automatically by the rules engine and cannot be updated via the shell.");
+ }
+
+ @Override
+ public void delete(String id) {
+ throw new UnsupportedOperationException(
+ "Rule statistics cannot be deleted individually via the shell (requested id: " + id
+ + "). Statistics are maintained by the rules engine.");
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Rule statistics are automatically managed by the rules engine and cannot be directly modified.",
+ "You can view statistics using the following properties:",
+ "",
+ "- itemId: The unique identifier of the rule statistics (matches the rule ID)",
+ "- executionCount: Total number of rule executions in the cluster",
+ "- localExecutionCount: Number of rule executions on this node since last sync",
+ "- conditionsTime: Total time spent evaluating conditions in the cluster (ms)",
+ "- localConditionsTime: Time spent evaluating conditions on this node since last sync (ms)",
+ "- actionsTime: Total time spent executing actions in the cluster (ms)",
+ "- localActionsTime: Time spent executing actions on this node since last sync (ms)",
+ "- lastSyncDate: Date of the last synchronization with the cluster",
+ "",
+ "Supported operations: list, read, help only (create, update, and delete are not supported)."
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
new file mode 100644
index 000000000..10bef1fca
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
@@ -0,0 +1,139 @@
+/*
+ * 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.shell.dev.commands.scopes;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Scope;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ScopeService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ScopeCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "restrictedVisibility", "metadata"
+ );
+
+ @Reference
+ private ScopeService scopeService;
+
+ @Reference
+ private PersistenceService persistenceService;
+
+ @Override
+ public String getObjectType() {
+ return "scope";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Activated",
+ "Hidden",
+ "Read-only",
+ "Identifier",
+ "Name",
+ "Description",
+ "Restricted",
+ "Tags",
+ "System tags"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return persistenceService.query(query.getCondition(), query.getSortby(), Scope.class, query.getOffset(), query.getLimit());
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Scope scope = (Scope) item;
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(scope.getMetadata().isEnabled() ? "x" : "");
+ rowData.add(scope.getMetadata().isHidden() ? "x" : "");
+ rowData.add(scope.getMetadata().isReadOnly() ? "x" : "");
+ rowData.add(scope.getItemId());
+ rowData.add(scope.getMetadata().getName());
+ rowData.add(scope.getMetadata().getDescription());
+ rowData.add(""); // No restricted visibility in Scope class
+ rowData.add(StringUtils.join(scope.getMetadata().getTags(), ","));
+ rowData.add(StringUtils.join(scope.getMetadata().getSystemTags(), ","));
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class);
+ scopeService.save(scope);
+ return scope.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Scope scope = scopeService.getScope(id);
+ if (scope == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(scope, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ properties.put("itemId", id);
+ Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class);
+ scopeService.save(scope);
+ }
+
+ @Override
+ public void delete(String id) {
+ scopeService.delete(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Scope ID (string)",
+ "- name: Scope name",
+ "",
+ "Optional properties:",
+ "- description: Scope description",
+ "- restrictedVisibility: Whether scope has restricted visibility (boolean)",
+ "- metadata: Scope metadata"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
new file mode 100644
index 000000000..78fd511dd
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
@@ -0,0 +1,142 @@
+/*
+ * 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.shell.dev.commands.scoring;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.segments.Scoring;
+import org.apache.unomi.api.services.SegmentService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class ScoringCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "scope", "name", "description", "elements", "metadata"
+ );
+
+ @Reference
+ private SegmentService segmentService;
+
+ @Override
+ public String getObjectType() {
+ return "scoring";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Activated",
+ "Hidden",
+ "Read-only",
+ "Identifier",
+ "Scope",
+ "Name",
+ "Tags",
+ "System tags"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return segmentService.getScoringMetadatas(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Metadata metadata = (Metadata) item;
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(metadata.isEnabled() ? "x" : "");
+ rowData.add(metadata.isHidden() ? "x" : "");
+ rowData.add(metadata.isReadOnly() ? "x" : "");
+ rowData.add(metadata.getId());
+ rowData.add(metadata.getScope());
+ rowData.add(metadata.getName());
+ rowData.add(StringUtils.join(metadata.getTags(), ","));
+ rowData.add(StringUtils.join(metadata.getSystemTags(), ","));
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Scoring scoring = OBJECT_MAPPER.convertValue(properties, Scoring.class);
+ segmentService.setScoringDefinition(scoring);
+ return scoring.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Scoring scoring = segmentService.getScoringDefinition(id);
+ if (scoring == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(scoring, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Scoring scoring = segmentService.getScoringDefinition(id);
+ if (scoring == null) {
+ throw new IllegalArgumentException("Scoring with id '" + id + "' not found");
+ }
+ Scoring updatedScoring = OBJECT_MAPPER.convertValue(properties, Scoring.class);
+ updatedScoring.setItemId(id);
+ segmentService.setScoringDefinition(updatedScoring);
+ }
+
+ @Override
+ public void delete(String id) {
+ segmentService.removeScoringDefinition(id, false);
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return "Required properties:\n" +
+ "- itemId (string): Unique identifier for the scoring\n" +
+ "- scope (string): Scope of the scoring\n" +
+ "- name (string): Human-readable name\n" +
+ "\n" +
+ "Optional properties:\n" +
+ "- description (string): Description of the scoring\n" +
+ "- elements (array): List of scoring elements, each containing:\n" +
+ " - condition (object): Condition that triggers the scoring element\n" +
+ " - value (number): Score value to add when condition is met\n" +
+ "- enabled (boolean): Whether the scoring is enabled (default: true)\n" +
+ "- hidden (boolean): Whether the scoring is hidden (default: false)\n" +
+ "- readOnly (boolean): Whether the scoring is read-only (default: false)\n" +
+ "- tags (array): List of tags for the scoring\n" +
+ "- systemTags (array): List of system tags for the scoring";
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
new file mode 100644
index 000000000..76446d359
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
@@ -0,0 +1,195 @@
+/*
+ * 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.shell.dev.commands.segments;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.segments.Segment;
+import org.apache.unomi.api.services.SegmentService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.apache.unomi.api.conditions.Condition;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class SegmentCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "scope", "condition", "metadata"
+ );
+ private static final List<String> CONDITION_TYPES = List.of(
+ "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition",
+ "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition",
+ "profileSegmentCondition", "scoringCondition"
+ );
+
+ @Reference
+ private SegmentService segmentService;
+
+ @Override
+ public String getObjectType() {
+ return "segment";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Activated",
+ "Hidden",
+ "Read-only",
+ "Identifier",
+ "Scope",
+ "Name",
+ "Tags",
+ "System tags"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return segmentService.getSegmentMetadatas(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Metadata segmentMetadata = (Metadata) item;
+ ArrayList<Comparable> rowData = new ArrayList<>();
+ rowData.add(segmentMetadata.isEnabled() ? "x" : "");
+ rowData.add(segmentMetadata.isHidden() ? "x" : "");
+ rowData.add(segmentMetadata.isReadOnly() ? "x" : "");
+ rowData.add(segmentMetadata.getId());
+ rowData.add(segmentMetadata.getScope());
+ rowData.add(segmentMetadata.getName());
+ rowData.add(StringUtils.join(segmentMetadata.getTags(), ","));
+ rowData.add(StringUtils.join(segmentMetadata.getSystemTags(), ","));
+ return rowData.toArray(new Comparable[0]);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class);
+ segmentService.setSegmentDefinition(segment);
+ return segment.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Segment segment = segmentService.getSegmentDefinition(id);
+ if (segment == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(segment, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ properties.put("itemId", id);
+ Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class);
+ segmentService.setSegmentDefinition(segment);
+ }
+
+ @Override
+ public void delete(String id) {
+ segmentService.removeSegmentDefinition(id, false);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Segment ID (string)",
+ "- name: Segment name",
+ "- condition: Segment condition object",
+ "",
+ "Optional properties:",
+ "- description: Segment description",
+ "- scope: Segment scope",
+ "- metadata: Segment metadata",
+ "",
+ "Condition types:",
+ "- booleanCondition: Simple true/false condition",
+ "- profilePropertyCondition: Match profile property",
+ "- sessionPropertyCondition: Match session property",
+ "- eventPropertyCondition: Match event property",
+ "- pastEventCondition: Match past events",
+ "- matchAllCondition: Match all sub-conditions",
+ "- notCondition: Negate sub-condition",
+ "- orCondition: Match any sub-condition",
+ "- andCondition: Match all sub-conditions",
+ "- profileSegmentCondition: Match profile segment",
+ "- scoringCondition: Match scoring value"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public List<String> completePropertyValue(String propertyName, String prefix) {
+ if ("condition.type".equals(propertyName)) {
+ return CONDITION_TYPES.stream()
+ .filter(type -> type.startsWith(prefix))
+ .collect(Collectors.toList());
+ } else if ("scope".equals(propertyName)) {
+ return List.of();
+ }
+ return List.of();
+ }
+
+ @Override
+ public List<String> completeId(String prefix) {
+ // Create a query to find segments that match the prefix
+ Query query = new Query();
+ query.setLimit(20); // Reasonable limit for auto-completion
+
+ try {
+ // If prefix is not empty, use it to filter results
+ if (!prefix.isEmpty()) {
+ Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
+ condition.setParameter("comparisonOperator", "startsWith");
+ condition.setParameter("propertyName", "itemId");
+ condition.setParameter("propertyValue", prefix);
+ query.setCondition(condition);
+ } else {
+ // Otherwise, match all
+ Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(matchAllCondition);
+ }
+
+ // Execute the query and extract segment IDs
+ PartialList<Metadata> metadatas = segmentService.getSegmentMetadatas(query);
+ return metadatas.getList().stream()
+ .map(Metadata::getId)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ return List.of(); // Return empty list on error
+ }
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
new file mode 100644
index 000000000..6be61be42
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
@@ -0,0 +1,128 @@
+/*
+ * 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.shell.dev.commands.sessions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Session;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A command to perform CRUD operations on sessions
+ */
+@Component(service = CrudCommand.class, immediate = true)
+public class SessionCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "profileId", "properties", "systemProperties", "timeStamp", "scope", "lastEventDate", "size", "duration", "originEventTypes", "originEventIds"
+ );
+
+ @Reference
+ private ProfileService profileService;
+
+ @Override
+ public String getObjectType() {
+ return "session";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[]{"ID", "Profile ID", "Scope", "Time Stamp", "Last Event", "Duration", "Size"};
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return profileService.searchSessions(query);
+ }
+
+ @Override
+ protected Comparable[] buildRow(Object item) {
+ Session session = (Session) item;
+ return new Comparable[]{
+ session.getItemId(),
+ session.getProfileId(),
+ session.getScope(),
+ session.getTimeStamp() != null ? session.getTimeStamp().toString() : "",
+ session.getLastEventDate() != null ? session.getLastEventDate().toString() : "",
+ String.valueOf(session.getDuration()),
+ String.valueOf(session.getSize())
+ };
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Session session = profileService.loadSession(id, null);
+ if (session == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(session, Map.class);
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Session session = OBJECT_MAPPER.convertValue(properties, Session.class);
+ profileService.saveSession(session);
+ return session.getItemId();
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ Session updatedSession = OBJECT_MAPPER.convertValue(properties, Session.class);
+ updatedSession.setItemId(id);
+ profileService.saveSession(updatedSession);
+ }
+
+ @Override
+ public void delete(String id) {
+ profileService.deleteSession(id);
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: The unique identifier of the session",
+ "- profileId: The identifier of the associated profile",
+ "- timeStamp: The session creation timestamp (ISO-8601 format)",
+ "- scope: The scope of the session",
+ "",
+ "Optional properties:",
+ "- properties: A map of custom properties for the session",
+ "- systemProperties: A map of system properties for internal use",
+ "- lastEventDate: The date of the last event in the session (ISO-8601 format)",
+ "- size: The size of the session",
+ "- duration: The duration of the session in milliseconds",
+ "- originEventTypes: List of event types that caused the session creation",
+ "- originEventIds: List of event IDs that caused the session creation"
+ );
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
new file mode 100644
index 000000000..f97bed28d
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
@@ -0,0 +1,120 @@
+/*
+ * 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.shell.dev.commands.topics;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Topic;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.TopicService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.shell.dev.services.BaseCrudCommand;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component(service = CrudCommand.class, immediate = true)
+public class TopicCrudCommand extends BaseCrudCommand {
+
+ private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
+ private static final List<String> PROPERTY_NAMES = List.of(
+ "itemId", "name", "description", "parentId", "metadata"
+ );
+
+ @Reference
+ private TopicService topicService;
+
+ @Override
+ public String getObjectType() {
+ return "topic";
+ }
+
+ @Override
+ protected String[] getHeadersWithoutTenant() {
+ return new String[] {
+ "Identifier",
+ "Name",
+ "Scope"
+ };
+ }
+
+ @Override
+ protected PartialList<?> getItems(Query query) {
+ return topicService.search(query);
+ }
+
+ @Override
+ protected String[] buildRow(Object item) {
+ Topic topic = (Topic) item;
+ return new String[] {
+ topic.getItemId(),
+ topic.getName(),
+ topic.getScope()
+ };
+ }
+
+ @Override
+ public String create(Map<String, Object> properties) {
+ Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class);
+ topicService.save(topic);
+ return topic.getItemId();
+ }
+
+ @Override
+ public Map<String, Object> read(String id) {
+ Topic topic = topicService.load(id);
+ if (topic == null) {
+ return null;
+ }
+ return OBJECT_MAPPER.convertValue(topic, Map.class);
+ }
+
+ @Override
+ public void update(String id, Map<String, Object> properties) {
+ properties.put("itemId", id);
+ Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class);
+ topicService.save(topic);
+ }
+
+ @Override
+ public void delete(String id) {
+ topicService.delete(id);
+ }
+
+ @Override
+ public String getPropertiesHelp() {
+ return String.join("\n",
+ "Required properties:",
+ "- itemId: Topic ID (string)",
+ "- name: Topic name",
+ "",
+ "Optional properties:",
+ "- description: Topic description",
+ "- parentId: Parent topic ID",
+ "- metadata: Topic metadata"
+ );
+ }
+
+ @Override
+ public List<String> completePropertyNames(String prefix) {
+ return filterPropertyNames(PROPERTY_NAMES, prefix);
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java
new file mode 100644
index 000000000..5581f519b
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+import java.io.PrintStream;
+import java.util.List;
+
+/**
+ * Base class for completers
+ */
+public abstract class BaseCompleter<T extends Item> implements Completer {
+ protected static final int DEFAULT_LIMIT = 50;
+
+ @Reference
+ protected PersistenceService persistenceService;
+
+ @Reference
+ protected DefinitionsService definitionsService;
+
+ protected abstract Class<T> getItemType();
+
+ protected abstract String getSortBy();
+
+ @Override
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ StringsCompleter delegate = new StringsCompleter();
+
+ try {
+ Query query = new Query();
+ query.setLimit(DEFAULT_LIMIT);
+ query.setSortby(getSortBy());
+
+ Condition condition = new Condition();
+ condition.setConditionType(definitionsService.getConditionType("matchAllCondition"));
+ query.setCondition(condition);
+
+ PartialList<T> items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit());
+
+ for (T item : items.getList()) {
+ delegate.getStrings().add(item.getItemId());
+ }
+
+ return delegate.complete(session, commandLine, candidates);
+ } catch (Exception e) {
+ // Log error but don't fail completion
+ // Note: Printing during completion can interfere with completion, but using console for consistency
+ PrintStream console = session.getConsole();
+ console.println("Error: Error completing items: " + e.getMessage());
+ return -1;
+ }
+ }
+
+ public void setPersistenceService(PersistenceService persistenceService) {
+ this.persistenceService = persistenceService;
+ }
+
+ public void setDefinitionsService(DefinitionsService definitionsService) {
+ this.definitionsService = definitionsService;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
new file mode 100644
index 000000000..71f6df412
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Init;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+@Service
+public class IdCompleter implements Completer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(IdCompleter.class.getName());
+
+ @Reference
+ private BundleContext bundleContext;
+
+ @Init
+ public void init() {
+ LOGGER.debug("IdCompleter initialized");
+ }
+
+ @Override
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ // Get the operation and type from the command line
+ // args[0] = "crud" (command name), args[1] = operation, args[2] = type, args[3+] = remaining
+ String operation = null;
+ String type = null;
+ String[] args = commandLine.getArguments();
+ if (args.length > 1) {
+ operation = args[1];
+ }
+ if (args.length > 2) {
+ type = args[2];
+ }
+ if (type == null) {
+ return -1;
+ }
+
+ // Determine if ID completion is appropriate based on operation
+ // ID completion is only appropriate for: read, delete, and update (first argument)
+ if (operation != null) {
+ String operationLower = operation.toLowerCase();
+ if (!"read".equals(operationLower) && !"delete".equals(operationLower) && !"update".equals(operationLower)) {
+ // For create, list, help - ID completion is not appropriate
+ return -1;
+ }
+ }
+
+ // Determine which argument we're completing based on args.length
+ // args[0] = "crud" (command name)
+ // args[1] = operation
+ // args[2] = type
+ // args[3+] = remaining (multi-valued)
+ // For read/delete: remaining[0] = ID (complete when args.length == 3, i.e., we're at remaining[0])
+ // For update: remaining[0] = ID, remaining[1] = JSON
+ // - Complete IDs when args.length == 3 (completing remaining[0], which is the ID)
+ // - Don't complete IDs when args.length >= 4 (completing remaining[1], which is JSON)
+
+ // For update operation, if args.length >= 4, we're past the ID argument
+ // and are completing the JSON part, so don't complete IDs
+ if (operation != null && "update".equals(operation.toLowerCase()) && args.length >= 4) {
+ // We're past the ID argument, so we're completing JSON - don't complete IDs
+ return -1;
+ }
+
+ // For read/delete/update with args.length == 3, we're completing the ID (remaining[0])
+ // The completer is attached to the "remaining" argument, so it will be called
+ // when we're at that position
+
+ // Find the CrudCommand for this type
+ try {
+ ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+ if (refs != null) {
+ for (ServiceReference<?> ref : refs) {
+ CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+ try {
+ if (cmd.getObjectType().equals(type)) {
+ // Get the prefix from what the user has typed so far
+ // StringsCompleter will handle the final matching, but we need prefix for server-side filtering
+ String prefix = extractPrefixFromBuffer(commandLine);
+
+ List<String> completions = cmd.completeId(prefix);
+
+ StringsCompleter delegate = new StringsCompleter();
+ delegate.getStrings().addAll(completions);
+ return delegate.complete(session, commandLine, candidates);
+ }
+ } finally {
+ bundleContext.ungetService(ref);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Log error but continue
+ // Note: Printing during completion can interfere with completion, but using console for consistency
+ // Only log if it's a serious error - avoid logging during normal completion
+ LOGGER.debug("Error getting completions", e);
+ }
+
+ return -1;
+ }
+
+ /**
+ * Extract the prefix from the command line buffer for completion.
+ * This extracts the last word being typed, skipping options that start with '-'.
+ *
+ * @param commandLine the command line
+ * @return the prefix to use for filtering completions, or empty string if none
+ */
+ private String extractPrefixFromBuffer(CommandLine commandLine) {
+ String buffer = commandLine.getBuffer();
+ if (buffer == null || buffer.trim().isEmpty()) {
+ return "";
+ }
+
+ // Get the last word from the buffer (the current value being typed)
+ String trimmed = buffer.trim();
+ int lastSpace = trimmed.lastIndexOf(' ');
+ if (lastSpace < 0 || lastSpace >= trimmed.length() - 1) {
+ return "";
+ }
+
+ String prefix = trimmed.substring(lastSpace + 1);
+ // Skip if it looks like an option
+ if (prefix.startsWith("-")) {
+ return "";
+ }
+
+ return prefix;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
similarity index 54%
rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
index 7a228be9f..7ae6e3c35 100644
--- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java
@@ -14,25 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.unomi.shell.commands;
+package org.apache.unomi.shell.dev.completers;
-import org.apache.karaf.shell.api.action.Action;
-import org.apache.karaf.shell.api.action.Command;
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.services.RulesService;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
-@Command(scope = "unomi", name = "rule-reset-stats", description = "This command will reset the rule statistics")
-@Service
-public class RuleResetStats implements Action {
+import java.util.List;
- @Reference
- RulesService rulesService;
+@Service
+public class OperationCompleter implements Completer {
+ private static final List<String> OPERATIONS = List.of("create", "read", "update", "delete", "list", "help");
@Override
- public Object execute() throws Exception {
- rulesService.resetAllRuleStatistics();
- System.out.println("Rule statistics successfully reset.");
- return null;
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ StringsCompleter delegate = new StringsCompleter();
+ delegate.getStrings().addAll(OPERATIONS);
+ return delegate.complete(session, commandLine, candidates);
}
}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java
new file mode 100644
index 000000000..4a962c9c9
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java
@@ -0,0 +1,66 @@
+/*
+ * 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.shell.dev.completers;
+
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.apache.unomi.shell.dev.services.CrudCommand;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import java.io.PrintStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class TypeCompleter implements Completer {
+
+ @Reference
+ private BundleContext bundleContext;
+
+ @Override
+ public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+ Set<String> types = new HashSet<>();
+ try {
+ ServiceReference<?>[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
+ if (refs != null) {
+ for (ServiceReference<?> ref : refs) {
+ CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
+ try {
+ types.add(cmd.getObjectType());
+ } finally {
+ bundleContext.ungetService(ref);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Log error but continue
+ // Note: Printing during completion can interfere with completion, but using console for consistency
+ PrintStream console = session.getConsole();
+ console.println("Error: Error getting object types: " + e.getMessage());
+ }
+
+ StringsCompleter delegate = new StringsCompleter();
+ delegate.getStrings().addAll(types);
+ return delegate.complete(session, commandLine, candidates);
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
new file mode 100644
index 000000000..97fee6b69
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
@@ -0,0 +1,399 @@
+/*
+ * 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.shell.dev.services;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.common.DataTable;
+import org.apache.unomi.shell.dev.commands.ListCommandSupport;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Base class for CRUD command implementations that provides common functionality
+ * for listing objects in a tabular format.
+ */
+public abstract class BaseCrudCommand extends ListCommandSupport implements CrudCommand {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BaseCrudCommand.class.getName());
+
+ @Reference
+ protected volatile DefinitionsService definitionsService;
+
+ @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false)
+ protected int maxEntries = 100;
+
+ @Option(name = "--csv", description = "Output in CSV format", required = false)
+ protected boolean csv;
+
+ @Override
+ protected DataTable buildDataTable() {
+ PrintStream console = getConsole();
+ try {
+ Query query = buildQuery(maxEntries);
+ PartialList<?> items = getItems(query);
+
+ printPaginationWarning(items, console);
+
+ DataTable dataTable = new DataTable();
+ for (Object item : items.getList()) {
+ Comparable[] rowWithTenant = buildRowWithTenant(item);
+ dataTable.addRow(rowWithTenant);
+ }
+
+ return dataTable;
+ } catch (Exception e) {
+ LOGGER.error("Error building data table", e);
+ console.println("Error: " + e.getMessage());
+ return new DataTable();
+ }
+ }
+
+ /**
+ * Get the sort criteria for the query.
+ * Default implementation sorts by last modification date.
+ * Override to provide different sorting.
+ *
+ * @return sort criteria (e.g., "metadata.lastModified:desc")
+ */
+ protected String getSortBy() {
+ return "metadata.lastModified:desc";
+ }
+
+ /**
+ * Get items using the provided query.
+ * Implementations must override this to use their specific service.
+ *
+ * @param query the query to execute
+ * @return partial list of items
+ */
+ protected abstract PartialList<?> getItems(Query query);
+
+ /**
+ * Build a row for the data table from an item.
+ * Implementations must override this to extract the relevant properties.
+ *
+ * @param item the item to convert to a row
+ * @return array of values for the row
+ */
+ protected abstract Comparable[] buildRow(Object item);
+
+ /**
+ * Get the column headers for the list output table.
+ * This implementation automatically prepends "Tenant" as the first column header,
+ * matching how tenantId is automatically prepended to rows in buildDataTable() and buildRows().
+ * Subclasses should implement getHeadersWithoutTenant() to provide their specific headers.
+ *
+ * Subclasses can override this method to provide custom header handling (e.g., to skip the tenant column
+ * for commands like TenantCrudCommand where it would be redundant).
+ *
+ * @return array of column headers with "Tenant" as the first element (unless overridden)
+ */
+ @Override
+ public String[] getHeaders() {
+ String[] headersWithoutTenant = getHeadersWithoutTenant();
+ String[] headersWithTenant = new String[headersWithoutTenant.length + 1];
+ headersWithTenant[0] = "Tenant";
+ System.arraycopy(headersWithoutTenant, 0, headersWithTenant, 1, headersWithoutTenant.length);
+ return headersWithTenant;
+ }
+
+ /**
+ * Get the column headers for the list output table without the "Tenant" column.
+ * Subclasses must implement this method to provide their specific headers.
+ * The "Tenant" column will be automatically prepended by getHeaders().
+ *
+ * @return array of column headers (without "Tenant")
+ */
+ protected abstract String[] getHeadersWithoutTenant();
+
+ /**
+ * Build a query with matchAllCondition and sort criteria.
+ * This is a common helper method used by buildDataTable(), buildRows(), buildCsvOutput(), and completeId().
+ *
+ * @param limit maximum number of entries
+ * @return the configured query
+ * @throws Exception if definitions service is not available or matchAllCondition cannot be found
+ */
+ protected Query buildQuery(int limit) throws Exception {
+ Query query = new Query();
+ query.setLimit(limit);
+
+ if (definitionsService == null) {
+ throw new Exception("Definitions service is not available");
+ }
+
+ ConditionType matchAllConditionType = definitionsService.getConditionType("matchAllCondition");
+ if (matchAllConditionType == null) {
+ throw new Exception("No matchAllCondition available");
+ }
+
+ Condition matchAllCondition = new Condition(matchAllConditionType);
+ query.setCondition(matchAllCondition);
+ query.setSortby(getSortBy());
+
+ return query;
+ }
+
+ /**
+ * Build a row array with tenant ID prepended as the first element.
+ * This is a common helper method used by buildDataTable(), buildRows(), and buildCsvOutput().
+ *
+ * @param item the item to build a row from
+ * @return array with tenant ID as first element, followed by row data
+ */
+ protected Comparable[] buildRowWithTenant(Object item) {
+ Comparable[] rowData = buildRow(item);
+ String tenantId = getTenantIdFromItem(item);
+
+ // Create a new array with tenantId as the first element
+ Comparable[] rowWithTenant = new Comparable[rowData.length + 1];
+ rowWithTenant[0] = tenantId;
+ System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length);
+
+ return rowWithTenant;
+ }
+
+ /**
+ * Print pagination warning if not all items were retrieved.
+ * This is a common helper method used by buildDataTable() and buildRows().
+ *
+ * @param items the partial list of items
+ * @param console console for output
+ */
+ protected void printPaginationWarning(PartialList<?> items, PrintStream console) {
+ if (items.getList().size() != items.getTotalSize()) {
+ console.println("WARNING : Only the first " + items.getPageSize() + " items have been retrieved, there are " + items.getTotalSize() + " items registered in total. Use the maxEntries parameter to retrieve more items");
+ }
+ }
+
+ @Override
+ public void buildRows(ShellTable table, int maxEntries) {
+ PrintStream console = getConsole();
+ try {
+ Query query = buildQuery(maxEntries);
+ PartialList<?> items = getItems(query);
+
+ printPaginationWarning(items, console);
+
+ for (Object item : items.getList()) {
+ Comparable[] rowWithTenant = buildRowWithTenant(item);
+ table.addRow().addContent(rowWithTenant);
+ }
+ } catch (Exception e) {
+ console.println("Error: " + e.getMessage());
+ LOGGER.error("Error building rows", e);
+ }
+ }
+
+ /**
+ * Generate CSV output directly using commons-csv API.
+ * This method uses the same logic as buildRows() but outputs as CSV.
+ *
+ * @param console console for output
+ * @param headers column headers
+ * @param limit maximum number of entries
+ * @throws Exception if generation fails
+ */
+ public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception {
+ Query query = buildQuery(limit);
+ PartialList<?> items = getItems(query);
+
+ // Generate CSV directly using commons-csv
+ CSVFormat csvFormat = CSVFormat.DEFAULT;
+ CSVPrinter printer = csvFormat.print(console);
+
+ // Print header
+ printer.printRecord((Object[]) headers);
+
+ // Print data rows
+ for (Object item : items.getList()) {
+ Comparable[] rowWithTenant = buildRowWithTenant(item);
+
+ // Convert to List<String> for CSV printer
+ List<String> row = new ArrayList<>();
+ for (Comparable<?> cell : rowWithTenant) {
+ row.add(cell != null ? cell.toString() : "");
+ }
+ printer.printRecord(row.toArray());
+ }
+
+ printer.flush();
+ }
+
+ /**
+ * Extract the tenant ID from an item.
+ *
+ * @param item the item to extract tenant ID from
+ * @return the tenant ID or a default value if it can't be determined
+ */
+ protected String getTenantIdFromItem(Object item) {
+ // Tenant column reserved for when tenant support is merged (Item#getTenantId, Tenant type, etc.).
+ return "n/a";
+ }
+
+ /**
+ * Default implementation of ID completion for all CRUD commands.
+ * This method fetches a limited number of items and filters their IDs based on the given prefix.
+ *
+ * @param prefix the prefix to filter IDs by
+ * @return a list of matching item IDs
+ */
+ @Override
+ public List<String> completeId(String prefix) {
+ try {
+ // Create a query with increased limit to provide more completions
+ Query query = buildQuery(50); // Higher limit for completions
+
+ // Get items using the appropriate service method
+ PartialList<?> items = getItems(query);
+
+ // Extract IDs from the items
+ List<String> ids = new ArrayList<>();
+ for (Object item : items.getList()) {
+ String id = extractIdFromItem(item);
+ if (id != null && (prefix.isEmpty() || id.startsWith(prefix))) {
+ ids.add(id);
+ }
+ }
+
+ return ids;
+ } catch (Exception e) {
+ LOGGER.error("Error completing IDs", e);
+ return List.of();
+ }
+ }
+
+ /**
+ * Get the console PrintStream, falling back to System.out if session is not available.
+ * This is needed because CrudCommand services retrieved via bundleContext.getService()
+ * may not have session injected (they're not shell command instances).
+ *
+ * @return PrintStream for console output
+ */
+ protected PrintStream getConsole() {
+ if (session != null) {
+ return session.getConsole();
+ }
+ return System.out;
+ }
+
+ /**
+ * Apply pagination to a list of items based on query parameters.
+ * This is a helper method for implementations that need to paginate in-memory lists.
+ *
+ * @param items the full list of items
+ * @param query the query with offset and limit parameters
+ * @param <T> the type of items in the list
+ * @return a PartialList with paginated results
+ */
+ protected <T> PartialList<T> paginateList(List<T> items, Query query) {
+ Integer offset = query.getOffset();
+ Integer limit = query.getLimit();
+ int start = offset == null ? 0 : offset;
+ int size = limit == null ? items.size() : limit;
+ int end = Math.min(start + size, items.size());
+
+ List<T> pagedItems = items.subList(start, end);
+ return new PartialList<>(pagedItems, start, pagedItems.size(), items.size(), PartialList.Relation.EQUAL);
+ }
+
+ /**
+ * Filter property names by prefix. This is a helper method for completePropertyNames implementations.
+ *
+ * @param propertyNames the list of property names to filter
+ * @param prefix the prefix to filter by
+ * @return filtered list of property names
+ */
+ protected List<String> filterPropertyNames(List<String> propertyNames, String prefix) {
+ return propertyNames.stream()
+ .filter(name -> name.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Extract the ID from an item. This method attempts to extract the ID using common patterns.
+ * Subclasses can override this method to provide specialized ID extraction for specific item types.
+ *
+ * @param item the item to extract the ID from
+ * @return the extracted ID, or null if it couldn't be extracted
+ */
+ protected String extractIdFromItem(Object item) {
+ // Handle Item subclasses
+ if (item instanceof Item) {
+ return ((Item) item).getItemId();
+ }
+
+ // Handle Metadata objects
+ if (item instanceof Metadata) {
+ return ((Metadata) item).getId();
+ }
+
+ // Try reflection as a fallback
+ try {
+ // Try common getter method names for ID
+ for (String methodName : new String[]{"getId", "getItemId", "getIdentifier", "getKey", "getName"}) {
+ try {
+ Method method = item.getClass().getMethod(methodName);
+ Object result = method.invoke(item);
+ if (result != null) {
+ return result.toString();
+ }
+ } catch (NoSuchMethodException e) {
+ // Method doesn't exist, try the next one
+ }
+ }
+
+ // Try direct field access as a last resort
+ for (String fieldName : new String[]{"id", "itemId", "identifier", "key", "name"}) {
+ try {
+ Field field = item.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(item);
+ if (value != null) {
+ return value.toString();
+ }
+ } catch (NoSuchFieldException e) {
+ // Field doesn't exist, try the next one
+ }
+ }
+ } catch (Exception e) {
+ // Ignore reflection errors
+ }
+
+ return null;
+ }
+}
diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
new file mode 100644
index 000000000..4c088e906
--- /dev/null
+++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
@@ -0,0 +1,206 @@
+/*
+ * 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.shell.dev.services;
+
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for implementing CRUD (Create, Read, Update, Delete) operations in Karaf shell commands for Unomi objects.
+ * This interface is designed to be implemented by OSGi components that provide shell command functionality for different
+ * types of Unomi objects (e.g., rules, segments, profiles, events).
+ *
+ * Implementations must:
+ * 1. Be annotated with @Component(service = CrudCommand.class, immediate = true)
+ * 2. Reference the appropriate Unomi service using @Reference
+ * 3. Handle JSON serialization/deserialization of objects
+ * 4. Provide property name and value completion for shell command auto-completion
+ * 5. Use server-side pagination and sorting in list operations to handle large datasets
+ * 6. Extend ListCommandSupport to implement rich tabular output
+ *
+ * The list functionality is provided by extending ListCommandSupport and implementing:
+ * 1. getHeaders() - Define column headers for the table
+ * 2. buildDataTable() - Build table data using Query with proper sorting and pagination
+ * 3. getSortBy() - Define sort criteria (default: metadata.lastModified:desc)
+ * 4. buildRow() - Convert an item to a table row
+ *
+ * Example usage in shell:
+ * unomi:crud create rule --file rule.json
+ * unomi:crud create rule -d={"itemId":"rule_id","enabled":true}
+ * unomi:crud read rule -i rule_id
+ * unomi:crud update rule -i rule_id --file updated_rule.json
+ * unomi:crud update rule -i rule_id -d={"itemId":"rule_id","enabled":false}
+ * unomi:crud delete rule -i rule_id
+ * unomi:crud list rule [--csv]
+ * unomi:crud help rule
+ */
+public interface CrudCommand {
+ /**
+ * Get the type of object this command handles. This is used to register the command
+ * for a specific object type in the Unomi shell command system.
+ *
+ * @return the object type identifier (e.g., "rule", "segment", "profile", "event")
+ */
+ String getObjectType();
+
+ /**
+ * Create a new object in Unomi from a map of properties. The properties are typically
+ * deserialized from a JSON file provided by the user.
+ *
+ * Implementations should:
+ * 1. Convert the properties map to the appropriate Unomi object type
+ * 2. Validate required properties are present
+ * 3. Set any default values or metadata
+ * 4. Save the object using the appropriate Unomi service
+ *
+ * @param properties the object properties from JSON
+ * @return the ID of the created object
+ * @throws IllegalArgumentException if required properties are missing or invalid
+ */
+ String create(Map<String, Object> properties);
+
+ /**
+ * Read an object by ID and return its properties. The properties will be serialized
+ * to JSON for display to the user.
+ *
+ * Implementations should:
+ * 1. Load the object using the appropriate Unomi service
+ * 2. Return null if the object doesn't exist
+ * 3. Convert the object to a map of properties
+ *
+ * @param id the object ID
+ * @return map of object properties, or null if not found
+ */
+ Map<String, Object> read(String id);
+
+ /**
+ * Update an existing object with new properties. The properties are typically
+ * deserialized from a JSON file provided by the user.
+ *
+ * Implementations should:
+ * 1. Ensure the ID in properties matches the target ID
+ * 2. Convert the properties map to the appropriate Unomi object type
+ * 3. Validate required properties are present
+ * 4. Update the object using the appropriate Unomi service
+ *
+ * @param id the object ID to update
+ * @param properties the new object properties from JSON
+ * @throws IllegalArgumentException if required properties are missing or invalid
+ */
+ void update(String id, Map<String, Object> properties);
+
+ /**
+ * Delete an object by ID.
+ *
+ * Implementations should:
+ * 1. Delete the object using the appropriate Unomi service
+ * 2. Handle any cleanup or cascading deletes if necessary
+ *
+ * @param id the object ID to delete
+ */
+ void delete(String id);
+
+ /**
+ * Get help text describing the properties supported by this object type.
+ * This is displayed when the user runs the help command.
+ *
+ * The help text should include:
+ * 1. Required properties with descriptions
+ * 2. Optional properties with descriptions
+ * 3. Property types and formats
+ * 4. Examples or additional notes
+ *
+ * @return formatted help text for object properties
+ */
+ String getPropertiesHelp();
+
+ /**
+ * Get completions for object IDs based on a prefix. Used for shell command
+ * auto-completion when the user is entering an object ID.
+ *
+ * Implementations should use their service's query capabilities to efficiently
+ * search for matching IDs rather than loading all objects.
+ *
+ * @param prefix the current input prefix to filter completions
+ * @return list of matching object IDs
+ */
+ default List<String> completeId(String prefix) {
+ // Implementations should override this with an efficient query
+ return List.of();
+ }
+
+ /**
+ * Get completions for property names based on a prefix. Used for shell command
+ * auto-completion when the user is editing a JSON file.
+ *
+ * Implementations should:
+ * 1. Define a static list of valid property names
+ * 2. Filter the list by the given prefix
+ *
+ * @param prefix the current input prefix to filter completions
+ * @return list of matching property names
+ */
+ default List<String> completePropertyNames(String prefix) {
+ return List.of(); // Default implementation returns no completions
+ }
+
+ /**
+ * Get completions for property values based on the property name and prefix.
+ * Used for shell command auto-completion when the user is editing a JSON file.
+ *
+ * Implementations should:
+ * 1. Handle specific property names that have predefined values
+ * 2. Filter the possible values by the given prefix
+ * 3. Return empty list for properties without predefined values
+ *
+ * @param propertyName the property being completed
+ * @param prefix the current input prefix to filter completions
+ * @return list of possible values for the property
+ */
+ default List<String> completePropertyValue(String propertyName, String prefix) {
+ return List.of(); // Default implementation returns no completions
+ }
+
+ /**
+ * Get the column headers for the list output table.
+ *
+ * @return array of column headers
+ */
+ String[] getHeaders();
+
+ /**
+ * Build the rows for the list output table.
+ *
+ * @param table the table to add rows to
+ * @param maxEntries maximum number of entries to include
+ */
+ void buildRows(ShellTable table, int maxEntries);
+
+ /**
+ * Generate CSV output directly using commons-csv API.
+ * This method uses the same logic as buildRows() but outputs as CSV.
+ *
+ * @param console console for output
+ * @param headers column headers
+ * @param limit maximum number of entries
+ * @throws Exception if generation fails
+ */
+ void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception;
+}