From 94ec7a3595977d7e1117693049e41cf79d33e01e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 6 May 2026 06:17:46 +0200 Subject: [PATCH 1/4] UNOMI-879 Unified CRUD shell developer commands - Replace legacy per-type list/view commands with unified unomi:crud routing, shared bases (BaseCrudCommand, CrudCommand), completers, and type-specific CRUD implementations under org.apache.unomi.shell.dev. - Relocate deployment/tail/watch helpers into dev.commands package. - EventService/ProfileService: add deleteEvent/deleteSession; implementations delegate to PersistenceService.remove (aligned with unomi-3-dev). - BOM: dependency adjustments needed by shell-dev-commands build. --- .../unomi/api/services/EventService.java | 7 + .../unomi/api/services/ProfileService.java | 7 + bom/pom.xml | 6 + .../impl/events/EventServiceImpl.java | 5 + .../impl/profiles/ProfileServiceImpl.java | 5 + tools/shell-dev-commands/pom.xml | 13 + .../unomi/shell/commands/ActionList.java | 64 -- .../unomi/shell/commands/ActionView.java | 48 -- .../unomi/shell/commands/ConditionList.java | 64 -- .../unomi/shell/commands/ConditionView.java | 50 -- .../shell/commands/DeployDefinition.java | 113 ---- .../unomi/shell/commands/EventList.java | 90 --- .../unomi/shell/commands/EventSearch.java | 99 --- .../unomi/shell/commands/EventView.java | 60 -- .../unomi/shell/commands/ProfileList.java | 84 --- .../unomi/shell/commands/ProfileView.java | 48 -- .../apache/unomi/shell/commands/RuleList.java | 108 ---- .../unomi/shell/commands/SegmentList.java | 73 --- .../unomi/shell/commands/SegmentRemove.java | 56 -- .../unomi/shell/commands/SegmentView.java | 48 -- .../unomi/shell/commands/SessionList.java | 79 --- .../shell/commands/UndeployDefinition.java | 111 ---- .../shell/dev/actions/UnomiCrudCommand.java | 608 ++++++++++++++++++ .../unomi/shell/dev/commands/BaseCommand.java | 53 ++ .../shell/dev/commands/BaseListCommand.java | 74 +++ .../shell/dev/commands/BaseSimpleCommand.java | 61 ++ .../shell/dev/commands/CommandUtils.java | 80 +++ .../shell/dev/commands/DeployDefinition.java | 137 ++++ .../commands/DeploymentCommandSupport.java | 87 ++- .../shell/{ => dev}/commands/EventTail.java | 16 +- .../commands/ListCommandSupport.java | 15 +- .../dev/commands/RemoveCommandSupport.java | 51 ++ .../{ => dev}/commands/RuleResetStats.java | 7 +- .../shell/{ => dev}/commands/RuleTail.java | 16 +- .../shell/{ => dev}/commands/RuleWatch.java | 18 +- .../commands/TailCommandSupport.java | 12 +- .../shell/dev/commands/TailCommandUtils.java | 91 +++ .../dev/commands/UndeployDefinition.java | 97 +++ .../actions/ActionTypeCrudCommand.java | 157 +++++ .../campaigns/CampaignCrudCommand.java | 151 +++++ .../campaigns/CampaignEventCrudCommand.java | 143 ++++ .../conditions/ConditionTypeCrudCommand.java | 153 +++++ .../commands/consents/ConsentCrudCommand.java | 244 +++++++ .../dev/commands/events/EventCrudCommand.java | 167 +++++ .../dev/commands/goals/GoalCrudCommand.java | 141 ++++ .../commands/personas/PersonaCrudCommand.java | 150 +++++ .../profiles/ProfileAliasCrudCommand.java | 155 +++++ .../commands/profiles/ProfileCrudCommand.java | 183 ++++++ .../properties/PropertyTypeCrudCommand.java | 174 +++++ .../dev/commands/rules/RuleCrudCommand.java | 209 ++++++ .../rules/RuleStatisticsCrudCommand.java | 135 ++++ .../dev/commands/scopes/ScopeCrudCommand.java | 139 ++++ .../commands/scoring/ScoringCrudCommand.java | 142 ++++ .../commands/segments/SegmentCrudCommand.java | 195 ++++++ .../commands/sessions/SessionCrudCommand.java | 129 ++++ .../dev/commands/topics/TopicCrudCommand.java | 120 ++++ .../shell/dev/completers/BaseCompleter.java | 86 +++ .../shell/dev/completers/IdCompleter.java | 156 +++++ .../dev/completers/OperationCompleter.java | 37 ++ .../shell/dev/completers/TypeCompleter.java | 66 ++ .../shell/dev/services/BaseCrudCommand.java | 400 ++++++++++++ .../unomi/shell/dev/services/CrudCommand.java | 206 ++++++ 62 files changed, 5241 insertions(+), 1258 deletions(-) delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/DeploymentCommandSupport.java (72%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/EventTail.java (81%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/ListCommandSupport.java (87%) create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleResetStats.java (86%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleTail.java (80%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleWatch.java (82%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/TailCommandSupport.java (93%) create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java 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 fb6d60d70b..64ca1beebd 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 03da6ce9be..bd4c537068 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,13 @@ default Session loadSession(String sessionId) { */ void removeProfileSessions(String profileId); + /** + * Deletes the session identified by the given identifier from persistence. + * + * @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/bom/pom.xml b/bom/pom.xml index 17b0f37529..c63868276b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -270,6 +270,12 @@ httpclient-osgi ${httpclient-osgi.version} + + org.apache.httpcomponents + httpclient-osgi + ${httpclient-osgi.version} + bundle + org.apache.kafka kafka-clients 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 f345466c7d..60680924e6 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 void removeProfileEvents(String profileId){ 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 9673a707cd..e6b74c9172 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 void removeProfileSessions(String profileId) { 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 27b0b641f3..e62a637589 100644 --- a/tools/shell-dev-commands/pom.xml +++ b/tools/shell-dev-commands/pom.xml @@ -92,6 +92,7 @@ org.apache.httpcomponents httpclient-osgi + bundle provided @@ -99,6 +100,10 @@ commons-lang3 provided + + org.apache.commons + commons-csv + junit @@ -115,6 +120,14 @@ * + + org.apache.unomi.shell.dev.services, + org.apache.unomi.shell.dev.commands + + <_dsannotations>* + <_dsannotations-options>inherit + <_metatypeannotations>* + <_metatypeannotations-options>version;nested 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 7839cb7c80..0000000000 --- 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 allActions = definitionsService.getAllActionTypes(); - - DataTable dataTable = new DataTable(); - - for (ActionType actionType : allActions) { - ArrayList 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 939e7f9d1d..0000000000 --- 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 83f6debc5a..0000000000 --- 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 allConditionTypes = definitionsService.getAllConditionTypes(); - - DataTable dataTable = new DataTable(); - - for (ConditionType conditionType : allConditionTypes) { - ArrayList 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 03efe9d48a..0000000000 --- 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 9365de8a98..0000000000 --- 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 b996a7085a..0000000000 --- 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 lastEvents = eventService.search(query); - DataTable dataTable = new DataTable(); - for (Event event : lastEvents.getList()) { - ArrayList 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 082f1d3d30..0000000000 --- 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 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 lastEvents = eventService.searchEvents(booleanCondition, 0, maxEntries); - DataTable dataTable = new DataTable(); - for (Event event : lastEvents.getList()) { - ArrayList 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 10054ddd46..0000000000 --- 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 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 24f3a2c97b..0000000000 --- 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 lastModifiedProfiles = profileService.search(query, Profile.class); - DataTable dataTable = new DataTable(); - for (Profile profile : lastModifiedProfiles.getList()) { - ArrayList 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 89705790f1..0000000000 --- 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 de5a4d8c62..0000000000 --- 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 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 allRuleStatistics = rulesService.getAllRuleStatistics(); - - DataTable dataTable = new DataTable(); - for (Metadata ruleMetadata : ruleMetadatas.getList()) { - ArrayList 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 5f8395137a..0000000000 --- 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 segmentMetadatas = segmentService.getSegmentMetadatas(0, maxEntries, null); - - DataTable dataTable = new DataTable(); - for (Metadata metadata : segmentMetadatas.getList()) { - ArrayList 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 14b4efdf1c..0000000000 --- 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 9016aa9ed0..0000000000 --- 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 a3d0fda989..0000000000 --- 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 lastModifiedProfiles = profileService.searchSessions(query); - DataTable dataTable = new DataTable(); - for (Session session : lastModifiedProfiles.getList()) { - ArrayList 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 e672bff149..0000000000 --- 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 0000000000..2ca8433f15 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java @@ -0,0 +1,608 @@ +/* + * 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 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 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 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 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 parsePropertiesWithErrorHandling(String jsonOrUrl, PrintStream console) { + final String errorMsg = "Error: Failed to parse JSON or URL: " + jsonOrUrl; + try { + Map 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 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 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 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 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 [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 [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.getObjectType().equals(type)) { + try { + executeOperation(cmd, operationLower, console); + return true; // Handler found and executed + } finally { + bundleContext.ungetService(ref); + } + } + } + return false; // No handler found + } + + @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 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 "); + 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 + * 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 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 "); + 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 remaining, String operation, PrintStream console) { + if (!hasMinimumRemainingArgs(remaining, 1)) { + console.println("Error: ID is required for " + operation + " operation"); + console.println("Usage: unomi:crud " + operation + " "); + console.println("Example: unomi:crud " + operation + " goal test-goal-123"); + return false; + } + return true; + } + + /** + * Handle read operation. + * Syntax: unomi:crud read + * 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 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 + * 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 "); + 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 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 + * 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 [--csv] [-n ] + * 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 0000000000..a968aaff1b --- /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 0000000000..f40b51a359 --- /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 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 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 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 0000000000..1ef8ee99bc --- /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 0000000000..44dfcbf821 --- /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 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 0000000000..243e04e518 --- /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 72% 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 56db909abf..7bfdf79af7 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.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 the type of the object + * @return the read object + * @throws IOException if there's an error reading the definition + */ + protected T readDefinition(URL definitionURL, Class 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 bundlesToUpdate; if ("*".equals(definitionType)) { @@ -137,7 +206,7 @@ public Object execute() throws Exception { 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; } @@ -149,7 +218,7 @@ public Object execute() throws Exception { 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; } @@ -159,14 +228,14 @@ public Object execute() throws Exception { } 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 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; } @@ -194,7 +263,7 @@ public Object execute() throws Exception { 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 36ecdeceba..c4fd462c95 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.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 String[] getColumnHeaders() { @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 int onEvent(Event event) { if (!event.isPersistent() && !withInternal) { return EventService.NO_CHANGE; } - List 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 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 dad45f10b7..154c3c96d5 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 @@ */ 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 Object execute() throws Exception { 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 0000000000..58009cca66 --- /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% 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/commands/RuleResetStats.java index 7a228be9fa..ad5c11aba2 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 @@ @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 6cf66b2192..e6de275a94 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.services.RuleListenerService; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; /** @@ -56,12 +55,12 @@ public String[] getColumnHeaders() { @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 void onAlreadyRaised(AlreadyRaisedFor alreadyRaisedFor, Rule rule, Event @Override public void onExecuteActions(Rule rule, Event event) { - List 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 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 c94394455c..63ba069e59 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.services.RuleListenerService; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; /** @@ -62,12 +61,12 @@ public String[] getColumnHeaders() { @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 void onEvaluate(Rule rule, Event event) { 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 void populateRuleInfo(Rule rule, Event event, String status) { if (!ArrayUtils.contains(ruleIds, rule.getItemId())) { return; } - List 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 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 f6af0328a4..7de0d44512 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.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 void outputLine(PrintStream out, List eventInfo) { } 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 0000000000..bb6fa50160 --- /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 extractEventInfo(Event event) { + List 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())); + 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 extractRuleExecutionInfo(Rule rule, Event event) { + List 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()); + 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 extractRuleExecutionInfoWithStatus(Rule rule, Event event, String status) { + List 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()); + 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 0000000000..6ed184d2b3 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java @@ -0,0 +1,97 @@ +/* + * 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 = "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.removeActionType(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: + Patch patch = readDefinition(definitionURL, Patch.class); + // patchService.patch(patch); + 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/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 0000000000..319464aef2 --- /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 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 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 properties) { + ActionType actionType = OBJECT_MAPPER.convertValue(properties, ActionType.class); + definitionsService.setActionType(actionType); + return actionType.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 0000000000..caa609f474 --- /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 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 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 properties) { + Campaign campaign = OBJECT_MAPPER.convertValue(properties, Campaign.class); + goalsService.setCampaign(campaign); + return campaign.getItemId(); + } + + @Override + public void update(String id, Map properties) { + Campaign existingCampaign = goalsService.getCampaign(id); + if (existingCampaign == null) { + return; + } + + 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 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 0000000000..8096e9dcb9 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.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.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.ArrayList; +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 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 read(String id) { + // There's no direct method to get a single campaign event, so we need to query for it + Query query = new Query(); + Condition condition = new Condition(); + condition.setConditionType(definitionsService.getConditionType("matchAllCondition")); + condition.setParameter("operator", "and"); + condition.setParameter("subConditions", new ArrayList<>()); + query.setCondition(condition); + + PartialList events = goalsService.getEvents(query); + CampaignEvent event = events.getList().stream() + .filter(e -> e.getItemId().equals(id)) + .findFirst() + .orElse(null); + + if (event == null) { + return null; + } + return OBJECT_MAPPER.convertValue(event, Map.class); + } + + @Override + public String create(Map properties) { + CampaignEvent event = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class); + goalsService.setCampaignEvent(event); + return event.getItemId(); + } + + @Override + public void update(String id, Map 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 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 0000000000..b98560290a --- /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 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 allTypes = new ArrayList<>(definitionsService.getAllConditionTypes()); + int offset = query.getOffset(); + int pageSize = query.getLimit(); + int totalSize = allTypes.size(); + + List pageTypes = allTypes.subList( + Math.min(offset, totalSize), + Math.min(offset + pageSize, totalSize) + ); + + return new PartialList(pageTypes, offset, pageSize, totalSize, PartialList.Relation.EQUAL); + } + + @Override + protected Comparable[] buildRow(Object item) { + ConditionType type = (ConditionType) item; + Metadata metadata = type.getMetadata(); + ArrayList 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 properties) { + ConditionType conditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class); + definitionsService.setConditionType(conditionType); + return conditionType.getItemId(); + } + + @Override + public Map 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 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 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 0000000000..ed59c42882 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java @@ -0,0 +1,244 @@ +/* + * 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.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 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 SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + private static final List 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) { + // Since consents are stored within profiles, we need to get all profiles and extract their consents + PartialList profiles = profileService.search(query, Profile.class); + List> consents = new ArrayList<>(); + + for (Profile profile : profiles.getList()) { + Map profileProperties = profile.getProperties(); + if (profileProperties.containsKey("consents")) { + @SuppressWarnings("unchecked") + Map profileConsents = (Map) profileProperties.get("consents"); + for (Map.Entry entry : profileConsents.entrySet()) { + Map consentMap = entry.getValue().toMap(DATE_FORMAT); + consentMap.put("profileId", profile.getItemId()); + consents.add(consentMap); + } + } + } + + return new PartialList>(consents, profiles.getOffset(), profiles.getPageSize(), profiles.getTotalSize(), PartialList.Relation.EQUAL); + } + + @Override + protected Comparable[] buildRow(Object item) { + @SuppressWarnings("unchecked") + Map consentMap = (Map) 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 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 profileProperties = profile.getProperties(); + if (!profileProperties.containsKey("consents")) { + return null; + } + + @SuppressWarnings("unchecked") + Map consents = (Map) profileProperties.get("consents"); + Consent consent = consents.get(typeIdentifier); + + if (consent == null) { + return null; + } + + Map consentMap = consent.toMap(DATE_FORMAT); + consentMap.put("profileId", profileId); + return consentMap; + } + + @Override + public String create(Map 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); + + Map profileProperties = profile.getProperties(); + @SuppressWarnings("unchecked") + Map consents = profileProperties.containsKey("consents") ? + (Map) 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 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); + + Map profileProperties = profile.getProperties(); + @SuppressWarnings("unchecked") + Map consents = profileProperties.containsKey("consents") ? + (Map) profileProperties.get("consents") : new HashMap<>(); + + consents.put(typeIdentifier, consent); + profileProperties.put("consents", consents); + profile.setProperties(profileProperties); + + profileService.save(profile); + } catch (Exception e) { + // Handle error + } + } + + @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 profileProperties = profile.getProperties(); + if (profileProperties.containsKey("consents")) { + @SuppressWarnings("unchecked") + Map consents = (Map) profileProperties.get("consents"); + consents.remove(typeIdentifier); + profileProperties.put("consents", consents); + profile.setProperties(profileProperties); + profileService.save(profile); + } + } + + @Override + public List 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 0000000000..697867bd3d --- /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 PROPERTY_NAMES = List.of( + "itemId", "eventType", "scope", "source", "target", "properties", "persistent" + ); + private static final List 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 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 properties) { + Event event = OBJECT_MAPPER.convertValue(properties, Event.class); + eventService.send(event); + return event.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 0000000000..38a7e77efa --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java @@ -0,0 +1,141 @@ +/* + * 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 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 to PartialList for consistency + Set metadatas = goalsService.getGoalMetadatas(query); + List goals = metadatas.stream() + .map(metadata -> goalsService.getGoal(metadata.getId())) + .filter(goal -> goal != null) + .collect(Collectors.toList()); + return new PartialList<>(goals, goals.size(), 0, goals.size(), null); + } + + @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 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 properties) { + Goal goal = OBJECT_MAPPER.convertValue(properties, Goal.class); + goalsService.setGoal(goal); + return goal.getItemId(); + } + + @Override + public void update(String id, Map properties) { + Goal existingGoal = goalsService.getGoal(id); + if (existingGoal == null) { + return; + } + + 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 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 0000000000..45e0b88459 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java @@ -0,0 +1,150 @@ +/* + * 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 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; + return new String[] { + persona.getItemId(), + (String) persona.getProperty("firstName"), + (String) persona.getProperty("lastName"), + (String) persona.getProperty("email"), + (String) persona.getProperty("description"), + persona.getSystemProperties().get("lastUpdated").toString() + }; + } + + @Override + public String create(Map properties) { + String personaId = (String) properties.get("itemId"); + if (personaId == null) { + throw new IllegalArgumentException("itemId is required"); + } + + Persona persona = new Persona(personaId); + for (Map.Entry 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 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 properties) { + Persona persona = profileService.loadPersona(id); + if (persona == null) { + throw new IllegalArgumentException("Persona not found: " + id); + } + + for (Map.Entry 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 0000000000..64b27af140 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java @@ -0,0 +1,155 @@ +/* + * 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 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 read(String id) { + // Since there's no direct method to get a single alias, we'll need to search for it + // Search with a reasonable limit to find the alias by ID + PartialList aliases = profileService.findProfileAliases(null, 0, 100, null); + ProfileAlias alias = aliases.getList().stream() + .filter(a -> a.getItemId().equals(id)) + .findFirst() + .orElse(null); + + if (alias == null) { + return null; + } + return OBJECT_MAPPER.convertValue(alias, Map.class); + } + + @Override + public String create(Map 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 properties) { + // Get the existing alias (check if it exists and get its data in one call) + Map 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 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 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 0000000000..3dc1c6f0c4 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java @@ -0,0 +1,183 @@ +/* + * 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 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 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 properties) { + Profile profile = new Profile(); + profile.setProperties(properties); + profileService.save(profile); + return profile.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 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 0000000000..55b31301fb --- /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 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> propertyTypesByTarget = profileService.getTargetPropertyTypes(); + List propertyTypes = new ArrayList<>(); + + // Combine all property types from all targets + for (Collection 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 properties) { + PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class); + profileService.setPropertyType(propertyType); + return propertyType.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 0000000000..64d523926f --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java @@ -0,0 +1,209 @@ +/* + * 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.unomi.api.rules.Rule; +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.apache.unomi.api.conditions.Condition; +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 RuleCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "priority", "condition", "actions", "metadata" + ); + private static final List CONDITION_TYPES = List.of( + "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition", + "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition" + ); + + @Reference + private RulesService rulesService; + + @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 Comparable[] buildRow(Object item) { + Rule rule = (Rule) item; + String ruleId = rule.getItemId(); + Map allRuleStatistics = rulesService.getAllRuleStatistics(); + + ArrayList 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 properties) { + Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class); + rulesService.setRule(rule); + return rule.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 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 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 0000000000..8a0507bdd5 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java @@ -0,0 +1,135 @@ +/* + * 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 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 statisticsMap = rulesService.getAllRuleStatistics(); + List 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 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 properties) { + // Note: RulesService doesn't provide a direct way to create rule statistics + // They are automatically managed by the rules engine + return null; + } + + @Override + public void update(String id, Map properties) { + // Note: RulesService doesn't provide a direct way to update rule statistics + // They are automatically managed by the rules engine + } + + @Override + public void delete(String id) { + // Note: RulesService doesn't provide a direct way to delete individual rule statistics + // They are automatically managed by the rules engine + // You can use resetAllRuleStatistics() to reset all statistics to zero + rulesService.resetAllRuleStatistics(); + } + + @Override + public List 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", + "", + "Note: Use 'unomi:crud rulestats reset' to reset all rule statistics to zero." + ); + } +} 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 0000000000..10bef1fca0 --- /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 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 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 properties) { + Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class); + scopeService.save(scope); + return scope.getItemId(); + } + + @Override + public Map 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 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 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 0000000000..78fd511dd3 --- /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 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 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 properties) { + Scoring scoring = OBJECT_MAPPER.convertValue(properties, Scoring.class); + segmentService.setScoringDefinition(scoring); + return scoring.getItemId(); + } + + @Override + public Map 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 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 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 0000000000..85e16aecc7 --- /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 PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "condition", "metadata" + ); + private static final List 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 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 properties) { + Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class); + segmentService.setSegmentDefinition(segment); + return segment.getItemId(); + } + + @Override + public Map 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 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 completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List 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 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("booleanCondition")); + condition.setParameter("operator", "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 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 0000000000..a185163d1a --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java @@ -0,0 +1,129 @@ +/* + * 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; +import java.util.stream.Collectors; + +/** + * 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 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 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 properties) { + Session session = OBJECT_MAPPER.convertValue(properties, Session.class); + profileService.saveSession(session); + return session.getItemId(); + } + + @Override + public void update(String id, Map 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 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 0000000000..f97bed28d9 --- /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 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 properties) { + Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class); + topicService.save(topic); + return topic.getItemId(); + } + + @Override + public Map 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 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 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 0000000000..5581f519bd --- /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 implements Completer { + protected static final int DEFAULT_LIMIT = 50; + + @Reference + protected PersistenceService persistenceService; + + @Reference + protected DefinitionsService definitionsService; + + protected abstract Class getItemType(); + + protected abstract String getSortBy(); + + @Override + public int complete(Session session, CommandLine commandLine, List 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 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 0000000000..71f6df4123 --- /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 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 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/dev/completers/OperationCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java new file mode 100644 index 0000000000..7ae6e3c35c --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java @@ -0,0 +1,37 @@ +/* + * 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.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 java.util.List; + +@Service +public class OperationCompleter implements Completer { + private static final List OPERATIONS = List.of("create", "read", "update", "delete", "list", "help"); + + @Override + public int complete(Session session, CommandLine commandLine, List 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 0000000000..4a962c9c9b --- /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 candidates) { + Set 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 0000000000..f9b0b204d7 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java @@ -0,0 +1,400 @@ +/* + * 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 for CSV printer + List row = new ArrayList<>(); + for (Comparable cell : rowWithTenant) { + row.add(cell != null ? cell.toString() : ""); + } + printer.printRecord(row.toArray()); + } + + printer.close(); + } + + /** + * 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 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 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 the type of items in the list + * @return a PartialList with paginated results + */ + protected PartialList paginateList(List 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 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 filterPropertyNames(List 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 + } + + // If all else fails, use toString and hope it's meaningful + return item.toString(); + } +} 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 0000000000..4c088e906b --- /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 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 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 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 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 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 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; +} From 124af7eaa96213ba3b0c7af4de06c46c72e39d0c Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:13:26 +0200 Subject: [PATCH 2/4] Add manual dispatch to CodeQL Java workflow --- .github/workflows/codeql-analysis-java.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-java.yml b/.github/workflows/codeql-analysis-java.yml index 0b8b7c4278..c823fc4720 100644 --- a/.github/workflows/codeql-analysis-java.yml +++ b/.github/workflows/codeql-analysis-java.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From c5b167c295acd2b04c25dd12f7ca3f107f8970d5 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:14:21 +0200 Subject: [PATCH 3/4] Add manual dispatch to CodeQL JavaScript workflow --- .github/workflows/codeql-analysis-javascript.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-javascript.yml b/.github/workflows/codeql-analysis-javascript.yml index 542b973b65..39efa7cbe7 100644 --- a/.github/workflows/codeql-analysis-javascript.yml +++ b/.github/workflows/codeql-analysis-javascript.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 4bd200a7cfdd381b244aa7b3ccb1f7dacc3a476e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 16 May 2026 19:06:46 +0200 Subject: [PATCH 4/4] UNOMI-879: Address PR review feedback for unified CRUD shell Fix condition undeploy, rule list performance, null-safety and pagination in CRUD commands, thread-safe consent date formatting, read-only rulestats behavior, segment ID completion, and BOM httpclient dependency management. --- bom/pom.xml | 6 --- tools/shell-dev-commands/pom.xml | 1 + .../dev/commands/UndeployDefinition.java | 2 +- .../commands/consents/ConsentCrudCommand.java | 16 +++++--- .../dev/commands/goals/GoalCrudCommand.java | 2 +- .../commands/personas/PersonaCrudCommand.java | 5 ++- .../dev/commands/rules/RuleCrudCommand.java | 40 ++++++++++++++++++- .../rules/RuleStatisticsCrudCommand.java | 18 ++++----- .../commands/segments/SegmentCrudCommand.java | 4 +- 9 files changed, 66 insertions(+), 28 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index c63868276b..17b0f37529 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -270,12 +270,6 @@ httpclient-osgi ${httpclient-osgi.version} - - org.apache.httpcomponents - httpclient-osgi - ${httpclient-osgi.version} - bundle - org.apache.kafka kafka-clients diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml index e62a637589..807677a638 100644 --- a/tools/shell-dev-commands/pom.xml +++ b/tools/shell-dev-commands/pom.xml @@ -92,6 +92,7 @@ org.apache.httpcomponents httpclient-osgi + ${httpclient-osgi.version} bundle provided 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 index 6ed184d2b3..70e8ccd7d8 100644 --- 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 @@ -50,7 +50,7 @@ protected boolean processDefinitionByType(String definitionType, URL definitionU switch (definitionType) { case CONDITION_DEFINITION_TYPE: ConditionType conditionType = readDefinition(definitionURL, ConditionType.class); - definitionsService.removeActionType(conditionType.getItemId()); + definitionsService.removeConditionType(conditionType.getItemId()); return true; case ACTION_DEFINITION_TYPE: ActionType actionType = readDefinition(definitionURL, ActionType.class); 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 index ed59c42882..0590c0c2f3 100644 --- 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 @@ -28,6 +28,7 @@ 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; @@ -42,7 +43,9 @@ public class ConsentCrudCommand extends BaseCrudCommand { private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + private static final String CONSENT_DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + private static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial( + () -> new SimpleDateFormat(CONSENT_DATE_FORMAT_PATTERN)); private static final List PROPERTY_NAMES = List.of( "profileId", "scope", "typeIdentifier", "status", "statusDate", "revokeDate" ); @@ -66,20 +69,21 @@ protected PartialList getItems(Query query) { PartialList profiles = profileService.search(query, Profile.class); List> consents = new ArrayList<>(); + DateFormat dateFormat = DATE_FORMAT.get(); for (Profile profile : profiles.getList()) { Map profileProperties = profile.getProperties(); if (profileProperties.containsKey("consents")) { @SuppressWarnings("unchecked") Map profileConsents = (Map) profileProperties.get("consents"); for (Map.Entry entry : profileConsents.entrySet()) { - Map consentMap = entry.getValue().toMap(DATE_FORMAT); + Map consentMap = entry.getValue().toMap(dateFormat); consentMap.put("profileId", profile.getItemId()); consents.add(consentMap); } } } - return new PartialList>(consents, profiles.getOffset(), profiles.getPageSize(), profiles.getTotalSize(), PartialList.Relation.EQUAL); + return paginateList(consents, query); } @Override @@ -125,7 +129,7 @@ public Map read(String id) { return null; } - Map consentMap = consent.toMap(DATE_FORMAT); + Map consentMap = consent.toMap(DATE_FORMAT.get()); consentMap.put("profileId", profileId); return consentMap; } @@ -143,7 +147,7 @@ public String create(Map properties) { } try { - Consent consent = new Consent(properties, DATE_FORMAT); + Consent consent = new Consent(properties, DATE_FORMAT.get()); Map profileProperties = profile.getProperties(); @SuppressWarnings("unchecked") @@ -178,7 +182,7 @@ public void update(String id, Map properties) { } try { - Consent consent = new Consent(properties, DATE_FORMAT); + Consent consent = new Consent(properties, DATE_FORMAT.get()); Map profileProperties = profile.getProperties(); @SuppressWarnings("unchecked") 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 index 38a7e77efa..cf0fa36829 100644 --- 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 @@ -65,7 +65,7 @@ protected PartialList getItems(Query query) { .map(metadata -> goalsService.getGoal(metadata.getId())) .filter(goal -> goal != null) .collect(Collectors.toList()); - return new PartialList<>(goals, goals.size(), 0, goals.size(), null); + return new PartialList<>(goals, query.getOffset(), goals.size(), goals.size(), PartialList.Relation.EQUAL); } @Override 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 index 45e0b88459..5633e89c49 100644 --- 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 @@ -72,13 +72,16 @@ protected PartialList getItems(Query query) { @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"), - persona.getSystemProperties().get("lastUpdated").toString() + lastUpdated != null ? lastUpdated.toString() : "" }; } 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 index 64d523926f..f99e5ad940 100644 --- 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 @@ -21,9 +21,11 @@ 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; @@ -31,6 +33,7 @@ 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; @@ -51,6 +54,9 @@ public class RuleCrudCommand extends BaseCrudCommand { @Reference private RulesService rulesService; + /** Cached for the duration of a single list/query operation (see {@link #buildQuery}). */ + private Map statisticsSnapshot; + @Override public String getObjectType() { return "rule"; @@ -78,11 +84,43 @@ 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 allRuleStatistics = rulesService.getAllRuleStatistics(); + Map allRuleStatistics = statisticsSnapshot != null + ? statisticsSnapshot + : rulesService.getAllRuleStatistics(); ArrayList rowData = new ArrayList<>(); rowData.add(rule.getMetadata().isEnabled() ? "x" : ""); 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 index 8a0507bdd5..84baa0e08d 100644 --- 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 @@ -90,23 +90,21 @@ public Map read(String id) { @Override public String create(Map properties) { - // Note: RulesService doesn't provide a direct way to create rule statistics - // They are automatically managed by the rules engine - return null; + 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 properties) { - // Note: RulesService doesn't provide a direct way to update rule statistics - // They are automatically managed by the rules engine + 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) { - // Note: RulesService doesn't provide a direct way to delete individual rule statistics - // They are automatically managed by the rules engine - // You can use resetAllRuleStatistics() to reset all statistics to zero - rulesService.resetAllRuleStatistics(); + throw new UnsupportedOperationException( + "Rule statistics cannot be deleted individually via the shell (requested id: " + id + + "). Statistics are maintained by the rules engine."); } @Override @@ -129,7 +127,7 @@ public String getPropertiesHelp() { "- localActionsTime: Time spent executing actions on this node since last sync (ms)", "- lastSyncDate: Date of the last synchronization with the cluster", "", - "Note: Use 'unomi:crud rulestats reset' to reset all rule statistics to zero." + "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/segments/SegmentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java index 85e16aecc7..7120184575 100644 --- 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 @@ -172,8 +172,8 @@ public List completeId(String prefix) { try { // If prefix is not empty, use it to filter results if (!prefix.isEmpty()) { - Condition condition = new Condition(definitionsService.getConditionType("booleanCondition")); - condition.setParameter("operator", "startsWith"); + Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition")); + condition.setParameter("comparisonOperator", "startsWith"); condition.setParameter("propertyName", "itemId"); condition.setParameter("propertyValue", prefix); query.setCondition(condition);