Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions CONFIGURATION_RESOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Configuration Resolution: Scope-First vs. Type-First

## The Two Strategies

Given `Foo extends Entity` with configurations at four points in the matrix:

| | Instance | Global |
|---|---|---|
| **Foo** | I·Foo | G·Foo |
| **Entity** | I·Entity | G·Entity |

**Scope-first** exhausts all instance settings before looking at global ones:
```
I·Foo → I·Entity → G·Foo → G·Entity → Default
```

**Type-first** exhausts all Foo-specific settings before looking at Entity:
```
I·Foo → G·Foo → I·Entity → G·Entity → Default
```

Both agree on `I·Foo` being first and `G·Entity` being last. The dispute is the middle two.

---

## Where Scope-First Makes Sense

**Setting: `textAlign` or `nullRepresentation`**

A developer creates a reporting grid and writes:
```java
easyGrid.configuration.forType(Entity.class).setTextAlign(RIGHT);
```
The intent is *"every Entity-like column in this grid should be right-aligned."* Since `Foo IS-AN Entity`, Foo columns should also be right-aligned. Instance context wins: the developer explicitly opted every Entity column into this layout decision, and Foo, being substitutable for Entity, should inherit it.

If type-first were applied, `G·Foo` would sit between `I·Foo` and `I·Entity`. An unrelated global renderer for Foo would silently block the instance-level alignment — which is the opposite of what the developer intended.

---

## Where Type-First Makes Sense

**Setting: `rendererFactory`**

Globally, `Foo` has a specialized renderer registered:
```java
GlobalEasyGridConfiguration.forType(Foo.class)
.setRendererFactory(FooRenderers.of(...));
```
Independently, an instance configures Entity columns with a generic formatter:
```java
easyGrid.configuration.forType(Entity.class).setFormatter(e -> e.getId().toString());
```

Under scope-first, `I·Entity` wins over `G·Foo`. The Foo-specific renderer — which encodes *what a Foo looks like*, arguably a type invariant — gets silently replaced by a generic Entity formatter that the developer may not have intended to apply to Foo.

Type-first preserves the type contract: `G·Foo` sits above `I·Entity`, so Foo's rendering semantics are honoured even across scope boundaries.

---

## Liskov Substitution Principle

LSP says: a `Foo` must be usable wherever an `Entity` is expected, without callers needing to know the difference.

Applied to configuration, it has two implications that pull in opposite directions:

**LSP supports scope-first for behavioural settings.** If you configure `Entity` at instance level — null representation, alignment, a locale-specific formatter — you are specifying how *this grid* handles all Entity values. A `Foo` substituting for `Entity` should satisfy those same observable postconditions. Refusing to inherit `I·Entity` because Foo has a `G·Foo` entry would mean the grid behaves differently for `Foo` vs `Entity` in ways the calling code did not anticipate.

**LSP supports type-first for type-defining settings.** LSP also requires that subtypes honour their own invariants. If `Foo` has a type-level rendering contract — *this is how a Foo is displayed* — then substituting a generic `Entity` renderer for it violates the Foo invariant. The renderer is not just a postcondition on the grid; it is a property of `Foo` itself.

The tension resolves in favour of **scope-first for all properties**. The type-first argument rests on the assumption that `I·Entity` displaces `G·Foo` unintentionally — but instance configuration is always deliberate. A developer who writes `forType(Entity.class).setFormatter(...)` knows `Foo IS-AN Entity`; if they wanted Foo to keep its global renderer they would have set `I·Foo` separately. The argument collapses entirely at broad overrides such as `forType(Object.class).setFormatter(...)`, where the developer has unambiguously stated that everything in this grid uses this formatter and global type-specific registrations cannot claim precedence. **Global registrations are defaults. Instance registrations are decisions. Decisions outrank defaults regardless of type specificity.**

---

## Current Implementation

The implementation uses **scope-first** for all properties. For a `Foo extends Entity` column the effective chain is:

```
per-column → I·Foo → I·Entity → I·Object → G·Foo → G·Entity → G·Object → Default
```

This is built in two pieces:

**Instance chain** — `EasyGridConfigurationClassMap.getOrCreate(Foo)` walks the Java class hierarchy within the same map, producing a `ColumnConfigurationImpl` chain:

```
I·Foo(impl) → I·Entity(impl) → I·Object(impl)
```

**Global chain** — `GlobalEasyGridConfiguration.resolve(Foo)` similarly produces:

```
G·Foo(impl) → G·Entity(impl) → G·Object(impl)
```

**Bridge** — `InstanceEasyGridConfiguration.forType(Foo)` wraps both into a `ColumnConfigurationLink`:

```
ColumnConfigurationLink(primary = I·Foo chain, fallback = G·Foo chain)
```

`ColumnConfigurationLink.get()` consults the primary chain first and only falls back to the global chain when the primary returns `null` for a given property.

**Per-column** — `InstanceEasyGridConfiguration.resolve(Foo)` wraps the link in a fresh `ColumnConfigurationImpl` whose fields are overridden by column-level setters (`setNullRepresentation`, `setFormatter`, `setRendererFactory`).

**Default** — when the entire chain returns `null` for `getRendererFactory()`, `EasyColumn.createRenderer` applies a `ColumnConfigurationTextRenderer` with null-representation support as the last resort.
93 changes: 79 additions & 14 deletions SPECIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ Each Java type maps to a default column renderer and configuration:
| `LocalDate` | `LocalDateRenderer` | Start | Chronological |
| `LocalDateTime` | `LocalDateTimeRenderer` | Start | Chronological |
| `LocalTime` | `TextRenderer` (formatted) | Start | Chronological |
| `Enum<?>` | `TextRenderer` (name) | Start | Alphabetical |
| `Enum<?>` | `TextRenderer` (`toString()`) | Start | Alphabetical |

