Skip to content

Control where properties are inserted #68

Description

@nx-eric-jarosch

Usecase

Currently properties are always inserted after the last property (or after the header, if no properties are present).

I'm using this library to migrate configuration files that are primarily human-edited.
It makes sense to group related properties nearby each other, so I arrived at this abstraction:

There is an interface whose implementations control where a property is inserted.

interface InsertWhere {
    Cursor navigate(Properties p);
}

And 5 implementations:

  • InsertWhere firstLine();
  • InsertWhere beforeProperty(String key, InsertWhere fallback);
  • InsertWhere afterProperty(String key, InsertWhere fallback);
  • InsertWhere lastProperty();
  • InsertWhere lastLine();

Concept

It'd be possible to add an overload to Properties#put and Properties#putCommented which accept an instance of InsertWhere.
This way users could optionally control where properties are inserted.

Discussion

Do you think this feature is suitable for this library?
Or is the Cursor API simple enough that end-users are expected to write these for themselves as needed?

Activity

  1. changed the title [-][Feature Discussion] Presets for controlling where properties are inserted[/-] [+]Control where properties are inserted[/+] on Sep 23, 2026
  2. quintesse commented on Sep 23, 2026

    @quintesse
    Collaborator

    The idea is definitely interesting, I have to think a bit about the suggested API. At first glance it doesn't really seem to "fit" with the rest of the API. But the idea of creating several ways to put the cursor where you want it to then use that to insert your new property seems a good one.

  3. quintesse commented on Sep 24, 2026

    @quintesse
    Collaborator

    I think that instead of creating a new interface and an entirely new API for this I'd rather go with some helper functions using the Cursor mechanism. For example:

    • Cursor Properties.first() - already exists
    • Cursor Properties.last() - also already exists
    • Cursor Properties.before(String key)
    • Cursor Properties.after(String key)

    Having two extra insertion points where you'd normally insert the first property (after any header comments) or right after the last property are also interesting, just not sure about the names, something like this I guess:

    • Cursor Properties.beforeFirstProperty() - should handle when no properties exist yet
    • Cursor Properties.afterLastProperty() - should call beforeFirstProperty() when no properties exist yet

    Once you get the cursor it's easy enough to insert something at that point using Cursor.add().

  4. quintesse commented on Sep 24, 2026

    @quintesse
    Collaborator

    Btw, the code to implement beforeFirstProperty() and afterLastProperty() really already exist, because it's what the internal method Properties.addNewKeyValue() uses to determine where to insert new properties. Its code could be refactored to create both beforeFirstProperty() and afterLastProperty() instead. And then it simply calls afterLastProperty() and things should be good.

  5. quintesse commented on Sep 24, 2026

    @quintesse
    Collaborator

    Just realized we probably need something else as well, because finding the right spot to insert a new property is the first step. But step 2 is making sure we can actually insert something there (imagine that after("foo") leaves us at the end, then we have to insert an EOL token before we can insert anything else.), perhaps something like:

    • prepareAdd(Cursor c) - prepares the Properties object to accept a new item being added to the current location
  6. nx-eric-jarosch commented on Sep 24, 2026

    @nx-eric-jarosch
    ContributorAuthor

    I think it would make sense to create Properties#addProperty(Cursor cursor, String key, String value).
    This method would be responsible for creating tokens in the correct order, converting to raw strings, and adding EOLs as needed.

    That would also lighten the cognitive load for end-users.

    (edit: this method should also navigate / throw an exception if the cursor points at a separator or value token, as an insertion here would be destructive)

  7. quintesse commented on Sep 24, 2026

    @quintesse
    Collaborator

    Yes, I was trying out this idea of prepareAdd() and I didn't like it very much. So I agree, I think a setProperty() is probably the best idea.

  8. added 2 commits that reference this issue on Sep 30, 2026
    552e044
    6b54071
  9. quintesse commented on Sep 30, 2026

    @quintesse
    Collaborator

    @nx-eric-jarosch I just merged an implementation for this and I wanted to know if this works for your use-case before creating a new release.

    Again thanks for all your help and suggestions!

    Let me know what you think.

  10. quintesse commented on Sep 30, 2026

    @quintesse
    Collaborator

    As a short explanation, you just use one of the methods for getting positions (first(), last(), beforeProperty(), afterProperty(), beforeFirstProperty(), afterLastProperty(), home()) together with put(pos, key, value) and things should just work. (Eg. put(afterProperty("foo"), "bar", "baz");)

  11. nx-eric-jarosch commented on Oct 1, 2026

    @nx-eric-jarosch
    ContributorAuthor

    @quintesse I've had a look and found these 4 issues. (linked above)

    last().next() is a bit clunky for inserting a new last line, but it does make sense based on how inserting works.. so shrug.

    Other than that, your change does suit my use-case. Thanks!


    Note that my test-cases assume this convenience method.

        private void expectStoreText(Properties props, String expectedText) throws IOException {
            StringWriter sw = new StringWriter();
            props.store(sw);
            assertThat(sw.toString()).isEqualTo(expectedText);
        }
  12. quintesse commented on Oct 1, 2026

    @quintesse
    Collaborator

    last().next() is a bit clunky for inserting a new last line

    indeed, I did actually think of adding things like start() (ie first().prev()) and end() (ie last().next()) but I felt that would actually be more confusing.

  13. quintesse commented on Oct 1, 2026

    @quintesse
    Collaborator

    @nx-eric-jarosch all your issues have been fixed, could you perhaps find some more? I mean, could you check if everything works now? 🙂

  14. nx-eric-jarosch commented on Oct 2, 2026

    @nx-eric-jarosch
    ContributorAuthor

    @quintesse I've had a look, but found no bugs :)

    I did find some "technically correct" behaviour:

    @Test
    void testTechnicallyCorrectPut() throws IOException {
        final String given = "# header\n\n";
        final String expected = "put=val\n# header\n\n";
    
        Properties p = Properties.loadProperties(new StringReader(given));
        p.put(p.afterLastProperty(), "put", "val");
        expectStoreText(p, expected);
    }

    this behaviour matches the documentation:

    Returns a Cursor pointing to the position past the last property, or to -1 if the Properties object is empty.

    But it's not what I'd intuitively expect: # header\n\nput=val\n


    Either way, I'm approaching nitpicking territory.
    I'm ready to call this solved.

  15. quintesse commented on Oct 2, 2026

    @quintesse
    Collaborator

    But it's not what I'd intuitively expect: # header\n\nput=val\n

    You're right! I think the second part of this sentence was never actually implemented:

    • Cursor Properties.afterLastProperty() - should call beforeFirstProperty() when no properties exist yet

    So, I'm still going to fix that before releasing. Thanks @nx-eric-jarosch !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions