diff --git a/.gitignore b/.gitignore index f1706f9..475e3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ gradle-app.setting # gradle/wrapper/gradle-wrapper.properties *.iml + +# Spring AI weather demo build artifacts +**/model-context-protocol/**/target/ + +# Claude Code workspace files +.claude/ diff --git a/README.md b/README.md index f964561..848b53b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,28 @@ iCal4j Command is a collection of operations for common iCalendar and vCard use cases. This includes direct manipulation of data objects, integration with persistence stores, and systems interoperability. -## Calendar Commands +### Example Use Cases + +| Use Case | Description | +|--------------|-------------------------------------------------| +| Workspaces | Create workspaces for distinct knowledge graphs | +| Collections | Group related activities, notes and contacts | +| Events | Schedule and manage events and tasks | +| Actions | Track actions and to-dos | +| Issues | Manage issues and bug reports | +| Notes | Maintain notes and annotations | +| Observances | Reminders for important dates | +| Reports | Report on past activities and events | +| Availability | Share free/busy information | +| Entities | Maintain contact and organization information | + +## Command Line Interface (CLI) + +The CLI provides access to iCal4j Command operations via a terminal +interface. It supports a variety of commands for manipulating iCalendar and +vCard data, as well as managing workspaces and collections. + +### Calendar Commands The following commands directly manipulate iCalendar objects: @@ -16,30 +37,96 @@ The following commands directly manipulate iCalendar objects: * Filter - select a subset of iCalendar components using filter expressions * Free Busy - generate free/busy information for specified iCalendar data * Replace UIDs - regenerate UIDs for all iCalendar components +* Serialize - output iCalendar data in various formats (e.g. XML, JSON) +* Deserialize - read iCalendar data from various formats (e.g. XML, JSON) -## vCard Commands +### vCard Commands * ... -## Template Commands +### Strategy Commands -* Event - generate/modify iCalendar data using shorthand template input +Strategy commands use the `generate` verb (command group `generate`, alias `new`): -## Store Commands +* Generate Event - generate/overlay iCalendar data using strategy patterns -* Create Collection - create a new collection for a calendar store -* ... +### Store Commands + +* Add Workspace - add a new workspace for storing iCalendar/vCard collections +* List Workspaces - list available workspaces +* Remove Workspace - remove an existing workspace +* Get Workspace - retrieve details of a specific workspace -## Collection Commands +### Collection Commands * ... -## Channel Commands +### Channel Commands * ... +## Model Context Protocol (MCP) + +iCal4j Command supports the Model Context Protocol (MCP) for managing +iCalendar and vCard data in a structured manner. MCP defines a set of +operations for creating, retrieving, updating, and deleting data objects within +a defined context. + +### MCP Operations + +MCP operations follow the canonical vocabulary (see `docs/command-protocols.md`): a **verb** +applied to an **entity type** within a **locus**. The supported verbs are: + +* `add` / `remove` — add or remove an item within a locus (e.g. add a workspace, remove a + collection) +* `list` — enumerate items of a type within a locus (e.g. list events in a collection) +* `get` — retrieve a single item or its metadata +* `update` — modify an existing item +* `import` / `export` — move calendar/card serializations into or out of a collection + +These apply across the loci `workspace` and `collection`, and the entity types `entity` +(contact), `event`, `action`, `issue`, `note`, `observance`, `report`, `availability` and +`resource`. (`create`/`delete` are standardised on `add`/`remove`.) + +## Agentic User Agent + +iCal4j Command includes an Agentic User Agent that provides intelligent +automation capabilities for managing iCalendar and vCard data. The User Agent +can perform tasks such as scheduling events, sending notifications, and +managing contacts based on predefined rules and user preferences. + +### Prompts + +Some example prompts for the User Agent: + +* "Create a new event for a team meeting next Monday at 10 AM." +* "Add a contact for John Doe with email" +* "Generate a free/busy report for the next two weeks." +* "List all upcoming events in my calendar." +* "Update the location of the event 'Project Kickoff' to 'Conference Room B'." +* "Delete the contact for Jane Smith." +* "Send a notification to all attendees of the event 'Weekly Sync'." +* "Create a new workspace for project management." +* "List all workspaces available in my account." +* "Add a new collection for storing project-related notes." + +### Interactions + +The User Agent can interact with users through various channels, including +email, messaging apps, and voice assistants. It can also integrate with other +systems and services to enhance its capabilities. + +When prompted the agent may ask clarifying questions to ensure it understands the user's intent before taking action. + +For example: + +**User:** "Create birthday reminders for my family members." +**Agent:** "Could you please provide the names and birth dates of your family members?" + + + ## System Properties The following table describes System properties applicable to the iCal4j User Agent. @@ -51,29 +138,3 @@ The following table describes System properties applicable to the iCal4j User Ag | org.ical4j.command.organizer | The default URI used as the value for the iCalendar `ORGANIZER` property | A URI (e.g. `mailto:johnd@example.com`) | - | | org.ical4j.command.uidgenerator | The `UidGenerator` implementation used to create `UID` properties | A fully qualified class name | `net.fortuna.ical4j.util.RandomUidGenerator` | - -## Command Line Usage - -### Publish a collection of events - - ical4j publish -F ./Australian32Holidays.ics - -Result: - -``` -BEGIN:VCALENDAR -PRODID:iCal4j User Agent -VERSION:2.0 -METHOD:PUBLISH -BEGIN:VEVENT -UID:D416469E-C414-11D6-BA97-003065F198AC -DTSTAMP:20020906T094459Z -SUMMARY:Australia Day -RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=1 -DTSTART;VALUE=DATE:20020126 -DTEND;VALUE=DATE:20020127 -ORGANIZER:johnd@example.com -SEQUENCE:0 -END:VEVENT -... -``` diff --git a/build.gradle b/build.gradle index 5f96eff..52375e1 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ configure(subprojects) { // spock testImplementation platform(libs.spock.bom), libs.spock.core + testRuntimeOnly libs.junit.platform } group = 'org.ical4j' diff --git a/docs/command-protocols.md b/docs/command-protocols.md new file mode 100644 index 0000000..06e4661 --- /dev/null +++ b/docs/command-protocols.md @@ -0,0 +1,118 @@ +# iCal4j Command Protocols + +This document describes the various command protocols and tools provided by the iCal4j Command library for managing iCalendar and vCard data. + +## Canonical Vocabulary + +All command surfaces (CLI, MCP, agent) name operations from a single controlled vocabulary +defined in the `ical4j-command-core` module. Each operation is identified by a **verb**, an +**entity type**, and a **locus** (container). Surfaces share these names but dispatch +independently. + +- **Verbs** (`org.ical4j.command.core.Verb`): `list` (many), `get` (one), `generate` + (synthesise from a strategy/template), `add` (place into a locus), `update`, `remove` + (take out of a locus), `copy`, `move`, `filter`, `validate`, `import`, `export`, `send`, + `receive`. There is no `create`/`delete` — these are standardised on `add`/`remove`. +- **Entity types** (`EntityType`): `event`, `action`, `issue`, `note`, `observance`, + `availability`, `report`, `entity` (contact), `resource`. All live directly within a + collection. +- **Loci** (`Locus`): `workspace` ⊃ `collection` (two levels; entities live directly in a + collection). +- **Serialization families** (`SerializationFamily`): `calendar` (iCalendar), `card` + (vCard). These are NOT loci or entity types; data crosses a collection boundary via + `import`/`export`. +- **iTIP methods** (`SendMethod`): `publish`, `request`, `reply` are specialised forms of + `send`, not independent verbs. + +## Calendar Commands + +### Validator +- Purpose: Validates iCalendar data for conformance to specifications +- Location: `org.ical4j.command.calendar.Validator` +- Usage: Validates iCalendar objects for RFC compliance and structural integrity + +### Filter +- Purpose: Filters iCalendar components based on expressions +- Location: `org.ical4j.command.calendar.FilterCalendar` +- Usage: Selects subsets of iCalendar components using filter criteria + +### Free/Busy +- Purpose: Generates availability information +- Location: `org.ical4j.command.calendar.FreeBusy` +- Usage: Creates free/busy time reports from calendar data + +### Replace UIDs +- Purpose: Regenerates unique identifiers +- Location: `org.ical4j.command.calendar.ReplaceUids` +- Usage: Updates UIDs across all calendar components + +## Collection Management + +### Calendar Collections +- Import Calendar (`import-calendar`): Imports calendar data into a collection +- Export Calendar (`export-calendar`): Exports calendar data from a collection +- Remove Calendar (`remove-calendar`): Removes calendar data from a collection +- List: Enumerates calendar entities in a collection + +### Card Collections +- Import Card (`import-card`): Imports vCard data into a collection +- Export Card (`export-card`): Exports vCard data from a collection +- Remove Card (`remove-card`): Removes vCard data from a collection +- List: Enumerates vCard entities in a collection + +## Channel Operations + +### Calendar Channels +- Send Calendar: Transmits calendar data to specified endpoints. The iTIP methods + `publish`, `request` and `reply` are specialised forms of `send`. +- Receive Calendar: Accepts incoming calendar data + +### vCard Channels +- Send vCard: Transmits vCard data to specified endpoints +- Receive vCard: Accepts incoming vCard data + +## Store Operations + +### Collection Management +- Add Collection: Adds a collection to a workspace +- Remove Collection: Removes a collection from a workspace +- Get Collection: Retrieves collection metadata +- List Collections: Enumerates available collections +- Update Collection: Modifies collection properties + +## Strategy (Generation) Operations + +### Event Templates +- Purpose: Generates calendar data using simplified templates (the `generate` verb) +- Location: `org.ical4j.command.strategy.CreateEvent` (command name `generate event`) +- Usage: Synthesises calendar entries from template definitions + +## Configuration + +### System Properties +The following properties can be configured: + +- `org.ical4j.command.prodid`: Customizes the PRODID property +- `org.ical4j.command.organizer`: Sets default organizer URI +- `org.ical4j.command.uidgenerator`: Specifies UID generation implementation + +## Common Patterns + +### Input/Output Handling +- Input handling through `InputHandler` interface +- Output processing via `DefaultOutputHandlers` +- Support for various data formats and sources + +### Command Groups +- Calendar Command Group: Calendar-specific operations +- Card Command Group: vCard-specific operations +- Channel Command Group: Communication operations +- Collection Command Group: Data collection management +- Store Command Group: Storage operations +- Template Command Group: Template-based generation + +Each command follows a consistent protocol pattern: +1. Input validation +2. Operation execution +3. Result handling +4. Error management diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c18caa4..4ca2999 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -ical4j = "4.2.1" -ical4j-vcard = "2.0.1" -ical4j-extensions = "2.0.1-develop-SNAPSHOT" -ical4j-connector = "2.0.0-beta2-SNAPSHOT" +ical4j = "4.2.3" +ical4j-vcard = "2.1.0" +ical4j-extensions = "2.0.0" +ical4j-connector = "2.0.0-beta1" ical4j-integration = "0.3.1" -ical4j-serializer = "0.4.0-develop-SNAPSHOT" +ical4j-serializer = "0.4.1" groovy = "3.0.25" log4j = "2.25.2" spock = "2.4-M7-groovy-3.0" @@ -42,3 +42,4 @@ spock-bom = { module = "org.spockframework:spock-bom", version.ref = "spock"} spock-core = { module = "org.spockframework:spock-core", version.ref = "spock"} junit-jupiter = { group = "org.junit.jupiter", name="junit-jupiter", version.ref = "junitVintage"} +junit-platform = { module = "org.junit.platform:junit-platform-launcher" } diff --git a/ical4j-command-agent/build.gradle b/ical4j-command-agent/build.gradle new file mode 100644 index 0000000..c538335 --- /dev/null +++ b/ical4j-command-agent/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':ical4j-command-core') + + implementation libs.langchain4j, libs.langchain4j.openai +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/config/StoreConfiguration.java b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarUserAgent.java similarity index 66% rename from ical4j-command-cli/src/main/java/org/ical4j/command/config/StoreConfiguration.java rename to ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarUserAgent.java index b59997a..1b042b6 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/config/StoreConfiguration.java +++ b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarUserAgent.java @@ -1,16 +1,6 @@ -package org.ical4j.command.config; +package org.ical4j.command.agent; import net.fortuna.ical4j.model.Calendar; -import net.fortuna.ical4j.vcard.VCard; -import org.ical4j.command.util.Filesystem; -import org.ical4j.connector.CalendarCollection; -import org.ical4j.connector.CardCollection; -import org.ical4j.connector.ObjectCollection; -import org.ical4j.connector.ObjectStore; -import org.ical4j.connector.local.LocalCalendarStore; -import org.ical4j.connector.local.LocalCardStore; - -import java.io.File; /* * Copyright (c) 2025, Ben Fortuna @@ -43,13 +33,11 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -public class StoreConfiguration { +public interface CalendarUserAgent { + + Calendar publish(String prompt) throws Exception; - public ObjectStore getCalendarStore() { - return new LocalCalendarStore(new File(Filesystem.getDataDirectory(), "iCal4j/calendars")); - } + Calendar request(String prompt) throws Exception; - public ObjectStore getCardStore() { - return new LocalCardStore(new File(Filesystem.getDataDirectory(), "iCal4j/vcards")); - } + Calendar reply(String prompt) throws Exception; } diff --git a/ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarValidator.java b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarValidator.java new file mode 100644 index 0000000..c46d80e --- /dev/null +++ b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/CalendarValidator.java @@ -0,0 +1,53 @@ +package org.ical4j.command.agent; + +import dev.langchain4j.agent.tool.Tool; +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.validate.ValidationResult; + +import java.io.IOException; +import java.io.StringReader; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +public class CalendarValidator { + + /** + * Validate a calendar string. + * @param calendar a calendar string + * @return a validation result, or null if the calendar is valid + */ + @Tool("Validate a calendar string") + public ValidationResult validate(String calendar) throws ParserException, IOException { + return new CalendarBuilder().build(new StringReader(calendar)).validate(); + } +} diff --git a/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EntityBuilder.java b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EntityBuilder.java new file mode 100644 index 0000000..69e674e --- /dev/null +++ b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EntityBuilder.java @@ -0,0 +1,51 @@ +package org.ical4j.command.agent; + +import dev.langchain4j.agent.tool.Tool; +import net.fortuna.ical4j.vcard.Entity; +import net.fortuna.ical4j.vcard.property.Email; +import net.fortuna.ical4j.vcard.property.Fn; +import net.fortuna.ical4j.vcard.property.immutable.ImmutableKind; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +public class EntityBuilder { + + @Tool + Entity generateIndividual(String name) { + return new Entity().add(ImmutableKind.INDIVIDUAL).add(new Fn(name)); + } + + @Tool + Entity addEmail(Entity entity, String email) { + return entity.add(new Email(email)); + } +} diff --git a/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EventGenerator.java b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EventGenerator.java new file mode 100644 index 0000000..c8881ee --- /dev/null +++ b/ical4j-command-agent/src/main/java/org/ical4j/command/agent/EventGenerator.java @@ -0,0 +1,58 @@ +package org.ical4j.command.agent; + +import dev.langchain4j.agent.tool.Tool; +import net.fortuna.ical4j.extensions.strategy.event.Appointment; +import net.fortuna.ical4j.extensions.strategy.event.Meeting; +import net.fortuna.ical4j.model.component.VEvent; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +public class EventGenerator { + + @Tool + VEvent generateEvent(String prompt) { + // Implementation to generate an event based on the prompt + return null; // Placeholder return + } + + @Tool + VEvent generateMeeting(String prompt) { + // Implementation to generate a meeting based on the prompt + return new Meeting().get(); // Placeholder return + } + + @Tool + VEvent generateAppointment(String prompt) { + // Implementation to generate an appointment based on the prompt + return new Appointment().get(); // Placeholder return + } +} diff --git a/ical4j-command-agent/src/test/groovy/org/ical4j/command/agent/CalendarValidatorTest.groovy b/ical4j-command-agent/src/test/groovy/org/ical4j/command/agent/CalendarValidatorTest.groovy new file mode 100644 index 0000000..0cb39b4 --- /dev/null +++ b/ical4j-command-agent/src/test/groovy/org/ical4j/command/agent/CalendarValidatorTest.groovy @@ -0,0 +1,38 @@ +package org.ical4j.command.agent + +import spock.lang.Specification + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +class CalendarValidatorTest extends Specification { +} diff --git a/ical4j-command-cli/build.gradle b/ical4j-command-cli/build.gradle index 4303f02..f69c3c7 100644 --- a/ical4j-command-cli/build.gradle +++ b/ical4j-command-cli/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'application' dependencies { + api project(':ical4j-command-core') + implementation libs.picocli, libs.night.config.toml implementation libs.log4j.core, libs.log4j.slf4j2 diff --git a/ical4j-command-cli/src/main/java/module-info.java b/ical4j-command-cli/src/main/java/module-info.java index 068a35b..43734aa 100644 --- a/ical4j-command-cli/src/main/java/module-info.java +++ b/ical4j-command-cli/src/main/java/module-info.java @@ -1,5 +1,6 @@ module ical4j.command { requires java.base; + requires ical4j.command.core; requires ical4j.core; requires ical4j.vcard; requires ical4j.connector.api; @@ -12,20 +13,24 @@ requires org.slf4j; requires org.apache.logging.log4j.core; requires ical4j.extensions; + requires java.desktop; + requires com.electronwill.nightconfig.core; exports org.ical4j.command; exports org.ical4j.command.calendar; exports org.ical4j.command.channel; exports org.ical4j.command.collection; - exports org.ical4j.command.strategy; - exports org.ical4j.command.vcard; + exports org.ical4j.command.card; opens org.ical4j.command to info.picocli; opens org.ical4j.command.calendar to info.picocli; opens org.ical4j.command.channel to info.picocli; opens org.ical4j.command.collection to info.picocli; - opens org.ical4j.command.strategy to info.picocli; - opens org.ical4j.command.vcard to info.picocli; + opens org.ical4j.command.card to info.picocli; exports org.ical4j.command.workspace; opens org.ical4j.command.workspace to info.picocli; + exports org.ical4j.command.config; + opens org.ical4j.command.config to info.picocli; + exports org.ical4j.command.strategy; + opens org.ical4j.command.strategy to info.picocli; } \ No newline at end of file diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/CommandMain.java b/ical4j-command-cli/src/main/java/org/ical4j/command/CommandMain.java index 66baf70..850f88a 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/CommandMain.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/CommandMain.java @@ -1,16 +1,44 @@ package org.ical4j.command; -import org.ical4j.command.collection.CollectionCommandGroup; +import org.ical4j.command.collection.AddCollection; +import org.ical4j.command.collection.RemoveCollection; +import org.ical4j.command.config.ConfigureCommand; +import org.ical4j.command.metadata.AddCommandGroup; +import org.ical4j.command.metadata.GetProperty; +import org.ical4j.command.metadata.UnsetProperty; +import org.ical4j.command.metadata.SetCommandGroup; +import org.ical4j.command.revisions.ListRevisions; +import org.ical4j.command.revisions.RevertRevision; +import org.ical4j.command.revisions.UndoRevisions; import org.ical4j.command.strategy.StrategyCommandGroup; -import org.ical4j.command.workspace.WorkspaceCommandGroup; +import org.ical4j.command.strategy.StrategyInfo; +import org.ical4j.command.workspace.ActivateWorkspace; +import org.ical4j.command.workspace.ListWorkspaces; +import org.ical4j.command.workspace.PrintWorkspace; import picocli.CommandLine; -@CommandLine.Command(name = "ical4j", description = "iCal4j Command-line Tool", - subcommands = {ValidatorCommand.class, FilterCommand.class, ConverterCommand.class, - StrategyCommandGroup.class, CollectionCommandGroup.class, WorkspaceCommandGroup.class, - ConfigureCommand.class}, +@CommandLine.Command(name = "ict", description = "@|bold,fg(104) iCal4j Command-line Tool|@", + subcommands = { + // Collection commands + AddCollection.class, RemoveCollection.class, + // Common commands + ListCommand.class, CopyCommand.class, MoveCommand.class, DeleteCommand.class, EditCommand.class, PrintCommand.class, + // Strategy commands + StrategyCommandGroup.class, StrategyInfo.class, + // Metadata commands + AddCommandGroup.class, GetProperty.class, SetCommandGroup.class, UnsetProperty.class, + // Import/export commands + ImportCalendars.class, ExportCommand.class, + // Workspace commands + ListWorkspaces.class, ActivateWorkspace.class, PrintWorkspace.class, + // Revision commands + ListRevisions.class, RevertRevision.class, UndoRevisions.class, + // Utility commands + ValidatorCommand.class, FilterCommand.class, ConverterCommand.class, + ConfigureCommand.class}, scope = CommandLine.ScopeType.INHERIT, mixinStandardHelpOptions = true, versionProvider = VersionProvider.class, - footer = "Copyright (c) Ben Fortuna", showAtFileInUsageHelp = true) + commandListHeading = "%n@|bold,fg(104) Common commands:|@%n", + footer = "@|yellow Copyright (c) Ben Fortuna |@", showAtFileInUsageHelp = true) public class CommandMain extends GlobalOptions { public static void main(String[] args) { diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ConfigureCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ConfigureCommand.java deleted file mode 100644 index ae401f3..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/ConfigureCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.ical4j.command; - -import picocli.CommandLine; - -@CommandLine.Command(name = "configure", description = "Configure settings") -public class ConfigureCommand extends AbstractCommand { - - @Override - public Integer call() throws Exception { - return 0; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ConverterCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ConverterCommand.java index 5d2fddd..30118cf 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/ConverterCommand.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ConverterCommand.java @@ -6,10 +6,15 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.vcard.VCard; import org.mnode.ical4j.serializer.JCalSerializer; import org.mnode.ical4j.serializer.XCalSerializer; +import org.mnode.ical4j.serializer.jmap.JSCardSerializer; +import org.mnode.ical4j.serializer.jotn.CalendarSerializer; import picocli.CommandLine; +import java.util.EnumSet; + /* * Copyright (c) 2025, Ben Fortuna * All rights reserved. @@ -48,7 +53,8 @@ public enum Format { JCAL, XCAL, JCARD, XCARD, JSCALENDAR, JSCARD, - JSONLD, RDF + JSONLD, RDF, + JOTN } /** @@ -78,6 +84,18 @@ public Integer call() throws Exception { .with(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME)); objectMapper.registerModule(module); } + case JSCARD -> { + SimpleModule module = new SimpleModule(); + module.addSerializer(VCard.class, new JSCardSerializer(VCard.class)); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + } + case JOTN -> { + SimpleModule module = new SimpleModule(); + module.addSerializer(Calendar.class, new CalendarSerializer()); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + } default -> { SimpleModule module = new SimpleModule(); module.addSerializer(Calendar.class, new JCalSerializer(null)); @@ -85,11 +103,20 @@ public Integer call() throws Exception { objectMapper.registerModule(module); } } - if (prettyPrint) { - getOutputHandler().accept(objectMapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(input.toCalendar())); + if (EnumSet.of(Format.JCARD, Format.JSCARD).contains(format)) { + if (prettyPrint) { + getOutputHandler().accept(objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(input.toVCard())); + } else { + getOutputHandler().accept(objectMapper.writeValueAsString(input.toVCard())); + } } else { - getOutputHandler().accept(objectMapper.writeValueAsString(input.toCalendar())); + if (prettyPrint) { + getOutputHandler().accept(objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(input.toCalendar())); + } else { + getOutputHandler().accept(objectMapper.writeValueAsString(input.toCalendar())); + } } return 0; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/CopyCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/CopyCommand.java new file mode 100644 index 0000000..385aaca --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/CopyCommand.java @@ -0,0 +1,43 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "copy", aliases = {"cp"}, description = "Copy calendar or contact data between collections") +public class CopyCommand extends AbstractCommand { + + @Override + public Integer call() throws Exception { + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/DefaultOutputHandlers.java b/ical4j-command-cli/src/main/java/org/ical4j/command/DefaultOutputHandlers.java index 5b68d29..51770bb 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/DefaultOutputHandlers.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/DefaultOutputHandlers.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.Writer; +import java.util.Collection; import java.util.List; import java.util.function.Consumer; @@ -84,8 +85,10 @@ static Consumer STDOUT_REPORT_PRINTER(Writer out) { * @return a consumer instance * @param command result type */ - static > Consumer STDOUT_LIST_PRINTER() { - return (t) -> t.forEach(System.out::println); + static > Consumer STDOUT_LIST_PRINTER() { + List ignoredFiles = List.of(".DS_Store"); + return (T objects) -> objects.stream().filter(o -> !ignoredFiles.contains(o)) + .forEach(System.out::println); } /** diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateEvent.java b/ical4j-command-cli/src/main/java/org/ical4j/command/DeleteCommand.java similarity index 80% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateEvent.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/DeleteCommand.java index 2919948..006be1c 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateEvent.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/DeleteCommand.java @@ -1,4 +1,8 @@ -package org.ical4j.command.collection; +package org.ical4j.command; + +import picocli.CommandLine; + +import java.util.List; /* * Copyright (c) 2025, Ben Fortuna @@ -31,19 +35,11 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -import net.fortuna.ical4j.extensions.strategy.event.Appointment; -import net.fortuna.ical4j.model.Calendar; -import org.ical4j.connector.CalendarCollection; - -/** - * Create event command. - */ -public class CreateEvent extends AbstractCollectionCommand { +@CommandLine.Command(name = "remove", aliases = {"rm"}, description = "Remove calendar or contact data from collections") +public class DeleteCommand extends AbstractCommand> { @Override public Integer call() throws Exception { - getCollection().add(new Calendar().add(new Appointment().get())); return 0; } } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/EditCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/EditCommand.java new file mode 100644 index 0000000..7ba801c --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/EditCommand.java @@ -0,0 +1,38 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "edit", description = "Edit properties of calendar or vCard components") +public class EditCommand { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ExportCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ExportCommand.java new file mode 100644 index 0000000..9875f4e --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ExportCommand.java @@ -0,0 +1,38 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "export", description = "Export calendar or contact data to a file") +public class ExportCommand { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ImportCalendars.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ImportCalendars.java new file mode 100644 index 0000000..74274c9 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ImportCalendars.java @@ -0,0 +1,36 @@ +package org.ical4j.command; + +import net.fortuna.ical4j.data.ParserException; +import org.ical4j.command.config.CommandConfig; +import org.ical4j.connector.CalendarCollection; +import org.ical4j.connector.FailedOperationException; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; +import picocli.CommandLine; + +import java.io.IOException; + +@CommandLine.Command(name = "import", description = "Update an object collection") +public class ImportCalendars extends AbstractCommand { + + @CommandLine.ArgGroup(multiplicity = "1") + private InputOptions input; + + @CommandLine.Parameters(index = "0", description = "Name of the destination collection") + private String collectionName; + + @Override + public Integer call() { + try { + CalendarCollection collection = CommandConfig.INSTANCE.getCalendarStore().getCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + collection.merge(input.toCalendar()); + } catch (ObjectNotFoundException e) { + getOutputHandler().accept("Collection with name '" + collectionName + "' does not exist"); + return 1; + } catch (ObjectStoreException | ParserException | FailedOperationException | IOException e) { + throw new RuntimeException(e); + } + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCards.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ImportCards.java similarity index 91% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCards.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/ImportCards.java index fd3de9b..094c696 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCards.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ImportCards.java @@ -1,6 +1,5 @@ -package org.ical4j.command.collection; +package org.ical4j.command; -import net.fortuna.ical4j.vcard.VCard; import net.fortuna.ical4j.vcard.property.Uid; import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.CardCollection; diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/InputOptions.java b/ical4j-command-cli/src/main/java/org/ical4j/command/InputOptions.java index 28d1f65..d01ee20 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/InputOptions.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/InputOptions.java @@ -6,6 +6,8 @@ import net.fortuna.ical4j.util.Calendars; import net.fortuna.ical4j.vcard.VCard; import net.fortuna.ical4j.vcard.VCardBuilder; +import org.ical4j.command.config.CommandConfig; +import org.ical4j.connector.*; import picocli.CommandLine; import java.io.FileReader; @@ -17,13 +19,16 @@ */ public class InputOptions { - @CommandLine.Option(names = {"-url"}, required = true) + @CommandLine.Option(names = {"-url"}, required = true, description = "URL to load data from") private URL url; - @CommandLine.Option(names = {"-file"}, required = true) + @CommandLine.Option(names = {"-file"}, required = true, description = "Filename to load data from") private String filename; - @CommandLine.Option(names = {"-", "--stdin"}, required = true) + @CommandLine.Option(names = {"-collection"}, required = true, description = "Collection to load data from") + private String collection; + + @CommandLine.Option(names = {"-", "--stdin"}, required = true, description = "Read data from standard input") private boolean stdin; public boolean hasUrl() { @@ -46,20 +51,22 @@ public boolean isStdin() { return stdin; } - public Calendar toCalendar() throws ParserException, IOException { + public Calendar toCalendar() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { Calendar calendar = null; if (hasFilename()) { - calendar = Calendars.load(getFilename()); + calendar = Calendars.load(getFilename().replaceAll("~", System.getProperty("user.home"))); } else if (hasUrl()) { calendar = Calendars.load(getUrl()); } else if (isStdin()) { final CalendarBuilder builder = new CalendarBuilder(); calendar = builder.build(System.in); + } else { + calendar = ((CalendarCollection) getCollection()).export(); } return calendar; } - public VCard toVCard() throws ParserException, IOException { + public VCard toVCard() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { VCard card = null; if (hasFilename()) { card = new VCardBuilder(new FileReader(getFilename())).build(); @@ -67,7 +74,16 @@ public VCard toVCard() throws ParserException, IOException { card = new VCardBuilder(getUrl().openStream()).build(); } else if (isStdin()) { card = new VCardBuilder(System.in).build(); + } else { + card = ((CardCollection) getCollection()).export(); } return card; } + + public ObjectCollection getCollection() throws ObjectStoreException, ObjectNotFoundException { + if (collection != null) { + return CommandConfig.INSTANCE.getCalendarStore().getCollection(collection); + } + throw new ObjectNotFoundException("Collection not found"); + } } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ListCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ListCommand.java new file mode 100644 index 0000000..5ce1d6b --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ListCommand.java @@ -0,0 +1,116 @@ +package org.ical4j.command; + +import net.fortuna.ical4j.filter.ComponentFilter; +import net.fortuna.ical4j.filter.FilterExpressionParser; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.CalendarComponent; +import org.ical4j.command.config.CommandConfig; +import org.ical4j.connector.CalendarCollection; +import org.ical4j.connector.CardCollection; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +@CommandLine.Command(name = "list", aliases = {"ls"}, description = "List objects in a collection") +public class ListCommand extends AbstractCommand> { + + private static final Logger LOGGER = LoggerFactory.getLogger(ListCommand.class); + + @CommandLine.Parameters(index = "0", description = "List context specification", arity = "0..1", split = ":") + private String[] spec; + + @CommandLine.Option(names = "--filter", description = "Filter expression") + private String filter; + + private final CommandConfig config; + + public ListCommand() { + super(DefaultOutputHandlers.STDOUT_LIST_PRINTER()); + config = CommandConfig.INSTANCE; + } + + public ListCommand(Consumer> outputHandler) { + this(outputHandler, CommandConfig.INSTANCE); + } + + public ListCommand(Consumer> outputHandler, CommandConfig config) { + super(outputHandler); + this.config = config; + } + + @Override + public Integer call() { + try { + List result = new ArrayList<>(); + if (spec == null) { + List calendarCollections = config.getCalendarStore() + .getCollections(config.getActiveWorkspace()); + List cardCollections = config.getCardStore() + .getCollections(config.getActiveWorkspace()); + Set names = new HashSet<>(); + for (CalendarCollection collection : calendarCollections) { + names.add(collection.getDisplayName()); + } + for (CardCollection collection : cardCollections) { + names.add(collection.getDisplayName()); + } + result.addAll(names); + } else { + List calendars = config.getCalendarStore().getCollection(spec[0], + config.getActiveWorkspace()).getAll(); + + List components; + if (spec.length > 1) { + String uid = spec[1]; + components = calendars.stream().filter( + c -> c.getUid() != null && c.getUid().getValue().equals(uid)).findFirst().get().getComponents(); + components.forEach(c -> result.add(printComponent(c))); + } else { + if (filter != null) { + Predicate filterPredicate = new ComponentFilter<>().predicate( + new FilterExpressionParser().parse(filter)); + calendars = calendars.stream().filter( + c -> c.getComponents().stream().anyMatch(filterPredicate)).toList(); + } + for (Calendar calendar : calendars) { + result.add(printCalendar(calendar)); + } + } + } + getOutputHandler().accept(result); + } catch (ObjectStoreException | ObjectNotFoundException e) { + LOGGER.error("Unexpected error", e); + return 1; + } + return 0; + } + + private String printCalendar(Calendar calendar) { + StringBuilder sb = new StringBuilder(); + sb.append(calendar.getUid().getValue()); + sb.append(" | "); + sb.append(calendar.getComponents().get(0).getRequiredProperty("SUMMARY").getValue()); + sb.append(" | "); + sb.append(calendar.getComponents().get(0).getRequiredProperty("DTSTART").getValue()); + return sb.toString(); + } + + private String printComponent(CalendarComponent component) { + StringBuilder sb = new StringBuilder(); + sb.append(component.getUid().get().getValue()); + sb.append(" | "); + sb.append(component.getProperty("SUMMARY")); + sb.append(" | "); + sb.append(component.getProperty("DTSTART") ); + return sb.toString(); + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/MoveCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/MoveCommand.java new file mode 100644 index 0000000..ef7d580 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/MoveCommand.java @@ -0,0 +1,43 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "move", aliases = {"mv"}, description = "Move calendar or contact data between collections") +public class MoveCommand extends AbstractCommand { + + @Override + public Integer call() throws Exception { + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/OutputOptions.java b/ical4j-command-cli/src/main/java/org/ical4j/command/OutputOptions.java new file mode 100644 index 0000000..861f916 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/OutputOptions.java @@ -0,0 +1,46 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +public class OutputOptions { + + @CommandLine.Option(names = {"-file"}, required = true, description = "Filename to save data to") + private String filename; + + @CommandLine.Option(names = {"-collection"}, required = true, description = "Collection to save data to") + private String collection; + + @CommandLine.Option(names = {"--stdout"}, required = true, description = "Write data to standard output") + private boolean stdout; +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/PrintCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/PrintCommand.java new file mode 100644 index 0000000..2efbf07 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/PrintCommand.java @@ -0,0 +1,38 @@ +package org.ical4j.command; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "print", description = "Print calendar or contact data to standard output") +public class PrintCommand { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/ValidatorCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/ValidatorCommand.java index f4f906f..36ea438 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/ValidatorCommand.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/ValidatorCommand.java @@ -55,18 +55,12 @@ public enum ValidationMode { @CommandLine.ArgGroup(multiplicity = "1") private InputOptions input; - @CommandLine.Option(names = "--mode", interactive = true, arity = "0..1", + @CommandLine.Option(names = "--mode", description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "ICALENDAR") private ValidationMode mode; - @CommandLine.Option(names = "--lenient", description = "Enable lenient parsing") - private boolean lenient; - @Override public Integer call() throws Exception { - if (lenient) { - System.setProperty("ical4j.parsing.relaxed", "true"); - } try { ValidationResult result; switch (mode) { diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/VersionProvider.java b/ical4j-command-cli/src/main/java/org/ical4j/command/VersionProvider.java index da2b65b..7e3581b 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/VersionProvider.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/VersionProvider.java @@ -19,7 +19,7 @@ public VersionProvider() { @Override public String[] getVersion() { - return new String[] {String.format("iCal4j Command %s (ical4j=%s, jvm=%s)", + return new String[] {String.format("iCal4j Command-line Tool %s (ical4j=%s, java=%s)", commandVersion, ical4jVersion, javaVersion) }; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/AbstractCalendarCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/AbstractCalendarCommand.java index 6ea4493..6d5ef5c 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/AbstractCalendarCommand.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/AbstractCalendarCommand.java @@ -4,6 +4,8 @@ import net.fortuna.ical4j.model.Calendar; import org.ical4j.command.AbstractCommand; import org.ical4j.command.InputOptions; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; import picocli.CommandLine; import java.io.IOException; @@ -45,7 +47,7 @@ public AbstractCalendarCommand withCalendar(Calendar calendar) { return this; } - public Calendar getCalendar() throws ParserException, IOException { + public Calendar getCalendar() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (calendar == null) { calendar = input.toCalendar(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/DeleteCalendar.java similarity index 88% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCalendar.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/calendar/DeleteCalendar.java index 6722923..61bfd90 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/DeleteCalendar.java @@ -1,6 +1,7 @@ -package org.ical4j.command.collection; +package org.ical4j.command.calendar; import net.fortuna.ical4j.model.Calendar; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,7 +11,7 @@ import static org.ical4j.connector.ObjectCollection.DEFAULT_COLLECTION; -@CommandLine.Command(name = "delete-calendar", description = "Delete an existing calendar") +@CommandLine.Command(name = "remove-calendar", description = "Remove an existing calendar") public class DeleteCalendar extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(DeleteCalendar.class); diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/FilterCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/FilterCalendar.java index 1686021..9cd9aa8 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/FilterCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/FilterCalendar.java @@ -5,6 +5,8 @@ import net.fortuna.ical4j.filter.FilterExpression; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.component.CalendarComponent; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -42,6 +44,8 @@ public Integer call() { } catch (IOException | ParserException e) { LOGGER.error("Unexpected error", e); return 1; + } catch (ObjectStoreException | ObjectNotFoundException e) { + throw new RuntimeException(e); } return 0; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/GetCalendar.java similarity index 87% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCalendar.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/calendar/GetCalendar.java index 87456c6..fa0b5df 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/GetCalendar.java @@ -1,6 +1,7 @@ -package org.ical4j.command.collection; +package org.ical4j.command.calendar; import net.fortuna.ical4j.model.Calendar; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.CalendarCollection; import org.ical4j.connector.ObjectNotFoundException; import org.ical4j.connector.ObjectStore; @@ -11,7 +12,7 @@ import java.util.function.Consumer; -@CommandLine.Command(name = "get-calendar", description = "Retrieve an existing calendar") +@CommandLine.Command(name = "export-calendar", description = "Export a calendar from a collection") public class GetCalendar extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(GetCalendar.class); diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Serializer.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Serializer.java index 6ba3c75..c70bc03 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Serializer.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Serializer.java @@ -106,11 +106,11 @@ private ObjectMapper serializeAtom() { private ObjectMapper serializeJotn(String[] filter) { SimpleModule module = new SimpleModule(); if (filter != null) { - module.addSerializer(Calendar.class, new CalendarSerializer(null, filter)); + module.addSerializer(Calendar.class, new CalendarSerializer(filter)); } else { - module.addSerializer(Calendar.class, new CalendarSerializer(null)); - module.addSerializer(VEvent.class, new VEventSerializer(null)); - module.addSerializer(VTimeZone.class, new VTimeZoneSerializer(null)); + module.addSerializer(Calendar.class, new CalendarSerializer()); + module.addSerializer(VEvent.class, new VEventSerializer()); + module.addSerializer(VTimeZone.class, new VTimeZoneSerializer()); } ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/UpdateCalendar.java similarity index 87% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCalendar.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/calendar/UpdateCalendar.java index 7c0b572..67eb9b4 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/UpdateCalendar.java @@ -1,9 +1,10 @@ -package org.ical4j.command.collection; +package org.ical4j.command.calendar; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.property.Uid; import org.ical4j.command.InputOptions; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +15,7 @@ import static org.ical4j.connector.ObjectCollection.DEFAULT_COLLECTION; -@CommandLine.Command(name = "update-calendar", description = "Update an existing calendar") +@CommandLine.Command(name = "import-calendar", description = "Import a calendar into a collection") public class UpdateCalendar extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(UpdateCalendar.class); @@ -45,7 +46,7 @@ public UpdateCalendar withCalendar(Calendar calendar) { return this; } - public Calendar getCalendar() throws ParserException, IOException { + public Calendar getCalendar() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (calendar == null) { calendar = input.toCalendar(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Validator.java b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Validator.java index 193961d..76a02e4 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Validator.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/calendar/Validator.java @@ -3,6 +3,8 @@ import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.validate.ValidationResult; import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -31,6 +33,8 @@ public Integer call() { } catch (IOException | ParserException e) { LOGGER.error("Unexpected error", e); return 1; + } catch (ObjectStoreException | ObjectNotFoundException e) { + throw new RuntimeException(e); } return 0; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/vcard/AbstractCardCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/AbstractEntityCommand.java similarity index 54% rename from ical4j-command-cli/src/main/java/org/ical4j/command/vcard/AbstractCardCommand.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/AbstractEntityCommand.java index f19cbf2..bef7f9a 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/vcard/AbstractCardCommand.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/AbstractEntityCommand.java @@ -1,34 +1,36 @@ -package org.ical4j.command.vcard; +package org.ical4j.command.card; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.vcard.VCard; import org.ical4j.command.AbstractCommand; import org.ical4j.command.InputOptions; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStoreException; import picocli.CommandLine; import java.io.IOException; import java.util.function.Consumer; -public abstract class AbstractCardCommand extends AbstractCommand { +public abstract class AbstractEntityCommand extends AbstractCommand { @CommandLine.ArgGroup(multiplicity = "1") protected InputOptions input; private VCard card; - public AbstractCardCommand() { + public AbstractEntityCommand() { } - public AbstractCardCommand(Consumer consumer) { + public AbstractEntityCommand(Consumer consumer) { super(consumer); } - public AbstractCardCommand withCard(VCard card) { + public AbstractEntityCommand withCard(VCard card) { this.card = card; return this; } - public VCard getCard() throws ParserException, IOException { + public VCard getCard() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (card == null) { card = input.toVCard(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/entity/CardCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/CardCommandGroup.java similarity index 83% rename from ical4j-command-cli/src/main/java/org/ical4j/command/entity/CardCommandGroup.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/CardCommandGroup.java index 712dc55..c6648d3 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/entity/CardCommandGroup.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/CardCommandGroup.java @@ -1,4 +1,4 @@ -package org.ical4j.command.entity; +package org.ical4j.command.card; import picocli.CommandLine; diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCard.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/DeleteCard.java similarity index 88% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCard.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/DeleteCard.java index 31309b0..eb0fc4d 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/DeleteCard.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/DeleteCard.java @@ -1,6 +1,7 @@ -package org.ical4j.command.collection; +package org.ical4j.command.card; import net.fortuna.ical4j.vcard.VCard; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,7 +9,7 @@ import java.util.function.Consumer; -@CommandLine.Command(name = "delete-card", description = "Delete an existing card") +@CommandLine.Command(name = "remove-card", description = "Remove an existing card") public class DeleteCard extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(DeleteCard.class); diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCard.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/GetCard.java similarity index 86% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCard.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/GetCard.java index f337c75..08923a5 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCard.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/GetCard.java @@ -1,6 +1,7 @@ -package org.ical4j.command.collection; +package org.ical4j.command.card; import net.fortuna.ical4j.vcard.VCard; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,7 +9,7 @@ import java.util.function.Consumer; -@CommandLine.Command(name = "get-card", description = "Retrieve an existing card") +@CommandLine.Command(name = "export-card", description = "Export a card from a collection") public class GetCard extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(GetCard.class); diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/vcard/Serializer.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/Serializer.java similarity index 90% rename from ical4j-command-cli/src/main/java/org/ical4j/command/vcard/Serializer.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/Serializer.java index 05ecfa1..64fc7cc 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/vcard/Serializer.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/Serializer.java @@ -1,10 +1,11 @@ -package org.ical4j.command.vcard; +package org.ical4j.command.card; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyName; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import net.fortuna.ical4j.vcard.Entity; import net.fortuna.ical4j.vcard.VCard; import org.mnode.ical4j.serializer.JCardSerializer; import org.mnode.ical4j.serializer.XCardSerializer; @@ -14,7 +15,7 @@ import picocli.CommandLine; @CommandLine.Command(name = "serialize", description = "Serialize a specified vCard") -public class Serializer extends AbstractCardCommand { +public class Serializer extends AbstractEntityCommand { public enum FormatOption { json, xml, jotn, atom, jmap, jsonld; @@ -61,7 +62,7 @@ public Integer call() throws Exception { private ObjectMapper serializeJson() { SimpleModule module = new SimpleModule(); - module.addSerializer(VCard.class, new JCardSerializer(null)); + module.addSerializer(Entity.class, new JCardSerializer(null)); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); return mapper; @@ -69,7 +70,7 @@ private ObjectMapper serializeJson() { private ObjectMapper serializeXml() { SimpleModule module = new SimpleModule(); - module.addSerializer(VCard.class, new XCardSerializer(null)); + module.addSerializer(Entity.class, new XCardSerializer(null)); XmlMapper mapper = XmlMapper.builder().defaultUseWrapper(true).build(); mapper.setConfig(mapper.getSerializationConfig().withRootName( PropertyName.construct("vcard", "urn:ietf:params:xml:ns:vcard-4.0")) @@ -91,7 +92,7 @@ private ObjectMapper serializeAtom() { private ObjectMapper serializeJotn(String[] filter) { SimpleModule module = new SimpleModule(); - module.addSerializer(VCard.class, new VCardSerializer(null)); + module.addSerializer(Entity.class, new VCardSerializer()); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); return mapper; @@ -107,7 +108,7 @@ private ObjectMapper serializeJmap() { private ObjectMapper serializeJsonld() { SimpleModule module = new SimpleModule(); - module.addSerializer(VCard.class, new PersonJsonLdSerializer(null)); + module.addSerializer(Entity.class, new PersonJsonLdSerializer(null)); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); return mapper; diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCard.java b/ical4j-command-cli/src/main/java/org/ical4j/command/card/UpdateCard.java similarity index 84% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCard.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/card/UpdateCard.java index b72b698..34e6d21 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCard.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/card/UpdateCard.java @@ -1,10 +1,11 @@ -package org.ical4j.command.collection; +package org.ical4j.command.card; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.ConstraintViolationException; import net.fortuna.ical4j.vcard.VCard; import net.fortuna.ical4j.vcard.property.Uid; import org.ical4j.command.InputOptions; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,7 +14,7 @@ import java.io.IOException; import java.util.function.Consumer; -@CommandLine.Command(name = "update-card", description = "Update an existing card") +@CommandLine.Command(name = "import-card", description = "Import a card into a collection") public class UpdateCard extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(UpdateCard.class); @@ -40,7 +41,7 @@ public UpdateCard withCard(VCard card) { return this; } - public VCard getCard() throws ParserException, IOException { + public VCard getCard() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (card == null) { card = input.toVCard(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/AddCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/AddCollection.java new file mode 100644 index 0000000..604109b --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/AddCollection.java @@ -0,0 +1,90 @@ +package org.ical4j.command.collection; + +import org.ical4j.command.config.CommandConfig; +import org.ical4j.command.workspace.AbstractWorkspaceCommand; +import org.ical4j.connector.ObjectCollection; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStore; +import org.ical4j.connector.ObjectStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.util.function.Consumer; + +/** + * A command to create a new collection for a specified object store. + */ +@CommandLine.Command(name = "mkcol", description = "Add a new collection") +public class AddCollection extends AbstractWorkspaceCommand, String> { + + public enum CollectionType { + CALENDAR, + CARD + } + + private static final Logger LOGGER = LoggerFactory.getLogger(AddCollection.class); + + @CommandLine.Parameters(index = "0", description = "Name of the collection to create") + private String collectionName; + + @CommandLine.Option(names = "--type", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "CALENDAR") + private CollectionType collectionType; + + public AddCollection() { + } + + public AddCollection(Consumer outputHandler, ObjectStore> store) { + super(outputHandler); + setStore(store); + } + + public AddCollection withCollectionName(String collectionName) { + this.collectionName = collectionName; + return this; + } + + @Override + public Integer call() { + try { + // Injected store: add directly to the supplied store. + if (getStore() != null) { + getStore().addCollection(collectionName); + getOutputHandler().accept("Collection '" + collectionName + "' created successfully"); + return 0; + } + + // CLI path: resolve the appropriate store from configuration by type. + ObjectCollection collection = null; + if (collectionType == CollectionType.CALENDAR) { + try { + CommandConfig.INSTANCE.getCalendarStore().getCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + getOutputHandler().accept("Collection with name '" + collectionName + "' already exists"); + return 1; + } catch (ObjectNotFoundException e) { + } + collection = CommandConfig.INSTANCE.getCalendarStore().addCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + } else if (collectionType == CollectionType.CARD) { + try { + CommandConfig.INSTANCE.getCardStore().getCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + getOutputHandler().accept("Collection with name '" + collectionName + "' already exists"); + return 1; + } catch (ObjectNotFoundException e) { + } + collection = CommandConfig.INSTANCE.getCardStore().addCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + } + if (collection != null) { + getOutputHandler().accept("Collection '" + collectionName + "' created successfully"); + } + } catch (ObjectStoreException e) { + LOGGER.error("Unexpected error", e); + return 1; + } + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CollectionCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CollectionCommandGroup.java index 7931668..34a8e3a 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CollectionCommandGroup.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CollectionCommandGroup.java @@ -1,11 +1,12 @@ package org.ical4j.command.collection; +import org.ical4j.command.ImportCalendars; +import org.ical4j.command.ListCommand; import picocli.CommandLine; @CommandLine.Command(name = "collections", description = "Manage calendar and card collections", - subcommands = {GetCalendar.class, ListCalendars.class, CreateCalendar.class, UpdateCalendar.class, - DeleteCalendar.class, GetCard.class, ListCards.class, CreateCard.class, UpdateCard.class, - DeleteCard.class,}) + subcommands = {AddCollection.class, ListCommand.class, + RemoveCollection.class, ImportCalendars.class}) public class CollectionCommandGroup { } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/GetCollectionDetails.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCollectionDetails.java similarity index 93% rename from ical4j-command-cli/src/main/java/org/ical4j/command/workspace/GetCollectionDetails.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCollectionDetails.java index 3104324..b66f36f 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/GetCollectionDetails.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/GetCollectionDetails.java @@ -1,5 +1,6 @@ -package org.ical4j.command.workspace; +package org.ical4j.command.collection; +import org.ical4j.command.workspace.AbstractWorkspaceCommand; import org.ical4j.connector.ObjectCollection; import org.ical4j.connector.ObjectNotFoundException; import org.ical4j.connector.ObjectStore; diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCalendars.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCalendars.java deleted file mode 100644 index 9a38154..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ImportCalendars.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.ical4j.command.collection; - -import net.fortuna.ical4j.model.Calendar; -import net.fortuna.ical4j.model.property.Uid; -import org.ical4j.command.collection.AbstractCollectionCommand; -import org.ical4j.connector.CalendarCollection; -import org.ical4j.connector.ObjectStore; -import picocli.CommandLine; - -import java.util.List; -import java.util.function.Consumer; - -@CommandLine.Command(name = "import", description = "Update an object collection") -public class ImportCalendars extends AbstractCollectionCommand> { - - public ImportCalendars() { - } - - public ImportCalendars(String collection, Consumer> consumer) { - super(collection, consumer); - } - - public ImportCalendars(String collection, Consumer> consumer, ObjectStore store) { - super(collection, consumer, store); - } - - @Override - public Integer call() { - return 1; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ListCalendars.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ListCalendars.java deleted file mode 100644 index de1a4ea..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/ListCalendars.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.ical4j.command.collection; - -import net.fortuna.ical4j.model.Calendar; -import org.ical4j.connector.CalendarCollection; -import org.ical4j.connector.ObjectNotFoundException; -import org.ical4j.connector.ObjectStore; -import org.ical4j.connector.ObjectStoreException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import static org.ical4j.command.DefaultOutputHandlers.STDOUT_LIST_PRINTER; -import static org.ical4j.connector.ObjectCollection.DEFAULT_COLLECTION; - -@CommandLine.Command(name = "list-calendars", description = "List all existing calendars") -public class ListCalendars extends AbstractCollectionCommand> { - - private static final Logger LOGGER = LoggerFactory.getLogger(ListCalendars.class); - - public ListCalendars() { - super(DEFAULT_COLLECTION, STDOUT_LIST_PRINTER()); - } - - public ListCalendars(String collectionName, Consumer> consumer) { - super(collectionName, consumer); - } - - public ListCalendars(ObjectStore store) { - super(DEFAULT_COLLECTION, STDOUT_LIST_PRINTER(), store); - } - - public ListCalendars(String collectionName, ObjectStore store) { - super(collectionName, STDOUT_LIST_PRINTER(), store); - } - - @Override - public Integer call() { - try { - List calendars = new ArrayList<>(); - for (String uid : getCollection().listObjectUIDs()) { - calendars.add(getCollection().getCalendar(uid)); - } - getOutputHandler().accept(calendars); - } catch (ObjectStoreException | ObjectNotFoundException e) { - LOGGER.error("Unexpected error", e); - return 1; - } - return 0; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/QueryCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/QueryCollection.java deleted file mode 100644 index 8e3e592..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/QueryCollection.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.ical4j.command.collection; - -import org.ical4j.connector.ObjectCollection; -import org.ical4j.connector.ObjectStore; -import picocli.CommandLine; - -import java.util.List; -import java.util.function.Consumer; - -import static org.ical4j.connector.ObjectCollection.DEFAULT_COLLECTION; - -@CommandLine.Command(name = "query-collection", - description = "Query a calendar collection for calendar subsets matching an expression") -public class QueryCollection extends AbstractCollectionCommand, List> { - - public QueryCollection() { - super(); - } - - public QueryCollection(Consumer> consumer, ObjectStore> store) { - super(DEFAULT_COLLECTION, consumer, store); - } - - public QueryCollection(String collectionName, Consumer> consumer, ObjectStore> store) { - super(collectionName, consumer, store); - } - - @Override - public Integer call() { - //TODO: add support for get all calendars in calendar collection.. - return 1; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/RemoveCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/RemoveCollection.java new file mode 100644 index 0000000..fdb5298 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/RemoveCollection.java @@ -0,0 +1,71 @@ +package org.ical4j.command.collection; + +import org.ical4j.command.config.CommandConfig; +import org.ical4j.command.workspace.AbstractWorkspaceCommand; +import org.ical4j.connector.ObjectCollection; +import org.ical4j.connector.ObjectNotFoundException; +import org.ical4j.connector.ObjectStore; +import org.ical4j.connector.ObjectStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.util.function.Consumer; + +/** + * A command to delete an existing collection from an object store. + */ +@CommandLine.Command(name = "rmcol", description = "Purge a collection") +public class RemoveCollection extends AbstractWorkspaceCommand, String> { + + private static final Logger LOGGER = LoggerFactory.getLogger(RemoveCollection.class); + + @CommandLine.Parameters(index = "0", description = "Name of the collection to remove") + private String collectionName; + + @CommandLine.Option(names = "--type", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "CALENDAR") + private AddCollection.CollectionType collectionType; + + public RemoveCollection() { + } + + public RemoveCollection(Consumer outputHandler, ObjectStore> store) { + super(outputHandler); + setStore(store); + } + + public RemoveCollection withCollectionName(String collectionName) { + this.collectionName = collectionName; + return this; + } + + @Override + public Integer call() { + try { + // Injected store: remove directly from the supplied store. + if (getStore() != null) { + ObjectCollection collection = getStore().getCollection(collectionName); + collection.delete(); + getOutputHandler().accept("Collection '" + collectionName + "' deleted"); + return 0; + } + + // CLI path: resolve the appropriate store from configuration by type. + ObjectCollection collection; + if (collectionType == AddCollection.CollectionType.CARD) { + collection = CommandConfig.INSTANCE.getCardStore().getCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + } else { + collection = CommandConfig.INSTANCE.getCalendarStore().getCollection(collectionName, + CommandConfig.INSTANCE.getActiveWorkspace()); + } + collection.delete(); + getOutputHandler().accept("Collection '" + collectionName + "' deleted"); + } catch (ObjectStoreException | ObjectNotFoundException e) { + LOGGER.error("Unexpected error", e); + return 1; + } + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/UpdateCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCollection.java similarity index 87% rename from ical4j-command-cli/src/main/java/org/ical4j/command/workspace/UpdateCollection.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCollection.java index 3e4e8c4..15464b3 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/UpdateCollection.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/collection/UpdateCollection.java @@ -1,5 +1,6 @@ -package org.ical4j.command.workspace; +package org.ical4j.command.collection; +import org.ical4j.command.workspace.AbstractWorkspaceCommand; import org.ical4j.connector.ObjectCollection; import org.ical4j.connector.ObjectStore; import picocli.CommandLine; diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/config/CommandConfig.java b/ical4j-command-cli/src/main/java/org/ical4j/command/config/CommandConfig.java new file mode 100644 index 0000000..f81198e --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/config/CommandConfig.java @@ -0,0 +1,132 @@ +package org.ical4j.command.config; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import com.electronwill.nightconfig.core.file.FileConfig; +import org.ical4j.command.util.Filesystem; +import org.ical4j.connector.CalendarCollection; +import org.ical4j.connector.CardCollection; +import org.ical4j.connector.ObjectStore; +import org.ical4j.connector.local.LocalCalendarStore; +import org.ical4j.connector.local.LocalCardStore; + +import java.io.File; + +/** + * Persists command configuration. + */ +public class CommandConfig { + + public static final CommandConfig INSTANCE = new CommandConfig(); + + private final FileConfig fileConfig; + + public CommandConfig() { + this(FileConfig.builder(new File(Filesystem.getDataDirectory(), "iCal4j/ictrc.toml")) + .defaultResource("/ictrc.default").autosave().sync().build()); + } + + public CommandConfig(FileConfig fileConfig) { + this.fileConfig = fileConfig; + } + + public String get(String key, String defaultValue) { + fileConfig.load(); + return fileConfig.getOrElse(key, defaultValue); + } + + public String set(String key, String value) { + String previous = get(key, null); + fileConfig.set(key, value); + return previous; + } + + public String getUserAgent() { + return get("user.agent", "iCal4j-Command-Line/1.0"); + } + + public String getUserEmail() { + return get("user.email", ""); + } + + public String setUserEmail(String email) { + return set("user.email", email); + } + + public String getUserName() { + return get("user.name", ""); + } + + public String setUserName(String name) { + return set("user.name", name); + } + + public String getActiveWorkspace() { + return get("workspace.active", "default"); + } + + public String setActiveWorkspace(String workspaceId) { + return set("workspace.active", workspaceId); + } + + public ObjectStore getCalendarStore() { + return new LocalCalendarStore(new File(Filesystem.getDataDirectory(), + "iCal4j/" + get("store.calendar.path", "calendars"))); + } + + public String setCalendarStorePath(String path) { + return set("store.calendar.path", path); + } + + public ObjectStore getCardStore() { + return new LocalCardStore(new File(Filesystem.getDataDirectory(), "iCal4j/" + + get("store.card.path", "vcards"))); + } + + public String setCardStorePath(String path) { + return set("store.card.path", path); + } + + public String getDefaultContentType(String concept) { + return get("contentType." + concept, + get("contentType.default", "text/plain")); + } + + public String setDefaultContentType(String concept, String contentType) { + return set("contentType." + concept, contentType); + } + + public String setDefaultContentType(String contentType) { + return set("contentType.default", contentType); + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/config/ConfigureCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/config/ConfigureCommand.java new file mode 100644 index 0000000..d05814e --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/config/ConfigureCommand.java @@ -0,0 +1,30 @@ +package org.ical4j.command.config; + +import org.ical4j.command.AbstractCommand; +import picocli.CommandLine; + +import java.util.List; + +@CommandLine.Command(name = "configure", description = "Configure settings") +public class ConfigureCommand extends AbstractCommand { + + private static final List CONFIG_SECTIONS = List.of("user", "workspace", "strategy", "editor", + "filter", "remote"); + + @CommandLine.Parameters(index = "0", description = "Setting to configure") + private String setting; + + @CommandLine.Parameters(index = "1", description = "Setting value", arity = "1..*") + private List value; + + @Override + public Integer call() throws Exception { + if (!CONFIG_SECTIONS.contains(setting.split("\\.")[0])) { + getOutputHandler().accept("Unknown setting: " + setting); + return 1; + } + String previous = CommandConfig.INSTANCE.set(setting, String.join(" ", value)); + getOutputHandler().accept("Updated setting '" + setting + "' from '" + previous + "' to '" + String.join(" ", value) + "'"); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddAttachment.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddAttachment.java new file mode 100644 index 0000000..9d91953 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddAttachment.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "attach", description = "Adds an attachment to data") +public class AddAttachment { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCategories.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCategories.java new file mode 100644 index 0000000..60c105f --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCategories.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "categories", description = "Adds categories to data") +public class AddCategories { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCommandGroup.java new file mode 100644 index 0000000..3d7c1a2 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddCommandGroup.java @@ -0,0 +1,44 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +import java.util.List; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "add", description = "Add metadata properties to data", + subcommands = {AddAttachment.class, AddCategories.class, AddComment.class, AddRelated.class}) +public class AddCommandGroup { + + private static final List PROPS = List.of("ATTACH", "ATTENDEE", "CATEGORIES", "COMMENT", + "CONTACT", "EXDATE", "RDATE", "RELATED-TO", "REQUEST-STATUS", "RESOURCES"); +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddComment.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddComment.java new file mode 100644 index 0000000..a3d17a1 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddComment.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "comment", description = "Adds a comment to data") +public class AddComment { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddRelated.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddRelated.java new file mode 100644 index 0000000..c1d3720 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/AddRelated.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "related", description = "Adds related-to links to data") +public class AddRelated { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/GetProperty.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/GetProperty.java new file mode 100644 index 0000000..9986701 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/GetProperty.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "get", description = "Gets a property from components in an iCalendar file.") +public class GetProperty { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/PropertyCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/PropertyCommandGroup.java new file mode 100644 index 0000000..0b65480 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/PropertyCommandGroup.java @@ -0,0 +1,35 @@ +package org.ical4j.command.metadata; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +public class PropertyCommandGroup { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/SetCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/SetCommandGroup.java new file mode 100644 index 0000000..a675d17 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/SetCommandGroup.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "set", description = "Sets a property on components in an iCalendar file.") +public class SetCommandGroup { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/UnsetProperty.java b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/UnsetProperty.java new file mode 100644 index 0000000..e292166 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/metadata/UnsetProperty.java @@ -0,0 +1,38 @@ +package org.ical4j.command.metadata; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "unset", description = "Removes a property from components in an iCalendar file.") +public class UnsetProperty { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/ListRevisions.java b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/ListRevisions.java new file mode 100644 index 0000000..75d5c4b --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/ListRevisions.java @@ -0,0 +1,38 @@ +package org.ical4j.command.revisions; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "revisions", description = "List revisions for calendar or contact data") +public class ListRevisions { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/RevertRevision.java b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/RevertRevision.java new file mode 100644 index 0000000..67f36d3 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/RevertRevision.java @@ -0,0 +1,38 @@ +package org.ical4j.command.revisions; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "revert", description = "Revert to a specific revision") +public class RevertRevision { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/UndoRevisions.java b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/UndoRevisions.java new file mode 100644 index 0000000..e327933 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/revisions/UndoRevisions.java @@ -0,0 +1,38 @@ +package org.ical4j.command.revisions; + +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "undo", description = "Undo the last revision of calendar or contact data") +public class UndoRevisions { +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateAction.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateAction.java new file mode 100644 index 0000000..e7242bc --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateAction.java @@ -0,0 +1,104 @@ +package org.ical4j.command.strategy; + +import net.fortuna.ical4j.extensions.strategy.action.*; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.VToDo; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.util.RandomUidGenerator; +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.InputHandler; +import org.ical4j.command.config.CommandConfig; +import picocli.CommandLine; + +import java.io.File; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.util.function.Consumer; + +@CommandLine.Command(name = "action", description = "Generate an action item") +public class CreateAction extends AbstractCommand implements InputHandler { + + public enum ActionType { + Action, Agenda, Backlog, Milestone, Project, Task + } + + @CommandLine.Parameters(index = "0", description = "Object context specification", arity = "1", split = ":") + private String[] spec; + +// @CommandLine.ArgGroup(multiplicity = "1") +// private OutputOptions output; + + @CommandLine.Option(names = "--summary", arity = "0..1") + private String summary; + + @CommandLine.Option(names = "--type", arity = "0..1", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "Action") + private ActionType type; + + @CommandLine.Option(names = "--edit", arity = "0..1") + private boolean edit; + + public CreateAction() { + super(DefaultOutputHandlers.VALIDATING_CALENDAR_PRINTER(new PrintWriter(System.out, true))); + } + + public CreateAction(Consumer consumer) { + super(consumer); + } + + @Override + public Integer call() throws Exception { + String description = null; + if (summary == null && System.console() != null) { + summary = singleInput("Summary"); + } + if (edit) { + File temp = File.createTempFile("cal", ".md"); + ProcessBuilder pb = new ProcessBuilder("subl", "-w", temp.getAbsolutePath()); + Process p = pb.start(); + p.waitFor(); +// Desktop.getDesktop().edit(temp); +// while (temp.length() == 0) { +// Thread.sleep(1000); +// } + description = Files.readString(temp.toPath()); +// description = multiInput("Description"); + } + if (type == null && System.console() != null) { + type = ActionType.valueOf(singleInput("Type")); + } + VToDo action = new VToDo().add(new RandomUidGenerator().generateUid()) + .add(new Summary(summary)); + if (description != null) { + action.add(new Description(description)); + } + switch (type) { + case Agenda -> { + action = new Agenda().withPrototype(action).get(); + } + case Backlog -> { + action = new Backlog().withPrototype(action).get(); + } + case Milestone -> { + action = new Milestone().withPrototype(action).get(); + } + case Project -> { + action = new Project().withPrototype(action).get(); + } + case Task -> { + action = new Task().withPrototype(action).get(); + } + default -> { + action = new Action().withPrototype(action).get(); + } + } + Calendar calendar = new Calendar().withDefaults() + .withProperty(new ProdId(CommandConfig.INSTANCE.getUserAgent())) + .withComponent(action).getFluentTarget(); + getOutputHandler().accept(calendar); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCalendar.java similarity index 92% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCalendar.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCalendar.java index d749327..c0f9755 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCalendar.java @@ -1,9 +1,10 @@ -package org.ical4j.command.collection; +package org.ical4j.command.strategy; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.ConstraintViolationException; import org.ical4j.command.InputOptions; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,7 @@ * * overrideUid - always replace any existing UID with an internally generated one (implies generateUID = true) *

