Skip to content

Commit 5a7e383

Browse files
authored
Generate authentication modules reference in AsciiDoc format (#916)
1 parent b749d10 commit 5a7e383

File tree

7 files changed

+619
-3
lines changed

7 files changed

+619
-3
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
* The contents of this file are subject to the terms of the Common Development and
4+
* Distribution License (the License). You may not use this file except in compliance with the
5+
* License.
6+
*
7+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
8+
* specific language governing permission and limitations under the License.
9+
*
10+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
11+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
12+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
13+
* information: "Portions copyright [year] [name of copyright owner]".
14+
*
15+
* Copyright 2025 3A Systems LLC.
16+
-->
17+
<project xmlns="http://maven.apache.org/POM/4.0.0"
18+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
19+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
<parent>
22+
<groupId>org.openidentityplatform.openam</groupId>
23+
<artifactId>openam-documentation</artifactId>
24+
<version>15.2.2-SNAPSHOT</version>
25+
</parent>
26+
<artifactId>openam-doc-services-ref</artifactId>
27+
<dependencies>
28+
<dependency>
29+
<groupId>org.openidentityplatform.openam</groupId>
30+
<artifactId>openam-server-only</artifactId>
31+
<version>${project.version}</version>
32+
<type>pom</type>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.jsoup</groupId>
36+
<artifactId>jsoup</artifactId>
37+
<version>1.21.2</version>
38+
</dependency>
39+
</dependencies>
40+
<build>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.codehaus.mojo</groupId>
44+
<artifactId>exec-maven-plugin</artifactId>
45+
<version>3.5.1</version>
46+
<executions>
47+
<execution>
48+
<goals>
49+
<goal>java</goal>
50+
</goals>
51+
<phase>prepare-package</phase>
52+
</execution>
53+
</executions>
54+
<configuration>
55+
<mainClass>org.openidentityplatform.openam.docs.services.Generator</mainClass>
56+
<arguments>
57+
<argument>${project.basedir}/../../openam-server-only/target/OpenAM-ServerOnly-${project.version}</argument>
58+
<argument>${build.outputDirectory}</argument>
59+
</arguments>
60+
</configuration>
61+
</plugin>
62+
</plugins>
63+
</build>
64+
65+
</project>
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2025 3A Systems LLC.
15+
*/
16+
17+
package org.openidentityplatform.openam.docs.services;
18+
19+
import org.apache.commons.text.TextStringBuilder;
20+
import org.w3c.dom.Document;
21+
import org.w3c.dom.Element;
22+
import org.w3c.dom.NodeList;
23+
import org.xml.sax.InputSource;
24+
25+
import javax.xml.parsers.DocumentBuilder;
26+
import javax.xml.parsers.DocumentBuilderFactory;
27+
import javax.xml.xpath.XPath;
28+
import javax.xml.xpath.XPathConstants;
29+
import javax.xml.xpath.XPathExpressionException;
30+
import javax.xml.xpath.XPathFactory;
31+
import java.io.InputStream;
32+
import java.io.StringReader;
33+
import java.nio.charset.StandardCharsets;
34+
import java.nio.file.Files;
35+
import java.nio.file.Path;
36+
import java.nio.file.Paths;
37+
import java.nio.file.StandardOpenOption;
38+
import java.util.ArrayList;
39+
import java.util.Arrays;
40+
import java.util.HashMap;
41+
import java.util.LinkedHashMap;
42+
import java.util.List;
43+
import java.util.Locale;
44+
import java.util.Map;
45+
import java.util.Objects;
46+
import java.util.Properties;
47+
import java.util.ResourceBundle;
48+
import java.util.regex.Matcher;
49+
import java.util.regex.Pattern;
50+
import java.util.stream.Collectors;
51+
52+
public class Generator {
53+
54+
final HtmlConverter htmlConverter;
55+
56+
final DocumentBuilder builder;
57+
58+
final XPath xpath;
59+
60+
final Locale BUNDLE_LOCALE = Locale.forLanguageTag("en");
61+
62+
final String AUTH_CLASS_NAME_REGEX = "^(iPlanetAMAuth|sunAMAuth)(.*?)Service$";
63+
64+
public Generator() throws Exception {
65+
htmlConverter = new HtmlConverter();
66+
67+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
68+
factory.setValidating(false);
69+
builder = factory.newDocumentBuilder();
70+
builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
71+
72+
xpath = XPathFactory.newInstance().newXPath();
73+
}
74+
75+
public static void main(String[] args) throws Exception {
76+
String serverPath = args[0];
77+
String targetPath = args[1];
78+
79+
Generator generator = new Generator();
80+
81+
generator.generateServicesDoc(serverPath, targetPath);
82+
83+
}
84+
85+
private void generateServicesDoc(String serverPath, String targetPath) throws Exception {
86+
87+
try(WarClassLoader cl = new WarClassLoader(serverPath)) {
88+
89+
Map<String, Document> xmlServicesMap = fetchServicesMapFromWar(cl);
90+
91+
Path dirPath = Paths.get(targetPath);
92+
Files.createDirectories(dirPath);
93+
94+
generateAuthModulesDoc(xmlServicesMap, cl, dirPath);
95+
96+
generateDataStoreDoc(xmlServicesMap, cl, dirPath);
97+
}
98+
99+
}
100+
101+
private Map<String, Document> fetchServicesMapFromWar(WarClassLoader cl) throws Exception {
102+
Properties serviceNamesProps = cl.loadProperties("serviceNames.properties");
103+
String serviceNamesStr = serviceNamesProps.getProperty("serviceNames");
104+
String[] serviceNames = serviceNamesStr.split("\\s+");
105+
List<Exception> errors = new ArrayList<>();
106+
final Pattern INVALID_FILENAME_CHARACTERS_PATTERN = Pattern.compile("[<>:\"/|?*]");
107+
List<Document> xmlServices = Arrays.stream(serviceNames).map(String::trim)
108+
.filter(s -> {
109+
Matcher m = INVALID_FILENAME_CHARACTERS_PATTERN.matcher(s);
110+
return !m.find();
111+
})
112+
.map(s -> {
113+
try(InputStream is = cl.getResourceAsStream(s)) {
114+
if (is == null) {
115+
return null;
116+
}
117+
return builder.parse(is);
118+
} catch (Exception e) {
119+
errors.add(e);
120+
return null;
121+
}
122+
})
123+
.filter(Objects::nonNull)
124+
.collect(Collectors.toList());
125+
126+
if (!errors.isEmpty()) {
127+
String errMessage = "Errors occurred while parsing service files:" + errors;
128+
System.out.println(errMessage);
129+
throw new Exception(errMessage);
130+
}
131+
132+
return xmlServices.stream().collect(Collectors.toMap(xmlService -> {
133+
Element service = (Element) xmlService.getElementsByTagName("Service").item(0);
134+
return service.getAttribute("name");
135+
}, xmlService -> xmlService, (existing, replacement) -> existing, LinkedHashMap::new));
136+
}
137+
138+
139+
private void generateAuthModulesDoc(Map<String, Document> xmlServicesMap, WarClassLoader cl, Path targetPath) throws Exception {
140+
141+
Document iPlanetAMAuthService = xmlServicesMap.get("iPlanetAMAuthService");
142+
143+
Map<String, String> authClassMap = getAuthClassMap(iPlanetAMAuthService);
144+
145+
TextStringBuilder asciidoc = new TextStringBuilder();
146+
asciidoc.appendln(":table-caption!:").appendNewLine();
147+
asciidoc.appendln("[#chap-auth-modules]");
148+
asciidoc.append("== ").appendln("Authentication Modules Reference").appendNewLine();
149+
150+
for(Map.Entry<String, Document> entry : xmlServicesMap.entrySet()) {
151+
Document xmlService = entry.getValue();
152+
NodeList schema = xmlService.getElementsByTagName("Schema");
153+
Element schemaElement = (Element) schema.item(0);
154+
if(!isAuthService(xmlService, authClassMap)) {
155+
continue;
156+
}
157+
158+
String bundleName = schemaElement.getAttribute("i18nFileName");
159+
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, BUNDLE_LOCALE, cl);
160+
generateModuleDoc(schemaElement, bundle, asciidoc, authClassMap);
161+
}
162+
Path filePath = targetPath.resolve("chap-auth-modules.adoc");
163+
164+
Files.write(filePath, asciidoc.toString().getBytes(StandardCharsets.UTF_8),
165+
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
166+
167+
System.out.println("File written to: " + filePath.toAbsolutePath());
168+
}
169+
170+
private Map<String, String> getAuthClassMap(Document iPlanetAMAuthService) throws XPathExpressionException {
171+
Map<String, String> authClassMap = new HashMap<>();
172+
173+
final String authModuleClassesXpath = "/ServicesConfiguration/Service/Schema/Global/AttributeSchema[1]/DefaultValues/Value";
174+
175+
NodeList authClassNames = (NodeList) xpath.evaluate(authModuleClassesXpath, iPlanetAMAuthService, XPathConstants.NODESET);
176+
177+
for (int i = 0; i < authClassNames.getLength(); i++) {
178+
String authClassName = authClassNames.item(i).getTextContent();
179+
String[] tokenized = authClassName.split("\\.");
180+
String classShortName = tokenized[tokenized.length - 1].toLowerCase();
181+
authClassMap.put(classShortName, authClassName);
182+
}
183+
return authClassMap;
184+
}
185+
186+
private boolean isAuthService(Document xmlService, Map<String, String> authClassMap) {
187+
188+
189+
NodeList schema = xmlService.getElementsByTagName("Schema");
190+
Element schemaElement = (Element) schema.item(0);
191+
String serviceHierarchy = schemaElement.getAttribute("serviceHierarchy");
192+
if(!serviceHierarchy.startsWith("/DSAMEConfig/authentication/")) {
193+
return false;
194+
}
195+
Element service = (Element) xmlService.getElementsByTagName("Service").item(0);
196+
String serviceName = service.getAttribute("name");
197+
198+
if (!serviceName.matches(AUTH_CLASS_NAME_REGEX)) {
199+
System.out.println(serviceName + " is not auth service");
200+
return false;
201+
}
202+
203+
String authServiceClassFullName = getAuthClassName(serviceName, authClassMap);
204+
if(authServiceClassFullName == null) {
205+
System.out.println(serviceName + " is not auth module");
206+
return false;
207+
}
208+
return true;
209+
}
210+
211+
private String getAuthClassName(String serviceName, Map<String, String> authClassMap) {
212+
213+
String authServiceClassName = serviceName.replaceAll(AUTH_CLASS_NAME_REGEX, "$2").toLowerCase();
214+
return authClassMap.get(authServiceClassName);
215+
}
216+
217+
private void generateModuleDoc(Element schemaElement, ResourceBundle bundle, TextStringBuilder asciidoc, Map<String, String> authClassMap) {
218+
219+
String moduleNameKey = schemaElement.getAttribute("i18nKey");
220+
String moduleName = bundle.getString(moduleNameKey);
221+
asciidoc.append(String.format("[#%s-module-ref]", moduleName.toLowerCase().replace(" ", "-"))).appendNewLine();
222+
asciidoc.append("=== ").append(moduleName)
223+
.appendNewLine().appendNewLine();
224+
225+
String serviceName = ((Element) schemaElement.getParentNode()).getAttribute("name");
226+
227+
String className = getAuthClassName(serviceName, authClassMap);
228+
String classLink = String.format("link:../apidocs/index.html?%s.html[%s, window=\\_blank]",
229+
className.replaceAll("\\.", "/"), className);
230+
asciidoc.appendln(String.format("Java class: `%s`", classLink))
231+
.appendNewLine();
232+
233+
asciidoc.appendln(String.format("`ssoadm` service name: `%s`", ((Element) schemaElement.getParentNode()).getAttribute("name")));
234+
asciidoc.appendNewLine();
235+
236+
Element orgElement = (Element) schemaElement.getElementsByTagName("Organization").item(0);
237+
NodeList attributes = orgElement.getElementsByTagName("AttributeSchema");
238+
for (int i = 0; i < attributes.getLength(); i++) {
239+
Element attrElement = (Element) attributes.item(i);
240+
printAttributeElement(bundle, asciidoc, attrElement);
241+
}
242+
System.out.printf("generated doc for %s module%n", moduleName);
243+
}
244+
245+
private void generateDataStoreDoc(Map<String, Document> xmlServicesMap, WarClassLoader cl, Path targetPath) throws Exception {
246+
TextStringBuilder asciidoc = new TextStringBuilder();
247+
asciidoc.appendln(":table-caption!:").appendNewLine();
248+
asciidoc.appendln("[#chap-user-data-stores]");
249+
asciidoc.append("== ").appendln("User Data Stores Reference").appendNewLine();
250+
251+
Document xmlService = xmlServicesMap.get("sunIdentityRepositoryService");
252+
NodeList schema = xmlService.getElementsByTagName("Schema");
253+
Element schemaElement = (Element) schema.item(0);
254+
String bundleName = schemaElement.getAttribute("i18nFileName");
255+
256+
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, BUNDLE_LOCALE, cl);
257+
258+
String expression = "/ServicesConfiguration/Service/Schema/Organization/SubSchema";
259+
NodeList dataStoreList = (NodeList) xpath.evaluate(expression, xmlService, XPathConstants.NODESET);
260+
261+
for (int i = 0; i < dataStoreList.getLength(); i++) {
262+
Element dataStore = (Element)dataStoreList.item(i);
263+
264+
String i18nKey = dataStore.getAttribute("i18nKey");
265+
String dataStoreName;
266+
if(i18nKey.trim().isEmpty()) {
267+
dataStoreName = dataStore.getAttribute("name");
268+
} else if(bundle.containsKey(i18nKey)) {
269+
dataStoreName = bundle.getString(i18nKey);
270+
} else {
271+
dataStoreName = i18nKey;
272+
}
273+
asciidoc.appendln(String.format("[#%s-datastore-ref]", dataStoreName.toLowerCase().replace(" ", "-")));
274+
asciidoc.append("=== ").append(dataStoreName)
275+
.appendNewLine().appendNewLine();
276+
277+
NodeList attributes = dataStore.getElementsByTagName("AttributeSchema");
278+
for (int j = 0; j < attributes.getLength(); j++) {
279+
Element attrElement = (Element) attributes.item(j);
280+
printAttributeElement(bundle, asciidoc, attrElement);
281+
}
282+
}
283+
284+
Path filePath = targetPath.resolve("chap-user-data-stores.adoc");
285+
286+
Files.write(filePath, asciidoc.toString().getBytes(StandardCharsets.UTF_8),
287+
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
288+
289+
System.out.println("File written to: " + filePath.toAbsolutePath());
290+
}
291+
292+
293+
294+
private void printAttributeElement(ResourceBundle bundle, TextStringBuilder asciidoc, Element attrElement) {
295+
String type = attrElement.getAttribute("type");
296+
if (type.equals("validator")) {
297+
return;
298+
}
299+
String i18Key = attrElement.getAttribute("i18nKey");
300+
if ("".equals(i18Key.trim())) {
301+
return;
302+
}
303+
String attrName = i18Key;
304+
if(bundle.containsKey(i18Key)) {
305+
attrName = bundle.getString(i18Key);
306+
}
307+
asciidoc.append(attrName).append("::").appendNewLine()
308+
.append("+").appendNewLine().appendln("--");
309+
if (bundle.containsKey(i18Key.concat(".help"))) {
310+
asciidoc.appendNewLine();
311+
String attrHelp = bundle.getString(i18Key.concat(".help"));
312+
htmlConverter.convertToAsciidoc(attrHelp, asciidoc);
313+
asciidoc.appendNewLine();
314+
}
315+
if (bundle.containsKey(i18Key.concat(".help.txt"))) {
316+
317+
String attrHelpTxt = bundle.getString(i18Key.concat(".help.txt"));
318+
asciidoc.appendNewLine();
319+
htmlConverter.convertToAsciidoc(attrHelpTxt, asciidoc);
320+
asciidoc.appendNewLine();
321+
}
322+
asciidoc.appendNewLine();
323+
asciidoc.appendln(String.format("`ssoadm` attribute: `%s`", attrElement.getAttribute("name")));
324+
asciidoc.appendNewLine();
325+
asciidoc.appendln("--");
326+
asciidoc.appendNewLine();
327+
328+
}
329+
}

0 commit comments

Comments
 (0)