Custom type mappings can be registered globally or per-grid instance.

**Boolean rendering:** `Boolean` columns render as the string literals `"true"` or `"false"` by default. Any other representation ("Yes"/"No", "On"/"Off", localised strings, etc.) requires a custom formatter — typically one that delegates to an application i18n provider.

**Enum rendering:** Enum columns render using `Enum.toString()`, not `Enum.name()`. Per the Java documentation for `Enum.name()`: *"Most programmers should use the toString method in preference to this one, as the toString method may return a more user-friendly name."* Enums that need human-readable labels should override `toString()` accordingly.

### 2.3 Sorting

All columns backed by `Comparable` property types are sortable by default. Multi-column sorting is supported.
Expand Down Expand Up @@ -139,22 +143,83 @@ easyGrid.getColumn("firstName")

See [FEATURE_ROW_ACTIONS.md](FEATURE_ROW_ACTIONS.md).

### 3.5 Global Type Configuration
### 3.5 Type Configuration Tree

Column display configuration is resolved through a three-level tree, from most to least specific:

| Level | API | Scope |
|---|---|---|
| **Column** | `EasyColumn` setters | One specific column |
| **Instance** | `EasyGrid.forType(Class)` | All columns of that type in one grid |
| **Global** | `GlobalEasyGridConfiguration.forType(Class)` | All grids in the application |

Within each level the class hierarchy is walked before the tree falls through to the next level (scope-first). The full resolution order for a `Foo extends Entity` column is:

```
Column·Foo
→ Instance·Foo → Instance·Entity → Instance·Object
→ Global·Foo → Global·Entity → Global·Object
→ Built-in default
```

The first non-`null` value found wins. See [CONFIGURATION_RESOLUTION.md](CONFIGURATION_RESOLUTION.md) for the rationale behind scope-first ordering.
Comment thread
mlopezFC marked this conversation as resolved.

**Column level** — `EasyGrid.addColumn(…)` returns an `EasyColumn` whose setters write into an isolated configuration node at the top of the chain for that column only:

```java
easyGrid.addColumn("active").setNullRepresentation("—");
easyGrid.addColumn("salary").setTextAlign(ColumnTextAlign.END);
```

**Instance level** — `EasyGrid.forType(Class)` returns the instance-level `ColumnConfiguration` for a type. Changes apply to every column of that type on this grid:

```java
easyGrid.forType(BigDecimal.class)
.setRendererFactory(NumberRenderers.of("%,.2f", Locale.US));
```

**Global level** — `GlobalEasyGridConfiguration.forType(Class)` returns the application-wide `ColumnConfiguration`. Call `GlobalEasyGridConfiguration.freeze()` after startup to prevent further modifications:

```java
GlobalEasyGridConfiguration.forType(LocalDate.class)
.setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy"));
GlobalEasyGridConfiguration.freeze();
```

#### Null Representation

Register custom column configurations that apply to all `EasyGrid` instances:
The `nullRepresentation` property controls what is displayed when a column value is `null`. The built-in global default registers `""` (empty string) on `Object.class`, so every column starts with an empty cell for `null` values. Override it at any level:

```java
// Register a global formatter for a custom type
EasyGrid.registerTypeConfig(Money.class, config -> {
config.setFormatter(money -> money.getCurrency() + " " + money.getAmount());
});

// Register a global renderer for nested types
EasyGrid.registerTypeConfig(Address.class, config -> {
config.setFormatter(address ->
address.getStreet() + ", " + address.getCity()
);
});
// All columns in this grid show "–" for null
easyGrid.forType(Object.class).setNullRepresentation("–");

// Only the "email" column shows "(none)" for null
easyGrid.addColumn("email").setNullRepresentation("(none)");
```

Formatters that receive a `ColumnConfiguration` parameter can call `getNullRepresentation()` to produce consistent output.

#### Type Hierarchy Support

Inside each scope level, `EasyGridConfigurationClassMap` walks the Java class hierarchy. When a configuration is requested for `Foo` and none exists, it is created with `Foo`'s superclass configuration as its parent, continuing up to `Object`. Primitive types are mapped to their boxed counterparts before hierarchy walking (`int` → `Integer`, `boolean` → `Boolean`, etc.).

A global `Number.class` renderer factory is therefore automatically inherited by `Integer`, `Long`, `BigDecimal`, and every other `Number` subtype unless a more specific configuration overrides it.

#### Renderer Utility Classes

Three `@UtilityClass` types in `com.flowingcode.vaadin.addons.easygrid.renderers` produce `RendererFactory` instances for common value types:

- **`LocalDateRenderers`** — wraps `LocalDateRenderer`; overloads accept a format pattern, locale, null representation, or a `DateTimeFormatter` supplier.
- **`LocalDateTimeRenderers`** — wraps `LocalDateTimeRenderer`; same overloads as `LocalDateRenderers`.
- **`NumberRenderers`** — wraps `NumberRenderer`; overloads accept a `NumberFormat`, a `Locale`, or a `Formatter` pattern string with optional locale and null representation.

```java
GlobalEasyGridConfiguration.forType(LocalDate.class)
.setRendererFactory(LocalDateRenderers.of("dd/MM/yyyy", Locale.UK));

GlobalEasyGridConfiguration.forType(Number.class)
.setRendererFactory(NumberRenderers.of(NumberFormat.getInstance()));
```

## 4. Default Header Generation
Expand Down
Loading