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