Skip to content

Commit 8cdbc7e

Browse files
authored
Add javadoc redirect fallback (#5615)
1 parent c149095 commit 8cdbc7e

3 files changed

Lines changed: 213 additions & 0 deletions

File tree

site/static/404.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Page not found | RDF4J</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11+
line-height: 1.5;
12+
}
13+
body {
14+
margin: 2rem auto;
15+
max-width: 48rem;
16+
padding: 0 1.5rem;
17+
}
18+
h1 {
19+
font-size: 2rem;
20+
margin-bottom: 0.5rem;
21+
}
22+
p {
23+
margin-top: 0;
24+
}
25+
</style>
26+
</head>
27+
<body>
28+
<main>
29+
<h1>Page not found</h1>
30+
<p>We could not find the page you were looking for.</p>
31+
<p>If you followed a link to a specific Javadoc version, we will try to forward you to the closest available version automatically.</p>
32+
</main>
33+
<script type="module">
34+
import { attemptJavadocRedirect } from '/js/javadoc-redirect.js';
35+
36+
attemptJavadocRedirect();
37+
</script>
38+
</body>
39+
</html>

site/static/js/javadoc-redirect.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
function parseSemver(version) {
2+
if (typeof version !== 'string') {
3+
return null;
4+
}
5+
6+
const parts = version.match(/\d+/g);
7+
if (!parts || parts.length === 0) {
8+
return null;
9+
}
10+
11+
return parts.map((part) => Number.parseInt(part, 10));
12+
}
13+
14+
function compareParts(aParts, bParts) {
15+
const maxLength = Math.max(aParts.length, bParts.length);
16+
for (let i = 0; i < maxLength; i += 1) {
17+
const a = aParts[i] ?? 0;
18+
const b = bParts[i] ?? 0;
19+
if (a !== b) {
20+
return a - b;
21+
}
22+
}
23+
return 0;
24+
}
25+
26+
function compareSemver(a, b) {
27+
const parsedA = parseSemver(a);
28+
const parsedB = parseSemver(b);
29+
30+
if (!parsedA || !parsedB) {
31+
return 0;
32+
}
33+
34+
return compareParts(parsedA, parsedB);
35+
}
36+
37+
function findClosestVersion(requestedVersion, availableVersions) {
38+
const requestedParts = parseSemver(requestedVersion);
39+
if (!requestedParts) {
40+
return null;
41+
}
42+
43+
const parsedVersions = availableVersions
44+
.map((version) => ({ version, parts: parseSemver(version) }))
45+
.filter((entry) => entry.parts)
46+
.sort((a, b) => compareParts(a.parts, b.parts));
47+
48+
let lower = null;
49+
for (const entry of parsedVersions) {
50+
const comparison = compareParts(entry.parts, requestedParts);
51+
if (comparison === 0) {
52+
return entry.version;
53+
}
54+
if (comparison > 0) {
55+
return entry.version;
56+
}
57+
lower = entry.version;
58+
}
59+
60+
return lower;
61+
}
62+
63+
function isJavadocPath(pathname) {
64+
return typeof pathname === 'string' && pathname.startsWith('/javadoc/');
65+
}
66+
67+
export function findRedirectPath(pathname, availableVersions) {
68+
if (!isJavadocPath(pathname)) {
69+
return null;
70+
}
71+
72+
const segments = pathname.split('/');
73+
// ['', 'javadoc', '<version>', ...]
74+
const requestedVersion = segments[2];
75+
if (!requestedVersion) {
76+
return null;
77+
}
78+
79+
const targetVersion = findClosestVersion(requestedVersion, availableVersions ?? []);
80+
if (!targetVersion || targetVersion === requestedVersion) {
81+
return null;
82+
}
83+
84+
const nextSegments = segments.slice();
85+
nextSegments[2] = targetVersion;
86+
87+
return nextSegments.join('/') || '/';
88+
}
89+
90+
export function extractVersions(manifestEntries) {
91+
if (!Array.isArray(manifestEntries)) {
92+
return [];
93+
}
94+
95+
return manifestEntries
96+
.map((entry) => (typeof entry === 'string' ? entry : entry?.name))
97+
.filter((value) => typeof value === 'string')
98+
.filter((value) => parseSemver(value));
99+
}
100+
101+
export async function attemptJavadocRedirect(manifestUrl = '/javadoc/manifest.json') {
102+
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
103+
return null;
104+
}
105+
106+
const { pathname, search, hash } = window.location;
107+
if (!isJavadocPath(pathname)) {
108+
return null;
109+
}
110+
111+
let versions = [];
112+
try {
113+
const response = await fetch(manifestUrl, { cache: 'no-store' });
114+
if (!response.ok) {
115+
throw new Error(`Unexpected response ${response.status}`);
116+
}
117+
const manifest = await response.json();
118+
versions = extractVersions(manifest);
119+
} catch (error) {
120+
console.warn('Unable to load javadoc manifest for redirect:', error);
121+
return null;
122+
}
123+
124+
const targetPath = findRedirectPath(pathname, versions);
125+
if (!targetPath) {
126+
return null;
127+
}
128+
129+
const nextUrl = new URL(window.location.href);
130+
nextUrl.pathname = targetPath;
131+
nextUrl.search = search;
132+
nextUrl.hash = hash;
133+
window.location.replace(nextUrl.toString());
134+
return nextUrl.toString();
135+
}
136+
137+
if (typeof window !== 'undefined') {
138+
window.JavadocRedirect = {
139+
attemptJavadocRedirect,
140+
extractVersions,
141+
findRedirectPath,
142+
};
143+
}
144+
145+
export default {
146+
attemptJavadocRedirect,
147+
extractVersions,
148+
findRedirectPath,
149+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { findRedirectPath } from './javadoc-redirect.js';
4+
5+
const versions = ['5.1.0', '5.1.2', '5.2.0', '4.3.16'];
6+
7+
test('redirects to the next patch version when available', () => {
8+
const result = findRedirectPath('/javadoc/5.1.1/org/example/Foo.html', versions);
9+
assert.equal(result, '/javadoc/5.1.2/org/example/Foo.html');
10+
});
11+
12+
test('redirects to the next minor or major version when patch is missing', () => {
13+
const result = findRedirectPath('/javadoc/5.1.2/org/example/Foo.html', ['5.1.0', '5.2.0']);
14+
assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html');
15+
});
16+
17+
test('falls back to the highest available lower version when no newer version exists', () => {
18+
const result = findRedirectPath('/javadoc/9.0.0/org/example/Foo.html', versions);
19+
assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html');
20+
});
21+
22+
test('ignores non-semver entries when calculating redirects', () => {
23+
const result = findRedirectPath('/javadoc/5.1.1/org/example/Foo.html', ['latest', '5.2.0']);
24+
assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html');
25+
});

0 commit comments

Comments
 (0)