|
| 1 | +# Project Guidelines |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +RSS Reader is a zero-dependency Java 11+ library for parsing RSS and Atom feeds, published to Maven Central as `com.apptasticsoftware:rssreader`. It uses JPMS (`module com.apptasticsoftware.rssreader`) and StAX for XML parsing. |
| 6 | + |
| 7 | +## Build and Test |
| 8 | + |
| 9 | +```sh |
| 10 | +./gradlew build # compile + test + JaCoCo coverage |
| 11 | +./gradlew test # tests only |
| 12 | +./gradlew javadoc # generate API docs |
| 13 | +./gradlew jacocoTestReport # coverage report |
| 14 | +``` |
| 15 | + |
| 16 | +Java 11 source/target compatibility. No runtime dependencies — only test-scoped: JUnit 5, AssertJ, Mockito, EqualsVerifier. |
| 17 | + |
| 18 | +## Key Files |
| 19 | + |
| 20 | +| File | Purpose | |
| 21 | +|------|---------| |
| 22 | +| `src/main/java/module-info.java` | JPMS exports — update when adding packages | |
| 23 | +| `src/main/java/.../AbstractRssReader.java` | Base parser: StAX, HTTP, extension registry | |
| 24 | +| `src/main/java/.../FeedReader.java` | Unified reader registering ALL modules | |
| 25 | +| `src/main/java/.../FeedExtensionRegistry.java` | Tag → setter mapping API | |
| 26 | +| `src/main/java/.../util/Mapper.java` | Type conversion utilities | |
| 27 | +| `src/main/java/.../XMLInputFactorySecurity.java` | XXE protection hardening | |
| 28 | + |
| 29 | +## Architecture |
| 30 | + |
| 31 | +### Core class hierarchy |
| 32 | + |
| 33 | +- `AbstractRssReader<C extends Channel, I extends Item>` — generic base handling StAX parsing, HTTP, filters, timeouts |
| 34 | +- `RssReader` — basic reader (no extensions) |
| 35 | +- `FeedReader` — unified reader registering ALL module extensions via composed `FeedChannel`/`FeedItem` |
| 36 | +- `Channel` / `Item` — public interfaces; `ChannelImpl` / `ItemImpl` — internal implementations |
| 37 | + |
| 38 | +### Module extension pattern |
| 39 | + |
| 40 | +Each namespace module lives in `src/main/java/com/apptasticsoftware/rssreader/module/<name>/` and follows this structure: |
| 41 | + |
| 42 | +| File | Purpose | |
| 43 | +|------|---------| |
| 44 | +| `XyzChannel` | Interface extending `Channel` + `XyzChannelData` | |
| 45 | +| `XyzItem` | Interface extending `Item` + `XyzItemData` | |
| 46 | +| `XyzChannelData` / `XyzItemData` | Data interfaces with default getter/setter methods delegating to `getXyzItemData()` | |
| 47 | +| `XyzExtensions` | Static `register()` method wiring XML tags → setters via `FeedExtensionRegistry` | |
| 48 | +| `XyzFeedReader` | Standalone reader extending `AbstractRssReader` | |
| 49 | +| `internal/XyzChannelImpl` | Extends `ChannelImpl`, composes `XyzChannelDataImpl` | |
| 50 | +| `internal/XyzItemImpl` | Extends `ItemImpl`, composes `XyzItemDataImpl` | |
| 51 | +| `internal/XyzChannelDataImpl` / `XyzItemDataImpl` | Plain data holders with equals/hashCode | |
| 52 | + |
| 53 | +Modules: atom, content, dc, georss, itunes, mediarss, opensearch, podcast, psc, slash, spotify, wfw, youtube. |
| 54 | + |
| 55 | +When adding a new module, update these three locations: |
| 56 | +1. `module-info.java` — add `exports` for the new package |
| 57 | +2. `FeedReader.registerChannelTags()` — call `XyzExtensions.register(registry)` |
| 58 | +3. `FeedChannel`/`FeedItem` interfaces and their internal impls — compose new data interfaces |
| 59 | + |
| 60 | +### Extension registration API |
| 61 | + |
| 62 | +`FeedExtensionRegistry` provides these registration methods in `XyzExtensions.register()`: |
| 63 | + |
| 64 | +```java |
| 65 | +// Map XML tag text content to a setter |
| 66 | +registry.addChannelExtension("ns:tag", XyzChannel::setSomeField); |
| 67 | +registry.addItemExtension("ns:tag", XyzItem::setSomeField); |
| 68 | + |
| 69 | +// Map XML attribute value to a setter |
| 70 | +registry.addItemExtension("ns:tag", "attributeName", XyzItem::setSomeField); |
| 71 | + |
| 72 | +// Callback on tag start (e.g., to initialize lists) |
| 73 | +registry.addOnItemTag("ns:tag", item -> { /* on START_ELEMENT */ }); |
| 74 | +``` |
| 75 | + |
| 76 | +Use `Mapper` for non-String types: `(item, value) -> mapInteger(value, item::setCount)` |
| 77 | + |
| 78 | +## Conventions |
| 79 | + |
| 80 | +- **Getters return `Optional<T>`** for nullable fields; setters accept direct types |
| 81 | +- **Lists** for multi-valued fields (categories, enclosures) |
| 82 | +- **Internal packages** (`internal/`) are not exported — keep implementation details there |
| 83 | +- **Type conversions** use `Mapper` utilities (`mapBoolean`, `mapInteger`, `mapLong`, `mapDouble`) |
| 84 | +- **Deprecation**: Use `@Deprecated(since="X.Y.Z", forRemoval=true)` with migration guidance |
| 85 | +- **XML security**: `XMLInputFactorySecurity` hardens the StAX parser against XXE — never bypass it |
| 86 | + |
| 87 | +## Testing |
| 88 | + |
| 89 | +- **JUnit 5** with **AssertJ** assertions (including `hasValue()` for Optional assertions) |
| 90 | +- **Parameterized tests** via `@ParameterizedTest` + `@MethodSource` — test both the standalone `XyzFeedReader` AND the unified `FeedReader` |
| 91 | +- **EqualsVerifier** for equals/hashCode contracts on all `*Impl` and `*DataImpl` classes — ignore `"defaultComparator"` and `"dateTimeParser"` fields in `*ItemImpl`/`*ChannelImpl` |
| 92 | +- **Mockito** for HTTP response mocking |
| 93 | +- **Fixtures** in `src/test/resources/` — module-specific XML in `module/<name>/` |
| 94 | +- Load fixtures via `getClass().getClassLoader().getResourceAsStream(path)` |
| 95 | + |
| 96 | +## Pitfalls |
| 97 | + |
| 98 | +- **Namespace awareness is OFF** (`IS_NAMESPACE_AWARE=false`) — XML tags use their prefixed form (e.g., `slash:comments`, not just `comments`) |
| 99 | +- **`IS_COALESCING=true`** — adjacent text/CDATA are merged; don't rely on individual text events |
| 100 | +- **Data interface delegation pattern**: `XyzItemData.getXyzItemData()` returns `this` in the data impl but delegates in the composite — don't break this chain |
| 101 | +- **Module test fixture naming**: Place XML files in `src/test/resources/module/<name>/` matching the module name |
0 commit comments