*/ -@CommandLine.Command(name = "create-calendar", description = "Create a new calendar") +@CommandLine.Command(name = "calendar", description = "Generate a new calendar") public class CreateCalendar extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(CreateCalendar.class); @@ -64,7 +65,7 @@ public CreateCalendar withCalendar(Calendar calendar) { return this; } - public Calendar getCalendar() throws ParserException, IOException { + public Calendar getCalendar() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (calendar == null) { calendar = input.toCalendar(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCard.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCard.java similarity index 84% rename from ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCard.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCard.java index 7f66c52..0a5973b 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/collection/CreateCard.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateCard.java @@ -1,9 +1,10 @@ -package org.ical4j.command.collection; +package org.ical4j.command.strategy; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.ConstraintViolationException; import net.fortuna.ical4j.vcard.VCard; import org.ical4j.command.InputOptions; +import org.ical4j.command.collection.AbstractCollectionCommand; import org.ical4j.connector.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +15,7 @@ import static org.ical4j.connector.ObjectCollection.DEFAULT_COLLECTION; -@CommandLine.Command(name = "create-card", description = "Create a new card") +@CommandLine.Command(name = "card", description = "Generate a new card") public class CreateCard extends AbstractCollectionCommand { private static final Logger LOGGER = LoggerFactory.getLogger(CreateCard.class); @@ -37,7 +38,7 @@ public CreateCard withCard(VCard card) { return this; } - public VCard getCard() throws ParserException, IOException { + public VCard getCard() throws ParserException, IOException, ObjectStoreException, ObjectNotFoundException { if (card == null) { card = input.toVCard(); } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEntity.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEntity.java new file mode 100644 index 0000000..9620e55 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEntity.java @@ -0,0 +1,66 @@ +package org.ical4j.command.strategy; + +import net.fortuna.ical4j.extensions.strategy.entity.Individual; +import net.fortuna.ical4j.extensions.strategy.entity.Organization; +import net.fortuna.ical4j.vcard.Entity; +import net.fortuna.ical4j.vcard.VCard; +import net.fortuna.ical4j.vcard.property.Fn; +import net.fortuna.ical4j.vcard.property.Note; +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.InputHandler; +import picocli.CommandLine; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +@CommandLine.Command(name = "entity", description = "Generate an entity") +public class CreateEntity extends AbstractCommand implements InputHandler { + + public enum EntityType { + Individual, Org, Group, Location + } + + @CommandLine.Option(names = "--formatted-name", interactive = true) + private String fn; + + @CommandLine.Option(names = "--note", interactive = true) + private String note; + + @CommandLine.Option(names = "--type", interactive = true, arity = "0..1", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "Individual") + private EntityType type; + + public CreateEntity() { + super(DefaultOutputHandlers.VALIDATING_VCARD_PRINTER(new PrintWriter(System.out, true))); + } + + public CreateEntity(Consumer consumer) { + super(consumer); + } + + @Override + public Integer call() throws Exception { + if (fn == null && System.console() != null) { + fn = singleInput("Formatted Name"); + } + if (note == null && System.console() != null) { + note = multiInput("Note"); + } + if (type == null && System.console() != null) { + type = EntityType.valueOf(singleInput("Type")); + } + Entity entity = new Entity().add(new Fn(fn)).add(new Note(note)); + switch (type) { + case Org -> { + entity = new Organization().withPrototype(entity).get(); + } + default -> { + entity = new Individual().withPrototype(entity).get(); + } + } + VCard card = (VCard) new VCard().add(entity); + getOutputHandler().accept(card); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/EventCalendar.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEvent.java similarity index 59% rename from ical4j-command-cli/src/main/java/org/ical4j/command/strategy/EventCalendar.java rename to ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEvent.java index 5bd6601..c2f89b6 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/EventCalendar.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateEvent.java @@ -1,9 +1,12 @@ package org.ical4j.command.strategy; +import net.fortuna.ical4j.extensions.strategy.event.Meeting; import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.Description; -import net.fortuna.ical4j.model.property.DtStamp; +import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.util.RandomUidGenerator; import org.ical4j.command.AbstractCommand; import org.ical4j.command.DefaultOutputHandlers; import org.ical4j.command.InputHandler; @@ -12,8 +15,8 @@ import java.io.PrintWriter; import java.util.function.Consumer; -@CommandLine.Command(name = "event", description = "Create a new event calendar object") -public class EventCalendar extends AbstractCommand implements InputHandler { +@CommandLine.Command(name = "event", description = "Generate an event") +public class CreateEvent extends AbstractCommand implements InputHandler { @CommandLine.Option(names = "--summary", interactive = true) private String summary; @@ -21,11 +24,11 @@ public class EventCalendar extends AbstractCommand implements InputHan @CommandLine.Option(names = "--description", interactive = true) private String description; - public EventCalendar() { + public CreateEvent() { super(DefaultOutputHandlers.VALIDATING_CALENDAR_PRINTER(new PrintWriter(System.out, true))); } - public EventCalendar(Consumer consumer) { + public CreateEvent(Consumer consumer) { super(consumer); } @@ -37,10 +40,12 @@ public Integer call() throws Exception { if (description == null && System.console() != null) { description = multiInput("Description"); } + VEvent meeting = new Meeting().withPrototype(new VEvent().add(new Summary(summary)) + .add(new Description(description)) + .add(new RandomUidGenerator().generateUid())).get(); Calendar calendar = new Calendar().withDefaults() - .withProperty(new DtStamp()) - .withProperty(new Summary(summary)) - .withProperty(new Description(description)).getFluentTarget(); + .withProperty(new ProdId("-//ical4j//iCal4j 4.0//EN")) + .withComponent(meeting).getFluentTarget(); getOutputHandler().accept(calendar); return 0; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateIssue.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateIssue.java new file mode 100644 index 0000000..54d4177 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateIssue.java @@ -0,0 +1,69 @@ +package org.ical4j.command.strategy; + +import net.fortuna.ical4j.extensions.model.concept.IssueType; +import net.fortuna.ical4j.extensions.strategy.issue.Issue; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.VToDo; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.util.RandomUidGenerator; +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.InputHandler; +import picocli.CommandLine; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +@CommandLine.Command(name = "issue", description = "Generate an issue") +public class CreateIssue extends AbstractCommand implements InputHandler { + +// public enum IssueType { +// Issue +// } + + @CommandLine.Option(names = "--summary", interactive = true) + private String summary; + + @CommandLine.Option(names = "--description", interactive = true) + private String description; + + @CommandLine.Option(names = "--type", interactive = true, arity = "0..1", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "Issue") + private IssueType.Id type; + + public CreateIssue() { + super(DefaultOutputHandlers.VALIDATING_CALENDAR_PRINTER(new PrintWriter(System.out, true))); + } + + public CreateIssue(Consumer consumer) { + super(consumer); + } + + @Override + public Integer call() throws Exception { + if (summary == null && System.console() != null) { + summary = singleInput("Summary"); + } + if (description == null && System.console() != null) { + description = multiInput("Description"); + } + if (type == null && System.console() != null) { + type = IssueType.Id.valueOf(singleInput("Type")); + } + VToDo issue = new VToDo().add(new RandomUidGenerator().generateUid()) + .add(new Summary(summary)) + .add(new Description(description)); + switch (type) { + default -> { + issue = new Issue().withPrototype(issue).get(); + } + } + Calendar calendar = new Calendar().withDefaults() + .withProperty(new ProdId("-//ical4j//iCal4j 4.0//EN")) + .withComponent(issue).getFluentTarget(); + getOutputHandler().accept(calendar); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateNote.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateNote.java new file mode 100644 index 0000000..10448d7 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateNote.java @@ -0,0 +1,71 @@ +package org.ical4j.command.strategy; + +import net.fortuna.ical4j.extensions.model.concept.NoteType; +import net.fortuna.ical4j.extensions.strategy.note.Note; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.VJournal; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.util.RandomUidGenerator; +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.InputHandler; +import picocli.CommandLine; + +import java.io.PrintWriter; +import java.time.LocalDate; +import java.util.function.Consumer; + +@CommandLine.Command(name = "note", description = "Generate a note") +public class CreateNote extends AbstractCommand implements InputHandler { + +// public enum NoteType { +// Decision, Note, Objective, Risk +// } + + @CommandLine.Option(names = "--summary", interactive = true) + private String summary; + + @CommandLine.Option(names = "--description", interactive = true) + private String description; + + @CommandLine.Option(names = "--type", interactive = true, arity = "0..1", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "Note") + private NoteType.Id type; + + public CreateNote() { + super(DefaultOutputHandlers.VALIDATING_CALENDAR_PRINTER(new PrintWriter(System.out, true))); + } + + public CreateNote(Consumer consumer) { + super(consumer); + } + + @Override + public Integer call() throws Exception { + if (summary == null && System.console() != null) { + summary = singleInput("Summary"); + } + if (description == null && System.console() != null) { + description = multiInput("Description"); + } + if (type == null && System.console() != null) { + type = NoteType.Id.valueOf(singleInput("Type")); + } + VJournal note = new VJournal().add(new RandomUidGenerator().generateUid()) + .add(new Summary(summary)) + .add(new Description(description)); + switch (type) { + default -> { + note = new Note().title(summary).date(LocalDate.now()) + .withPrototype(note).get(); + } + } + Calendar calendar = new Calendar().withDefaults() + .withProperty(new ProdId("-//ical4j//iCal4j 4.0//EN")) + .withComponent(note).getFluentTarget(); + getOutputHandler().accept(calendar); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateObservance.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateObservance.java new file mode 100644 index 0000000..546a134 --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/CreateObservance.java @@ -0,0 +1,68 @@ +package org.ical4j.command.strategy; + +import net.fortuna.ical4j.extensions.strategy.observance.Observance; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.util.RandomUidGenerator; +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.InputHandler; +import picocli.CommandLine; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +@CommandLine.Command(name = "observance", description = "Generate an observance") +public class CreateObservance extends AbstractCommand implements InputHandler { + + public enum ObservanceType { + Anniversary, Observance + } + + @CommandLine.Option(names = "--summary", interactive = true) + private String summary; + + @CommandLine.Option(names = "--description", interactive = true) + private String description; + + @CommandLine.Option(names = "--type", interactive = true, arity = "0..1", + description = "Valid values: ${COMPLETION-CANDIDATES}", defaultValue = "Observance") + private ObservanceType type; + + public CreateObservance() { + super(DefaultOutputHandlers.VALIDATING_CALENDAR_PRINTER(new PrintWriter(System.out, true))); + } + + public CreateObservance(Consumer consumer) { + super(consumer); + } + + @Override + public Integer call() throws Exception { + if (summary == null && System.console() != null) { + summary = singleInput("Summary"); + } + if (description == null && System.console() != null) { + description = multiInput("Description"); + } + if (type == null && System.console() != null) { + type = ObservanceType.valueOf(singleInput("Type")); + } + VEvent observance = new VEvent().add(new RandomUidGenerator().generateUid()) + .add(new Summary(summary)) + .add(new Description(description)); + switch (type) { + default -> { + observance = new Observance().withPrototype(observance).get(); + } + } + Calendar calendar = new Calendar().withDefaults() + .withProperty(new ProdId("-//ical4j//iCal4j 4.0//EN")) + .withComponent(observance).getFluentTarget(); + getOutputHandler().accept(calendar); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyCommandGroup.java index f03f65d..161046a 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyCommandGroup.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyCommandGroup.java @@ -2,8 +2,9 @@ import picocli.CommandLine; -@CommandLine.Command(name = "strategy", description = "Create calendar and card objects using predefined strategies", - subcommands = {CreateAction.class, CreateEntity.class, CreateEvent.class, }) +@CommandLine.Command(name = "generate", aliases = {"new"}, description = "Generate objects using predefined strategies", + subcommands = {CreateAction.class, CreateEntity.class, CreateEvent.class, + CreateIssue.class, CreateNote.class, CreateObservance.class}) public class StrategyCommandGroup { } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyInfo.java b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyInfo.java new file mode 100644 index 0000000..9f1b89e --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/strategy/StrategyInfo.java @@ -0,0 +1,79 @@ +package org.ical4j.command.strategy; + +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import picocli.CommandLine; + +import java.util.List; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "strategy", description = "Information about input strategies") +public class StrategyInfo extends AbstractCommand> { + + @CommandLine.Parameters(index = "0", description = "The strategy to get information about", arity = "0..1") + private String strategy; + + public StrategyInfo() { + super(DefaultOutputHandlers.STDOUT_LIST_PRINTER()); + } + + @Override + public Integer call() throws Exception { + if (strategy != null) { + switch (strategy) { + case "event-appointment": + getOutputHandler().accept(List.of( + "Strategy: event-appointment", + "Description: Creates a basic event appointment.", + "Parameters: None" + )); + break; + case "todo-basic": + getOutputHandler().accept(List.of( + "Strategy: todo-basic", + "Description: Creates a basic to-do item.", + "Parameters: None" + )); + break; + default: + getOutputHandler().accept(List.of( + "Available strategies:", + " - event-appointment: Creates a basic event appointment.", + " - todo-basic: Creates a basic to-do item." + )); + break; + } + } + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/AbstractWorkspaceCommand.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/AbstractWorkspaceCommand.java index ec8c057..ab3d75d 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/AbstractWorkspaceCommand.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/AbstractWorkspaceCommand.java @@ -34,6 +34,16 @@ public void setStore(ObjectStore store) { this.store = store; } + public AbstractWorkspaceCommand withStore(ObjectStore store) { + this.store = store; + return this; + } + + public AbstractWorkspaceCommand withWorkspace(String workspace) { + this.workspace = workspace; + return this; + } + public String getWorkspace() { return workspace; } diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ActivateWorkspace.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ActivateWorkspace.java new file mode 100644 index 0000000..04fb70a --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ActivateWorkspace.java @@ -0,0 +1,50 @@ +package org.ical4j.command.workspace; + +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.config.CommandConfig; +import picocli.CommandLine; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "activate", aliases = {"chws"}, description = "Activate a workspace") +public class ActivateWorkspace extends AbstractCommand { + + @CommandLine.Parameters(index = "0", description = "The id of the workspace to activate") + private String workspaceId; + + @Override + public Integer call() throws Exception { + String previous = CommandConfig.INSTANCE.setActiveWorkspace(workspaceId); + getOutputHandler().accept("Workspace '" + workspaceId + "' activated. (was '" + previous + "')"); + return 0; + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/CreateCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/CreateCollection.java deleted file mode 100644 index 6d2731a..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/CreateCollection.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.ical4j.command.workspace; - -import org.ical4j.connector.ObjectCollection; -import org.ical4j.connector.ObjectStore; -import org.ical4j.connector.ObjectStoreException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; - -import java.util.function.Consumer; - -/** - * A command to create a new collection for a specified object store. - */ -@CommandLine.Command(name = "create-collection", description = "Create a new collection") -public class CreateCollection extends AbstractWorkspaceCommand, ObjectCollection> { - - private static final Logger LOGGER = LoggerFactory.getLogger(CreateCollection.class); - - @CommandLine.Option(names = {"-name"}) - private String collectionName; - - private String[] supportedComponents; - - public CreateCollection() { - } - - public CreateCollection(Consumer> consumer) { - super(consumer); - } - - public CreateCollection(Consumer> consumer, ObjectStore> store) { - super(consumer); - setStore(store); - } - - public CreateCollection withCollectionName(String collectionName) { - this.collectionName = collectionName; - return this; - } - - public CreateCollection withSupportedComponents(String... supportedComponents) { - this.supportedComponents = supportedComponents; - return this; - } - - @Override - public Integer call() { - try { - getOutputHandler().accept(getStore().addCollection(collectionName)); - } catch (ObjectStoreException e) { - LOGGER.error("Unexpected error", e); - return 1; - } - return 0; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/DeleteCollection.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/DeleteCollection.java deleted file mode 100644 index f50d700..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/DeleteCollection.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.ical4j.command.workspace; - -import org.ical4j.connector.ObjectCollection; -import org.ical4j.connector.ObjectNotFoundException; -import org.ical4j.connector.ObjectStore; -import org.ical4j.connector.ObjectStoreException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; - -import java.util.function.Consumer; - -/** - * A command to delete an existing collection from an object store. - */ -@CommandLine.Command(name = "delete-collection", description = "Purge a collection") -public class DeleteCollection extends AbstractWorkspaceCommand, ObjectCollection> { - - private static final Logger LOGGER = LoggerFactory.getLogger(DeleteCollection.class); - - @CommandLine.Option(names = {"-name"}) - private String collectionName; - - public DeleteCollection() { - } - - public DeleteCollection(Consumer> consumer) { - super(consumer); - } - - public DeleteCollection(Consumer> consumer, ObjectStore> store) { - super(consumer); - setStore(store); - } - - public DeleteCollection withCollectionName(String collectionName) { - this.collectionName = collectionName; - return this; - } - - @Override - public Integer call() { - try { - ObjectCollection collection = getStore().getCollection(collectionName); - collection.delete(); - getOutputHandler().accept(collection); - } catch (ObjectStoreException | ObjectNotFoundException e) { - LOGGER.error("Unexpected error", e); - return 1; - } - return 0; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListCollections.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListCollections.java deleted file mode 100644 index f85a733..0000000 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListCollections.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.ical4j.command.workspace; - -import org.ical4j.command.config.StoreConfiguration; -import org.ical4j.connector.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; - -import java.util.List; -import java.util.function.Consumer; - -@CommandLine.Command(name = "list-collections", description = "List collections in an object store") -public class ListCollections extends AbstractWorkspaceCommand, List>> { - - private static final Logger LOGGER = LoggerFactory.getLogger(ListCollections.class); - - public ListCollections() { - } - - public ListCollections(Consumer>> consumer) { - this(consumer, new StoreConfiguration().getCalendarStore()); - } - - public ListCollections(Consumer>> consumer, ObjectStore> store) { - super(consumer); -// setStore(store); - } - - @Override - public Integer call() { - try { - getOutputHandler().accept(getStore().getCollections(getWorkspace())); - } catch (ObjectStoreException | ObjectNotFoundException e) { - LOGGER.error("Unexpected error", e); - return 1; - } - return 0; - } -} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListWorkspaces.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListWorkspaces.java new file mode 100644 index 0000000..82f57ae --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/ListWorkspaces.java @@ -0,0 +1,69 @@ +package org.ical4j.command.workspace; + +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.config.CommandConfig; +import picocli.CommandLine; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "list-workspaces", aliases = {"lws"}, description = "List available workspaces") +public class ListWorkspaces extends AbstractCommand> { + + public ListWorkspaces() { + super(DefaultOutputHandlers.STDOUT_LIST_PRINTER()); + } + + @Override + public Integer call() throws Exception { + Set workspaces = new TreeSet<>(); + workspaces.add(CommandConfig.INSTANCE.getActiveWorkspace() + " *"); + workspaces.addAll(format(CommandConfig.INSTANCE.getCalendarStore().listWorkspaceIds())); + workspaces.addAll(format(CommandConfig.INSTANCE.getCardStore().listWorkspaceIds())); + getOutputHandler().accept(workspaces); + return 0; + } + + private List format(List ids) { + return ids.stream().map(id -> { + if (CommandConfig.INSTANCE.getActiveWorkspace().equals(id)) { + return id + " *"; + } else { + return id; + } + }).toList(); + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/PrintWorkspace.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/PrintWorkspace.java new file mode 100644 index 0000000..182bcbc --- /dev/null +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/PrintWorkspace.java @@ -0,0 +1,80 @@ +package org.ical4j.command.workspace; + +import org.ical4j.command.AbstractCommand; +import org.ical4j.command.DefaultOutputHandlers; +import org.ical4j.command.config.CommandConfig; +import org.ical4j.connector.CalendarCollection; +import org.ical4j.connector.CardCollection; +import org.ical4j.connector.ObjectCollection; +import picocli.CommandLine; + +import java.util.ArrayList; +import java.util.List; + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +@CommandLine.Command(name = "print-workspace", aliases = {"pws"}, description = "Show information about the selected workspace") +public class PrintWorkspace extends AbstractCommand> { + + @CommandLine.Parameters(index = "0", description = "Workspace ID", arity = "0..1") + private List workspaceId; + + public PrintWorkspace() { + super(DefaultOutputHandlers.STDOUT_LIST_PRINTER()); + } + + @Override + public Integer call() throws Exception { + if (workspaceId == null || workspaceId.isEmpty()) { + workspaceId = List.of(CommandConfig.INSTANCE.getActiveWorkspace()); + } + List calendarCollections = CommandConfig.INSTANCE.getCalendarStore() + .getCollections(workspaceId.get(0)); + List cardCollections = CommandConfig.INSTANCE.getCardStore() + .getCollections(workspaceId.get(0)); + List b = new ArrayList<>(); + b.add("Calendar Collections:\n"); + b.addAll(calendarCollections.stream().map(this::printCollection).toList()); + b.add("\nCard Collections:\n"); + b.addAll(cardCollections.stream().map(this::printCollection).toList()); + getOutputHandler().accept(b); + return 0; + } + + private String printCollection(ObjectCollection collection) { + StringBuilder b = new StringBuilder(); + b.append("Collection: ").append(collection.getDisplayName()).append(" | "); +// b.append("URI: ").append(collection.getUri()).append("\n"); + b.append("Size: ").append(collection.listObjectUIDs().size()).append(" | "); + return b.toString(); + } +} diff --git a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/WorkspaceCommandGroup.java b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/WorkspaceCommandGroup.java index 3f581aa..0a15b5b 100644 --- a/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/WorkspaceCommandGroup.java +++ b/ical4j-command-cli/src/main/java/org/ical4j/command/workspace/WorkspaceCommandGroup.java @@ -1,12 +1,9 @@ package org.ical4j.command.workspace; -import org.ical4j.command.collection.ListObjectUids; import picocli.CommandLine; -@CommandLine.Command(name = "workspace", description = "Calendar and card workspace operations", - subcommands = {GetCollectionDetails.class, ListCollections.class, - CreateCollection.class, UpdateCollection.class, - DeleteCollection.class, ListObjectUids.class,}) +@CommandLine.Command(name = "workspace", description = "Manage collection workspaces", + subcommands = {ListWorkspaces.class, ActivateWorkspace.class, PrintWorkspace.class}) public class WorkspaceCommandGroup { } diff --git a/ical4j-command-cli/src/main/resources/config.default.toml b/ical4j-command-cli/src/main/resources/config.default.toml new file mode 100644 index 0000000..534b8e7 --- /dev/null +++ b/ical4j-command-cli/src/main/resources/config.default.toml @@ -0,0 +1,10 @@ +[user] + agent = "ical4j-command-tool/1.0" + +[workpace] + active = "default" + +[contentType] + default = "text/plain" + +[filters] \ No newline at end of file diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/ListCommandTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/ListCommandTest.groovy new file mode 100644 index 0000000..fb8364c --- /dev/null +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/ListCommandTest.groovy @@ -0,0 +1,73 @@ +package org.ical4j.command + +import org.ical4j.command.config.CommandConfig +import org.ical4j.connector.CalendarCollection +import org.ical4j.connector.CardCollection +import org.ical4j.connector.ObjectStore +import spock.lang.Specification + +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +class ListCommandTest extends Specification { + + def 'test list command lists collection names from the active workspace'() { + given: 'mock calendar and card stores' + ObjectStore calendarStore = Mock() + ObjectStore cardStore = Mock() + CalendarCollection calendarCollection = Mock() + CardCollection cardCollection = Mock() + + calendarStore.getCollections('default') >> [calendarCollection] + cardStore.getCollections('default') >> [cardCollection] + calendarCollection.getDisplayName() >> 'My Calendar' + cardCollection.getDisplayName() >> 'My Contacts' + + and: 'a config exposing those stores for the active workspace' + CommandConfig config = new CommandConfig(null) { + @Override String getActiveWorkspace() { 'default' } + @Override ObjectStore getCalendarStore() { calendarStore } + @Override ObjectStore getCardStore() { cardStore } + } + + and: 'a list command capturing its output' + def output + def command = new ListCommand((o) -> output = o, config) + + when: 'the command is executed' + def result = command.call() + + then: 'it succeeds and reports both collection names' + result == 0 + output as Set == ['My Calendar', 'My Contacts'] as Set + } +} diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/CreateCalendarTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/CreateCalendarTest.groovy index 9dcac63..04148a1 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/CreateCalendarTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/CreateCalendarTest.groovy @@ -1,7 +1,7 @@ package org.ical4j.command.calendar import net.fortuna.ical4j.model.Calendar -import org.ical4j.command.collection.CreateCalendar +import org.ical4j.command.strategy.CreateCalendar import org.ical4j.connector.CalendarCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/DeleteCalendarTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/DeleteCalendarTest.groovy index e8098ae..a82a4e4 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/DeleteCalendarTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/DeleteCalendarTest.groovy @@ -1,6 +1,6 @@ package org.ical4j.command.calendar -import org.ical4j.command.collection.DeleteCalendar + import org.ical4j.connector.CalendarCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/GetCalendarTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/GetCalendarTest.groovy index 39eab39..28b514a 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/GetCalendarTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/GetCalendarTest.groovy @@ -1,6 +1,6 @@ package org.ical4j.command.calendar -import org.ical4j.command.collection.GetCalendar + import org.ical4j.connector.CalendarCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/UpdateCalendarTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/UpdateCalendarTest.groovy index 67dbe71..b1c6622 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/UpdateCalendarTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/calendar/UpdateCalendarTest.groovy @@ -1,7 +1,6 @@ package org.ical4j.command.calendar import net.fortuna.ical4j.model.Calendar -import org.ical4j.command.collection.UpdateCalendar import org.ical4j.connector.CalendarCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/CreateCardTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/CreateCardTest.groovy similarity index 88% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/CreateCardTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/card/CreateCardTest.groovy index 730ba4b..60c3d5f 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/CreateCardTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/CreateCardTest.groovy @@ -1,8 +1,8 @@ -package org.ical4j.command.vcard +package org.ical4j.command.card import net.fortuna.ical4j.vcard.VCard -import org.ical4j.command.collection.CreateCard +import org.ical4j.command.strategy.CreateCard import org.ical4j.connector.CardCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/DeleteCardTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/DeleteCardTest.groovy similarity index 88% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/DeleteCardTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/card/DeleteCardTest.groovy index 76349bb..a07df30 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/DeleteCardTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/DeleteCardTest.groovy @@ -1,6 +1,6 @@ -package org.ical4j.command.vcard +package org.ical4j.command.card + -import org.ical4j.command.collection.DeleteCard import org.ical4j.connector.CardCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/GetCardTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/GetCardTest.groovy similarity index 88% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/GetCardTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/card/GetCardTest.groovy index 2934c6c..a276808 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/GetCardTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/GetCardTest.groovy @@ -1,6 +1,6 @@ -package org.ical4j.command.vcard +package org.ical4j.command.card + -import org.ical4j.command.collection.GetCard import org.ical4j.connector.CardCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/UpdateCardTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/UpdateCardTest.groovy similarity index 89% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/UpdateCardTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/card/UpdateCardTest.groovy index 6f509b3..d5e8b47 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/vcard/UpdateCardTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/card/UpdateCardTest.groovy @@ -1,8 +1,7 @@ -package org.ical4j.command.vcard +package org.ical4j.command.card import net.fortuna.ical4j.vcard.VCard -import org.ical4j.command.collection.UpdateCard import org.ical4j.connector.CardCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/CreateCollectionTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/AddCollectionTest.groovy similarity index 65% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/CreateCollectionTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/AddCollectionTest.groovy index 12b4bad..7912942 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/CreateCollectionTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/AddCollectionTest.groovy @@ -1,17 +1,17 @@ package org.ical4j.command.collection -import org.ical4j.command.workspace.CreateCollection + import org.ical4j.connector.ObjectStore import spock.lang.Specification -class CreateCollectionTest extends Specification { +class AddCollectionTest extends Specification { def 'test create collection'() { given: 'a mock card store' ObjectStore store = Mock() when: 'a create collection command is run' - new CreateCollection((collection) -> {}, store).withCollectionName('testCollection').call() + new AddCollection((collection) -> {}, store).withCollectionName('testCollection').call() then: 'store add collection is invoked' 1 * store.addCollection('testCollection') diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/GetCollectionDetailsTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/GetCollectionDetailsTest.groovy index 74d8d3e..d770feb 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/GetCollectionDetailsTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/GetCollectionDetailsTest.groovy @@ -1,6 +1,6 @@ package org.ical4j.command.collection -import org.ical4j.command.workspace.GetCollectionDetails + import org.ical4j.connector.ObjectStore import spock.lang.Specification diff --git a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/DeleteCollectionTest.groovy b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/RemoveCollectionTest.groovy similarity index 76% rename from ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/DeleteCollectionTest.groovy rename to ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/RemoveCollectionTest.groovy index dc4bced..96da3c8 100644 --- a/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/DeleteCollectionTest.groovy +++ b/ical4j-command-cli/src/test/groovy/org/ical4j/command/collection/RemoveCollectionTest.groovy @@ -1,11 +1,11 @@ package org.ical4j.command.collection -import org.ical4j.command.workspace.DeleteCollection + import org.ical4j.connector.ObjectCollection import org.ical4j.connector.ObjectStore import spock.lang.Specification -class DeleteCollectionTest extends Specification { +class RemoveCollectionTest extends Specification { def 'test delete collection'() { given: 'a mock card store' @@ -13,7 +13,7 @@ class DeleteCollectionTest extends Specification { ObjectCollection collection = Mock() when: 'a delete collection command is run' - new DeleteCollection((c) -> {}, store).withCollectionName('testCollection').call() + new RemoveCollection((c) -> {}, store).withCollectionName('testCollection').call() then: 'store remove collection is invoked' 1 * store.getCollection('testCollection') >> collection diff --git a/ical4j-command-core/build.gradle b/ical4j-command-core/build.gradle new file mode 100644 index 0000000..3330fa7 --- /dev/null +++ b/ical4j-command-core/build.gradle @@ -0,0 +1,12 @@ +// Shared command vocabulary module: vocabulary definitions only, no dispatch/execution. +// Plugins and common dependencies are applied by the root `configure(subprojects)` block. + +jar { + manifest { + attributes( + 'Implementation-Title': 'iCal4j Command Core', + 'Implementation-Version': version, + 'Implementation-Vendor': 'Ben Fortuna' + ) + } +} diff --git a/ical4j-command-core/src/main/java/module-info.java b/ical4j-command-core/src/main/java/module-info.java new file mode 100644 index 0000000..37d5462 --- /dev/null +++ b/ical4j-command-core/src/main/java/module-info.java @@ -0,0 +1,10 @@ +/** + * Shared command vocabulary (verbs, entity types, loci, serialization families). This + * module contains vocabulary definitions only - no command dispatch or execution logic - + * preserving the "shared names, separate dispatch" model. + */ +module ical4j.command.core { + requires java.base; + + exports org.ical4j.command.core; +} diff --git a/ical4j-command-core/src/main/java/org/ical4j/command/core/EntityType.java b/ical4j-command-core/src/main/java/org/ical4j/command/core/EntityType.java new file mode 100644 index 0000000..fb4cae9 --- /dev/null +++ b/ical4j-command-core/src/main/java/org/ical4j/command/core/EntityType.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.core; + +/** + * The canonical entity types that operations act on. Every type lives directly within a + * {@link Locus#COLLECTION}. Note that {@code calendar} and {@code card} are NOT entity + * types - they are {@link SerializationFamily serialization families}. + */ +public enum EntityType { + + EVENT, + ACTION, + ISSUE, + NOTE, + OBSERVANCE, + AVAILABILITY, + REPORT, + + /** A contact (vCard {@code Entity}). */ + ENTITY, + + RESOURCE +} diff --git a/ical4j-command-core/src/main/java/org/ical4j/command/core/Locus.java b/ical4j-command-core/src/main/java/org/ical4j/command/core/Locus.java new file mode 100644 index 0000000..9c070cc --- /dev/null +++ b/ical4j-command-core/src/main/java/org/ical4j/command/core/Locus.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.core; + +/** + * The containment hierarchy an operation acts within. The hierarchy is exactly two levels + * deep: a {@link #WORKSPACE} contains {@link #COLLECTION}s, and a collection directly + * contains entities. There is no container level between a collection and its entities. + */ +public enum Locus { + + /** Top-level container of collections. */ + WORKSPACE, + + /** Container of entities; entities live directly here. */ + COLLECTION +} diff --git a/ical4j-command-core/src/main/java/org/ical4j/command/core/SendMethod.java b/ical4j-command-core/src/main/java/org/ical4j/command/core/SendMethod.java new file mode 100644 index 0000000..8f70236 --- /dev/null +++ b/ical4j-command-core/src/main/java/org/ical4j/command/core/SendMethod.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.core; + +/** + * iTIP (RFC 5546) methods that specialise a {@link Verb#SEND} operation. These are carried + * alongside {@code Verb.SEND} rather than being independent verbs: publish/request/reply + * differ by the method on the transmitted message, not by the action performed. + */ +public enum SendMethod { + + /** Post calendar data without an expectation of a response. */ + PUBLISH, + + /** Solicit a response (e.g. an invitation to attendees). */ + REQUEST, + + /** Respond to a prior request (e.g. an attendee's reply). */ + REPLY +} diff --git a/ical4j-command-core/src/main/java/org/ical4j/command/core/SerializationFamily.java b/ical4j-command-core/src/main/java/org/ical4j/command/core/SerializationFamily.java new file mode 100644 index 0000000..d7e1c4d --- /dev/null +++ b/ical4j-command-core/src/main/java/org/ical4j/command/core/SerializationFamily.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.core; + +/** + * The serialization / object families that data is imported from and exported to. These + * are NOT loci and NOT entity types - they are the representation that an operation reads + * or writes via {@link Verb#IMPORT} / {@link Verb#EXPORT} when crossing a collection + * boundary. + */ +public enum SerializationFamily { + + /** iCalendar (RFC 5545) representation. */ + CALENDAR, + + /** vCard (RFC 6350) representation. */ + CARD +} diff --git a/ical4j-command-core/src/main/java/org/ical4j/command/core/Verb.java b/ical4j-command-core/src/main/java/org/ical4j/command/core/Verb.java new file mode 100644 index 0000000..c635326 --- /dev/null +++ b/ical4j-command-core/src/main/java/org/ical4j/command/core/Verb.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.core; + +/** + * The canonical set of action verbs shared by all command surfaces (CLI, MCP, agent). + *

+ * Surfaces MUST name operations from this set and MUST NOT introduce synonyms for an + * existing verb. Notably {@code create}/{@code add} are standardised on {@link #ADD} and + * {@code delete}/{@code remove} on {@link #REMOVE}; the iTIP methods publish/request/reply + * are specialised forms of {@link #SEND} (see {@link SendMethod}), not independent verbs. + */ +public enum Verb { + + /** Return many items of a type within a locus. */ + LIST, + + /** Return one item of a type, addressed by identifier. */ + GET, + + /** Synthesise a new item from a strategy/template (e.g. an event from a meeting template). */ + GENERATE, + + /** Place an item into a locus, whether newly made or pre-existing. */ + ADD, + + /** Modify an existing item in place. */ + UPDATE, + + /** Take an item out of a locus. */ + REMOVE, + + /** Duplicate an item into a (possibly different) locus. */ + COPY, + + /** Relocate an item from one locus to another. */ + MOVE, + + /** Derive a subset of a type within a locus by expression. */ + FILTER, + + /** Check an item or locus for conformance. */ + VALIDATE, + + /** Read a serialization family into a locus. */ + IMPORT, + + /** Write a serialization family out of a locus. */ + EXPORT, + + /** Transmit items over a channel (see {@link SendMethod} for iTIP specialisations). */ + SEND, + + /** Accept incoming items over a channel. */ + RECEIVE +} diff --git a/ical4j-command-mcp/build.gradle b/ical4j-command-mcp/build.gradle new file mode 100644 index 0000000..92dde41 --- /dev/null +++ b/ical4j-command-mcp/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'org.springframework.boot' version '3.5.7' +} + +apply plugin: 'application' + +dependencies { + api project(':ical4j-command-core') + api project(':ical4j-command-cli') + + implementation libs.spring.ai, libs.spring.boot.web +} + +application { + mainClass = 'org.ical4j.command.mcp.iCal4jMcpServerMain' + applicationName = 'ical4j-command-mcp-server' +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CalendarService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CalendarService.java new file mode 100644 index 0000000..6e6ce05 --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CalendarService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.stereotype.Service; + +@Service +public class CalendarService { +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/ChannelService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/ChannelService.java new file mode 100644 index 0000000..d1de5b1 --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/ChannelService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.stereotype.Service; + +@Service +public class ChannelService { +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CollectionService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CollectionService.java new file mode 100644 index 0000000..69b4e24 --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/CollectionService.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class CollectionService { + + @Tool(description = "List all actions in the collection.") + public String listActions(String collectionName) { + return "List of actions"; + } + + @Tool(description = "List all availability in the collection.") + public String listAvailability(String collectionName) { + return "List of availability"; + } + + @Tool(description = "List all entities in the collection.") + public String listEntities(String collectionName) { + return "List of entities"; + } + + @Tool(description = "List all events in the collection.") + public String listEvents(String collectionName) { + return "List of events"; + } + + @Tool(description = "List all issues in the collection.") + public String listIssues(String collectionName) { + return "List of issues"; + } + + @Tool(description = "List all notes in the collection.") + public String listNotes(String collectionName) { + return "List of notes"; + } + + @Tool(description = "List all observances in the collection.") + public String listObservances(String collectionName) { + return "List of observances"; + } + + @Tool(description = "List all reports in the collection.") + public String listReports(String collectionName) { + return "List of reports"; + } + + @Tool(description = "List all resources in the collection.") + public String listResources(String collectionName) { + return "List of resources"; + } + + public String importCalendar(String name) { + return "Imported calendar: " + name; + } + + public String importCard(String name) { + return "Imported card: " + name; + } + + public String removeCalendar(String name) { + return "Removed calendar: " + name; + } + public String removeCard(String name) { + return "Removed card: " + name; + } + + public String updateCalendar(String name) { + return "Updated calendar: " + name; + } + + public String updateCard(String name) { + return "Updated card: " + name; + } + + public String getCalendar(String name) { + return "Info for calendar: " + name; + } + + public String getCard(String name) { + return "Info for card: " + name; + } +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/EntityService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/EntityService.java new file mode 100644 index 0000000..e051b18 --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/EntityService.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import net.fortuna.ical4j.vcard.Entity; +import net.fortuna.ical4j.vcard.property.Fn; +import net.fortuna.ical4j.vcard.property.immutable.ImmutableKind; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class EntityService { + + @Tool(description = "Creates a new vCard individual entity.") + public String newIndividual(String name) { + return new Entity().add(ImmutableKind.INDIVIDUAL).add(new Fn(name)).toString(); + } +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/StoreService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/StoreService.java new file mode 100644 index 0000000..7d5c59d --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/StoreService.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class StoreService { + + @Tool(description = "List all workspaces in the store") + public String listWorkspaces() { + // Placeholder implementation + return "List of workspaces"; + } + + @Tool(description = "Add a workspace to the store") + public String addWorkspace(String id) { + // Placeholder implementation + return "Workspace added"; + } + + @Tool(description = "Remove a workspace from the store") + public String removeWorkspace(String id) { + // Placeholder implementation + return "Workspace removed"; + } +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/WorkspaceService.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/WorkspaceService.java new file mode 100644 index 0000000..1df18c4 --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/WorkspaceService.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class WorkspaceService { + + @Tool(description = "List all collections in the workspace") + public String listCollections(String workspace) { + // Placeholder implementation + return "List of collections"; + } + + @Tool(description = "Add a collection to the workspace") + public String addCollection(String name, String workspace) { + return "Added collection: " + name; + } + + @Tool(description = "Remove a collection from the workspace") + public String removeCollection(String name, String workspace) { + return "Removed collection: " + name; + } + + @Tool(description = "Update a collection in the workspace") + public String updateCollection(String name, String workspace) { + return "Updated collection: " + name; + } + + @Tool(description = "Get information about a collection in the workspace") + public String getCollection(String name, String workspace) { + return "Info for collection: " + name; + } +} diff --git a/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/iCal4jMcpServerMain.java b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/iCal4jMcpServerMain.java new file mode 100644 index 0000000..b2fa9bb --- /dev/null +++ b/ical4j-command-mcp/src/main/java/org/ical4j/command/mcp/iCal4jMcpServerMain.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.ical4j.command.mcp; + +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class iCal4jMcpServerMain { + + public static void main(String[] args) { + SpringApplication.run(iCal4jMcpServerMain.class, args); + } + + @Bean + public ToolCallbackProvider ical4jTools(WorkspaceService workspaceService, CollectionService collectionService) { + return MethodToolCallbackProvider.builder() + .toolObjects(workspaceService, collectionService).build(); + } +} diff --git a/ical4j-command-mcp/src/main/resources/application.properties b/ical4j-command-mcp/src/main/resources/application.properties new file mode 100644 index 0000000..1e35a97 --- /dev/null +++ b/ical4j-command-mcp/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.main.web-application-type=none + +# NOTE: You must disable the banner and the console logging +# to allow the STDIO transport to work !!! +spring.main.banner-mode=off +logging.pattern.console= + +spring.ai.mcp.server.name=ical4j-command-server +spring.ai.mcp.server.version=0.0.1 + +logging.file.name=./model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server.log diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/.openspec.yaml b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/.openspec.yaml new file mode 100644 index 0000000..b4c82a0 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-06 diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/README.md b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/README.md new file mode 100644 index 0000000..f64d489 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/README.md @@ -0,0 +1,3 @@ +# unify-command-vocabulary + +Define a shared controlled vocabulary (verbs, types, loci) that CLI, MCP, and agent surfaces conform to (Option B: shared names, separate dispatch). diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/design.md b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/design.md new file mode 100644 index 0000000..4cef827 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/design.md @@ -0,0 +1,60 @@ +# Design — unify-command-vocabulary + +## Context + +The same operations appear across three surfaces, each organized around a different primary axis, which causes naming drift: + +| Surface | Organizes around | Result | +|---------|-------------------|--------| +| CLI (old) | type (noun groups) | `calendar delete`, `strategy create-event` | +| CLI (new) | verb (top-level) | `copy`, `move`, `delete` — type becomes an arg | +| MCP | verb×type flat matrix | `getEvents`, `addCalendar`, `createCollection` | +| Agent | task (fuzzy) | `generateMeeting`, `publish/request/reply` | + +Drift is already observable inside a single file (`CollectionService`): `getActions` is named `get` but returns a list; `createCollection` vs `addCalendar`; `deleteCollection` vs `removeCalendar`. + +## Decisions + +### D1 — Option B: shared vocabulary, separate dispatch +Surfaces agree on **names and taxonomy**, not on a shared execution core. + +- **Why:** Less invasive to the existing picocli tree and the Spring-AI MCP services; no need to find one abstraction that fits picocli *and* MCP *and* `@Tool` dispatch. +- **Trade-off accepted:** conformance is by convention/naming rule, with drift risk, rather than enforced by a single dispatcher. +- **Consequence:** the commented `new ListCommand(...)` reach-through in `WorkspaceService.java:48` (MCP → CLI) stays removed. That coupling was an Option-A artifact and is out of scope. + +### D2 — Locus is a real third axis, two levels deep +Operation identity is `(verb, type, locus)`. Locus is a containment hierarchy: `workspace ⊃ collection`. Entities live **directly in a collection**. + +- **Why:** `copy event between collections` only makes sense if locus is orthogonal to type. Keeping it to two levels caps model complexity. +- **Rejected alternative:** `collection ⊃ calendar ⊃ event` (a three-level hierarchy where calendar is a container). Rejected per D3. + +### D3 — calendar / card are serialization families, not loci or types +`calendar` (iCalendar) and `card` (vCard) are the import/export representations, not containers and not entity types. + +- **Why:** A collection groups events, notes, and contacts together (per README use cases). Events do not live "inside a calendar inside a collection" — the calendar is just how that collection's temporal entities serialize. Treating calendar/card as a locus level would have forced a third hierarchy level (D2) and split entities by format rather than by collection. +- **Consequence:** moving an `.ics`/`.vcf` in or out of a collection uses `import`/`export`, not `create`/`add` of a "calendar" container. + +### D4 — Verb canonicalization: standardise on add/remove +`list` (many) vs `get` (one). The `create`/`add` pair collapses to **`add`** (place into a locus, new or existing); the `delete`/`remove` pair collapses to **`remove`** (take out of a locus). Synonyms (`create`, `delete`, `fetch`, `destroy`, …) map to canonical verbs and are not exposed. + +- **Why:** the create/add and delete/remove distinctions added vocabulary without earning their keep across all three surfaces, and `add`/`remove` already match the CLI's existing `AddCollection` / `RemoveCollection` commands. + +### D5 — The vocabulary lives in a new shared core module +The canonical verbs/types/loci become constants/enums in a **new core module** that CLI, MCP, and agent all depend on. + +- **Why:** a docs-only convention drifts; a shared compile-time definition makes conformance cheap to follow and visible to refactoring tools. +- **Boundary:** the core module holds *vocabulary only* — no dispatch or execution. This keeps Option B intact (shared names, separate dispatch) rather than sliding into Option A. + +### D6 — iTIP methods are specialised sends +`publish` / `request` / `reply` are the same axis as `send` — a `send` carrying a specific iTIP method — not independent verbs. + +- **Why:** they differ by the iTIP method on the message, not by the action; collapsing them keeps the verb axis small and reflects that `CalendarUserAgent` is really emitting calendar messages over a channel. + +### D7 — A `generate` verb for strategy/template synthesis (revealed during apply) +Implementation surfaced a command family the vocabulary did not cover: the CLI's strategy generators (`CreateEvent`, `CreateAction`, …, `CreateCalendar`, `CreateCard`) *synthesise* objects from templates rather than placing existing items into a locus. `add` ("place into a locus") does not fit, and the `add` name is already owned by the metadata command group. + +- **Decision:** add `generate` to the canonical verb set. The strategy group is named `generate` (alias `new`); the metadata group keeps `add`. Generation is locus-independent; placing a generated item into a collection is a separate `add`. + +## Open questions + +- (none currently — D1–D7 resolve the forks surfaced during exploration and apply) diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/proposal.md b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/proposal.md new file mode 100644 index 0000000..c7e23c3 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/proposal.md @@ -0,0 +1,31 @@ +## Why + +The same operations are described three times, in three idioms, across the CLI, MCP, and agent surfaces — and the names already drift even within a single file (e.g. `getEvents` is named `get` but *lists*; `createCollection` vs `addCalendar`; `delete` vs `remove`). Without a shared, controlled vocabulary, every new surface re-invents names and the three surfaces diverge further over time. + +## What Changes + +- Introduce a **controlled vocabulary** of canonical **verbs**, **entity types**, and **loci** (containers) that every surface — CLI, MCP, and agent — names operations from. +- Adopt **Option B**: surfaces share the *vocabulary* (names + taxonomy) but keep their *own dispatch*. There is no shared command-execution core; conformance is by naming rule, not by a common dispatcher. +- Fix the established naming axes: + - `list` (many) vs `get` (one-by-id) — never `get` for plural results. + - Standardise on `add` / `remove` (collapsing `create`→`add` and `delete`→`remove`); synonyms are not exposed. + - Treat `publish` / `request` / `reply` as specialised `send` operations (iTIP methods), not independent verbs. +- Define the **locus hierarchy** as exactly two levels: `workspace ⊃ collection`. Entities (event, action, issue, note, observance, availability, report, entity/contact, resource) live **directly in a collection**. +- Classify `calendar` and `card` as **serialization/object families** (iCalendar / vCard), not loci and not entity types. +- Introduce a new **shared core module** that holds the vocabulary (constants/enums) and is depended on by CLI, MCP, and agent — vocabulary only, no dispatch. + +## Capabilities + +### New Capabilities +- `command-vocabulary`: The canonical verbs, entity types, and loci, plus the naming rules each surface (CLI, MCP, agent) must conform to. + +### Modified Capabilities + + +## Impact + +- **New core module**: a vocabulary-only module (constants/enums) added to `settings.gradle`, depended on by the three surfaces. +- **CLI** (`ical4j-command-cli`): the in-flight verb-first top-level commands (`copy`, `move`, …) and noun-grouped commands must align to canonical names; `delete` aligns to `remove`. +- **MCP** (`ical4j-command-mcp`): `@Tool` method names (e.g. `getEvents` → list semantics, `addCalendar`/`removeCalendar`) must align. +- **Agent** (`ical4j-command-agent`): `@Tool` names (`generateEvent`, `generateMeeting`) map to canonical verb/type. +- No runtime coupling introduced; the commented `new ListCommand(...)` reach-through in `WorkspaceService` stays removed under Option B. diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/specs/command-vocabulary/spec.md b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/specs/command-vocabulary/spec.md new file mode 100644 index 0000000..871f302 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/specs/command-vocabulary/spec.md @@ -0,0 +1,112 @@ +## ADDED Requirements + +### Requirement: Operation identity is (verb, type, locus) +Every operation exposed by any surface (CLI, MCP, agent) SHALL be identified by a tuple of exactly three axes: a **verb** (the action), an **entity type** (what is acted on), and a **locus** (the container the action acts within). Surface-specific naming SHALL be derivable from this tuple. + +#### Scenario: Decomposing an existing operation +- **WHEN** the MCP method `listCollections(workspace)` is mapped to the vocabulary +- **THEN** it resolves to verb `list`, type `collection`, locus `workspace` + +#### Scenario: Two surfaces name the same operation +- **WHEN** the CLI and the agent both expose "list the events in a collection" +- **THEN** both resolve to the same tuple (`list`, `event`, `collection`) even though their surface syntax differs + +### Requirement: Canonical verb vocabulary +The system SHALL define a closed set of canonical verbs. Surfaces MUST use these verbs and MUST NOT introduce synonyms for an existing verb. The verbs and their meanings are: + +- `list` — return many items of a type within a locus. +- `get` — return one item of a type, addressed by identifier. +- `generate` — synthesise a new item from a strategy/template (e.g. an event from a meeting template), independent of any locus. +- `add` — place an item into a locus, whether newly made or pre-existing. +- `update` — modify an existing item in place. +- `remove` — take an item out of a locus. +- `copy` — duplicate an item into a (possibly different) locus. +- `move` — relocate an item from one locus to another. +- `filter` — derive a subset of a type within a locus by expression. +- `validate` — check an item or locus for conformance. +- `import` / `export` — read/write a serialization family (calendar/card) into/out of a locus. +- `send` / `receive` — transmit/accept items over a channel. + +#### Scenario: Plural results never use `get` +- **WHEN** an operation returns more than one item +- **THEN** its verb is `list` (or `filter`), never `get` + +#### Scenario: Add covers both new and existing items +- **WHEN** an operation places an item into a locus, whether it is brand-new or already exists +- **THEN** its verb is `add` — there is no separate `create` verb + +#### Scenario: Generate is distinct from add +- **WHEN** an operation synthesises an item from a strategy/template without placing it into a locus (e.g. emitting a generated event to output) +- **THEN** its verb is `generate`, not `add` +- **WHEN** the generated item is subsequently placed into a locus +- **THEN** that placement is a separate `add` operation + +#### Scenario: Remove covers both detach and destroy +- **WHEN** an operation takes an item out of a locus, whether the item continues to exist elsewhere or not +- **THEN** its verb is `remove` — there is no separate `delete` verb + +#### Scenario: Rejecting a synonym +- **WHEN** a surface proposes an operation named with a verb synonym (e.g. `fetch`, `make`, `create`, `delete`, `destroy`) +- **THEN** it is mapped to the canonical verb (`get`, `add`, `remove`) and the synonym is not exposed + +### Requirement: Canonical entity types +The system SHALL define the set of entity types that operations act on: `event`, `action`, `issue`, `note`, `observance`, `availability`, `report`, `entity` (contact), and `resource`. Surfaces MUST name types from this set. + +#### Scenario: Naming an operation by type +- **WHEN** an operation lists actions in a collection +- **THEN** the type axis is `action` and the operation resolves to (`list`, `action`, `collection`) + +### Requirement: Locus hierarchy has exactly two levels +The system SHALL define loci as a containment hierarchy of exactly two levels: a `workspace` contains `collection`s, and a `collection` directly contains entities. No additional container level exists between a collection and its entities. + +#### Scenario: Entities live directly in a collection +- **WHEN** an event is created within a collection +- **THEN** its locus is `collection` directly, with no intervening container + +#### Scenario: Collections live in a workspace +- **WHEN** a collection is created +- **THEN** its locus is `workspace` + +### Requirement: Calendar and card are serialization families, not loci or types +The system SHALL treat `calendar` (iCalendar) and `card` (vCard) as serialization/object families used for import and export, NOT as loci and NOT as entity types. Operations that move data between a serialization family and a collection SHALL use the `import`/`export` verbs. + +#### Scenario: Importing a calendar populates a collection +- **WHEN** a `.ics` calendar is brought into a collection +- **THEN** the operation is `import` with the `calendar` family, targeting locus `collection` — not an `add` of a `calendar` container + +#### Scenario: Calendar is not a container level +- **WHEN** resolving the locus of an event +- **THEN** the locus is `collection`, and `calendar` does not appear as a container in the locus hierarchy + +### Requirement: iTIP methods are specialised send operations +The system SHALL treat the calendar messaging methods `publish`, `request`, and `reply` as specialised forms of the `send` verb (a `send` carrying a specific iTIP method), not as independent verbs on the verb axis. + +#### Scenario: Publish resolves to a specialised send +- **WHEN** the agent's `CalendarUserAgent.publish(...)` is mapped to the vocabulary +- **THEN** it resolves to `send` specialised with the iTIP method `PUBLISH`, on locus `channel` + +#### Scenario: Request and reply share the send axis +- **WHEN** `request` and `reply` are mapped to the vocabulary +- **THEN** both resolve to `send` specialised with their respective iTIP methods, and neither introduces a new top-level verb + +### Requirement: Surfaces share vocabulary but dispatch independently +Each surface (CLI, MCP, agent) SHALL name its operations from the shared vocabulary, but SHALL retain its own dispatch and execution. There SHALL NOT be a shared command-execution core that surfaces delegate into; conformance is enforced by naming rules, not by a common dispatcher. + +#### Scenario: No cross-surface execution coupling +- **WHEN** the MCP layer exposes a `list` operation +- **THEN** it implements its own dispatch and does not call the CLI's command classes to execute the operation + +#### Scenario: Adding a verb to the vocabulary +- **WHEN** a new canonical verb is added to the vocabulary +- **THEN** each surface may adopt it independently using the shared name, with no shared dispatcher required to change + +### Requirement: The vocabulary lives in a shared core module +The canonical verbs, entity types, and loci SHALL be defined as constants/enums in a new shared **core module**. Each surface module (CLI, MCP, agent) SHALL depend on the core module for these names. The core module SHALL contain vocabulary definitions only — it MUST NOT contain command dispatch or execution logic, preserving Option B (shared names, separate dispatch). + +#### Scenario: Surfaces reference core vocabulary constants +- **WHEN** a surface names an operation +- **THEN** it derives the verb, type, and locus names from the core module's definitions rather than declaring its own string literals + +#### Scenario: Core module carries no dispatch +- **WHEN** the core module is inspected +- **THEN** it contains only vocabulary definitions (verbs, types, loci) and no command-execution or dispatch code diff --git a/openspec/changes/archive/2026-06-06-unify-command-vocabulary/tasks.md b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/tasks.md new file mode 100644 index 0000000..db7cb14 --- /dev/null +++ b/openspec/changes/archive/2026-06-06-unify-command-vocabulary/tasks.md @@ -0,0 +1,40 @@ +## 1. Core vocabulary module + +- [x] 1.1 Create new Gradle module (e.g. `ical4j-command-core`) and register it in `settings.gradle` +- [x] 1.2 Define `Verb` enum: `LIST`, `GET`, `ADD`, `UPDATE`, `REMOVE`, `COPY`, `MOVE`, `FILTER`, `VALIDATE`, `IMPORT`, `EXPORT`, `SEND`, `RECEIVE` +- [x] 1.3 Define `EntityType` enum: `EVENT`, `ACTION`, `ISSUE`, `NOTE`, `OBSERVANCE`, `AVAILABILITY`, `REPORT`, `ENTITY`, `RESOURCE` +- [x] 1.4 Define `Locus` enum: `WORKSPACE`, `COLLECTION` (two levels only) +- [x] 1.5 Define `SerializationFamily` enum: `CALENDAR`, `CARD` (not loci, not types) +- [x] 1.6 Model iTIP methods (`PUBLISH`, `REQUEST`, `REPLY`) as a specialisation of `SEND` (e.g. `SendMethod` carried alongside `Verb.SEND`) +- [x] 1.7 Confirm the core module has NO dispatch/execution dependencies (vocabulary only) + +## 2. CLI conformance (`ical4j-command-cli`) + +- [x] 2.1 Add dependency on the core module +- [x] 2.2 Rename/align `delete`-named commands to `remove` (keep `cp`/`rm`-style aliases for usability) +- [x] 2.3 Align `create`/`new`-named commands to `generate` (strategy group `new`→`generate`; `create-calendar`/`create-card`→`calendar`/`card`). Note: `add` reserved for metadata group; added `generate` verb (D7) +- [x] 2.4 Audit `list` vs `get`: no violations — `list-*` return many, `get-*` return one; `collections` is a group namespace, not an operation +- [x] 2.5 Reframe calendar/card CRUD as import/export: `get-calendar`→`export-calendar`, `update-calendar`→`import-calendar` (+ card equivalents); `remove-*` unchanged +- [x] 2.6 Channel commands already use `send`/`receive` (compliant); `publish`/`request`/`reply` are agent-side (see 4.3) + +## 3. MCP conformance (`ical4j-command-mcp`) + +- [x] 3.1 Add dependency on the core module +- [x] 3.2 Rename `getEvents`/`getActions`/… (list semantics) to `list*` (9 methods in CollectionService) +- [x] 3.3 Rename `createCollection`→`addCollection`, `deleteCollection`→`removeCollection` (also `createWorkspace`/`deleteWorkspace`→`add`/`remove` in StoreService for consistency) +- [x] 3.4 Reframe `addCalendar`/`addCard`→`importCalendar`/`importCard`; `removeCalendar`/`removeCard` already use `remove` +- [x] 3.5 Reconcile `getCollectionInfo`/`getCalendarInfo`/`getCardInfo` to canonical `getCollection`/`getCalendar`/`getCard` +- [x] 3.6 Remove the dead `new ListCommand(...)` reach-through in `WorkspaceService` (Option B: no cross-surface dispatch) + +## 4. Agent conformance (`ical4j-command-agent`) + +- [x] 4.1 Add dependency on the core module (required enabling the module in `settings.gradle`) +- [x] 4.2 `@Tool` `generateEvent`/`generateMeeting`/`generateAppointment` map to the `generate` verb (D7); `EntityBuilder.newIndividual`→`generateIndividual`, `addEmail` already uses `add` +- [x] 4.3 `CalendarUserAgent.publish/request/reply` align to specialised `send` semantics (D6) — names already correct, no code change + +## 5. Documentation & verification + +- [x] 5.1 Update `docs/command-protocols.md` to reference the canonical vocabulary as the source of truth (added Canonical Vocabulary section + aligned names) +- [x] 5.2 Update `README.md` command/MCP listings to canonical names (Store commands + MCP operations rewritten as verb×type×locus) +- [x] 5.3 Ran `openspec validate` (PASS); core/CLI/MCP/agent all compile. CLI tests 10/13 pass; 3 failures (`ListCommandTest`, `AddCollectionTest`, `RemoveCollectionTest`) are **pre-existing** in-flight stub breakage, unmodified by this change +- [x] 5.4 Confirmed: all command/tool names match core vocabulary. Note: picocli/Spring annotations require literal name strings at declaration sites (cannot reference enums) — strict literal-elimination would need `static final String` constants in core (follow-up) diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/openspec/specs/command-vocabulary/spec.md b/openspec/specs/command-vocabulary/spec.md new file mode 100644 index 0000000..d8575a7 --- /dev/null +++ b/openspec/specs/command-vocabulary/spec.md @@ -0,0 +1,121 @@ +# command-vocabulary Specification + +## Purpose + +Define a single controlled vocabulary — canonical verbs, entity types, and loci — that all +command surfaces (CLI, MCP, agent) name operations from. Surfaces share these names but +dispatch independently (Option B: shared names, separate dispatch). The vocabulary is +defined in the `ical4j-command-core` module. + +## Requirements + +### Requirement: Operation identity is (verb, type, locus) +Every operation exposed by any surface (CLI, MCP, agent) SHALL be identified by a tuple of exactly three axes: a **verb** (the action), an **entity type** (what is acted on), and a **locus** (the container the action acts within). Surface-specific naming SHALL be derivable from this tuple. + +#### Scenario: Decomposing an existing operation +- **WHEN** the MCP method `listCollections(workspace)` is mapped to the vocabulary +- **THEN** it resolves to verb `list`, type `collection`, locus `workspace` + +#### Scenario: Two surfaces name the same operation +- **WHEN** the CLI and the agent both expose "list the events in a collection" +- **THEN** both resolve to the same tuple (`list`, `event`, `collection`) even though their surface syntax differs + +### Requirement: Canonical verb vocabulary +The system SHALL define a closed set of canonical verbs. Surfaces MUST use these verbs and MUST NOT introduce synonyms for an existing verb. The verbs and their meanings are: + +- `list` — return many items of a type within a locus. +- `get` — return one item of a type, addressed by identifier. +- `generate` — synthesise a new item from a strategy/template (e.g. an event from a meeting template), independent of any locus. +- `add` — place an item into a locus, whether newly made or pre-existing. +- `update` — modify an existing item in place. +- `remove` — take an item out of a locus. +- `copy` — duplicate an item into a (possibly different) locus. +- `move` — relocate an item from one locus to another. +- `filter` — derive a subset of a type within a locus by expression. +- `validate` — check an item or locus for conformance. +- `import` / `export` — read/write a serialization family (calendar/card) into/out of a locus. +- `send` / `receive` — transmit/accept items over a channel. + +#### Scenario: Plural results never use `get` +- **WHEN** an operation returns more than one item +- **THEN** its verb is `list` (or `filter`), never `get` + +#### Scenario: Add covers both new and existing items +- **WHEN** an operation places an item into a locus, whether it is brand-new or already exists +- **THEN** its verb is `add` — there is no separate `create` verb + +#### Scenario: Generate is distinct from add +- **WHEN** an operation synthesises an item from a strategy/template without placing it into a locus (e.g. emitting a generated event to output) +- **THEN** its verb is `generate`, not `add` +- **WHEN** the generated item is subsequently placed into a locus +- **THEN** that placement is a separate `add` operation + +#### Scenario: Remove covers both detach and destroy +- **WHEN** an operation takes an item out of a locus, whether the item continues to exist elsewhere or not +- **THEN** its verb is `remove` — there is no separate `delete` verb + +#### Scenario: Rejecting a synonym +- **WHEN** a surface proposes an operation named with a verb synonym (e.g. `fetch`, `make`, `create`, `delete`, `destroy`) +- **THEN** it is mapped to the canonical verb (`get`, `add`, `remove`) and the synonym is not exposed + +### Requirement: Canonical entity types +The system SHALL define the set of entity types that operations act on: `event`, `action`, `issue`, `note`, `observance`, `availability`, `report`, `entity` (contact), and `resource`. Surfaces MUST name types from this set. + +#### Scenario: Naming an operation by type +- **WHEN** an operation lists actions in a collection +- **THEN** the type axis is `action` and the operation resolves to (`list`, `action`, `collection`) + +### Requirement: Locus hierarchy has exactly two levels +The system SHALL define loci as a containment hierarchy of exactly two levels: a `workspace` contains `collection`s, and a `collection` directly contains entities. No additional container level exists between a collection and its entities. + +#### Scenario: Entities live directly in a collection +- **WHEN** an event is created within a collection +- **THEN** its locus is `collection` directly, with no intervening container + +#### Scenario: Collections live in a workspace +- **WHEN** a collection is created +- **THEN** its locus is `workspace` + +### Requirement: Calendar and card are serialization families, not loci or types +The system SHALL treat `calendar` (iCalendar) and `card` (vCard) as serialization/object families used for import and export, NOT as loci and NOT as entity types. Operations that move data between a serialization family and a collection SHALL use the `import`/`export` verbs. + +#### Scenario: Importing a calendar populates a collection +- **WHEN** a `.ics` calendar is brought into a collection +- **THEN** the operation is `import` with the `calendar` family, targeting locus `collection` — not an `add` of a `calendar` container + +#### Scenario: Calendar is not a container level +- **WHEN** resolving the locus of an event +- **THEN** the locus is `collection`, and `calendar` does not appear as a container in the locus hierarchy + +### Requirement: iTIP methods are specialised send operations +The system SHALL treat the calendar messaging methods `publish`, `request`, and `reply` as specialised forms of the `send` verb (a `send` carrying a specific iTIP method), not as independent verbs on the verb axis. + +#### Scenario: Publish resolves to a specialised send +- **WHEN** the agent's `CalendarUserAgent.publish(...)` is mapped to the vocabulary +- **THEN** it resolves to `send` specialised with the iTIP method `PUBLISH`, on locus `channel` + +#### Scenario: Request and reply share the send axis +- **WHEN** `request` and `reply` are mapped to the vocabulary +- **THEN** both resolve to `send` specialised with their respective iTIP methods, and neither introduces a new top-level verb + +### Requirement: Surfaces share vocabulary but dispatch independently +Each surface (CLI, MCP, agent) SHALL name its operations from the shared vocabulary, but SHALL retain its own dispatch and execution. There SHALL NOT be a shared command-execution core that surfaces delegate into; conformance is enforced by naming rules, not by a common dispatcher. + +#### Scenario: No cross-surface execution coupling +- **WHEN** the MCP layer exposes a `list` operation +- **THEN** it implements its own dispatch and does not call the CLI's command classes to execute the operation + +#### Scenario: Adding a verb to the vocabulary +- **WHEN** a new canonical verb is added to the vocabulary +- **THEN** each surface may adopt it independently using the shared name, with no shared dispatcher required to change + +### Requirement: The vocabulary lives in a shared core module +The canonical verbs, entity types, and loci SHALL be defined as constants/enums in a new shared **core module**. Each surface module (CLI, MCP, agent) SHALL depend on the core module for these names. The core module SHALL contain vocabulary definitions only — it MUST NOT contain command dispatch or execution logic, preserving Option B (shared names, separate dispatch). + +#### Scenario: Surfaces reference core vocabulary constants +- **WHEN** a surface names an operation +- **THEN** it derives the verb, type, and locus names from the core module's definitions rather than declaring its own string literals + +#### Scenario: Core module carries no dispatch +- **WHEN** the core module is inspected +- **THEN** it contains only vocabulary definitions (verbs, types, loci) and no command-execution or dispatch code diff --git a/settings.gradle b/settings.gradle index e5e5e73..b0d493a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'ical4j-command' +include "ical4j-command-core" include "ical4j-command-cli" //include "ical4j-command-agent" -//include "ical4j-command-mcp" +include "ical4j-command-mcp"