001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind.codegen; 007 008import java.net.URI; 009import java.net.URISyntaxException; 010import java.util.ArrayList; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Set; 015 016import dev.metaschema.core.util.ObjectUtils; 017import edu.umd.cs.findbugs.annotations.NonNull; 018 019/** 020 * A variety of utility methods for normalizing Java class related names. 021 */ 022public final class ClassUtils { 023 private static final Map<String, String> JAVA_NAME_MAPPER = Map.ofEntries( 024 Map.entry("Class", "Clazz")); 025 026 private ClassUtils() { 027 // disable construction 028 } 029 030 /** 031 * Transforms the provided name into a string suitable for use as a Java 032 * property name. 033 * 034 * @param name 035 * the name of an information element definition 036 * @return a Java property name 037 */ 038 @SuppressWarnings("null") 039 @NonNull 040 public static String toPropertyName(@NonNull String name) { 041 String property = upperCamelCase(name); 042 return JAVA_NAME_MAPPER.getOrDefault(property, property); 043 } 044 045 /** 046 * Transforms the provided name into a string suitable for use as a Java 047 * variable name. 048 * 049 * @param name 050 * the name of an information element definition 051 * @return a Java variable name 052 */ 053 @NonNull 054 public static String toVariableName(@NonNull String name) { 055 return lowerCamelCase(name); 056 } 057 058 /** 059 * Transforms the provided name into a string suitable for use as a Java class 060 * name. 061 * 062 * @param name 063 * the name of an information element definition 064 * @return a Java variable name 065 */ 066 @NonNull 067 public static String toClassName(@NonNull String name) { 068 return upperCamelCase(name); 069 } 070 071 /** 072 * Transforms the provided name into a string suitable for use as a Java package 073 * name. 074 * 075 * @param namespace 076 * a namespace URI to convert to a package name 077 * @return a Java package name 078 */ 079 @NonNull 080 public static String toPackageName(@NonNull String namespace) { 081 return getPackageFromNamespace(namespace); 082 } 083 084 /** 085 * Converts a string to UpperCamelCase. 086 * <p> 087 * Splits on common separators (hyphen, underscore, period, whitespace) and 088 * capitalizes the first letter of each word. 089 * 090 * @param name 091 * the name to convert 092 * @return the name in UpperCamelCase 093 */ 094 @NonNull 095 private static String upperCamelCase(@NonNull String name) { 096 List<String> words = splitWords(name); 097 StringBuilder sb = new StringBuilder(); 098 for (String word : words) { 099 if (!word.isEmpty()) { 100 sb.append(Character.toUpperCase(word.charAt(0))); 101 if (word.length() > 1) { 102 sb.append(word.substring(1).toLowerCase(Locale.ROOT)); 103 } 104 } 105 } 106 return sb.length() > 0 ? ObjectUtils.notNull(sb.toString()) : name; 107 } 108 109 /** 110 * Converts a string to lowerCamelCase. 111 * <p> 112 * Splits on common separators (hyphen, underscore, period, whitespace) and 113 * capitalizes the first letter of each word except the first. 114 * 115 * @param name 116 * the name to convert 117 * @return the name in lowerCamelCase 118 */ 119 @NonNull 120 private static String lowerCamelCase(@NonNull String name) { 121 List<String> words = splitWords(name); 122 StringBuilder sb = new StringBuilder(); 123 boolean first = true; 124 for (String word : words) { 125 if (!word.isEmpty()) { 126 if (first) { 127 sb.append(word.toLowerCase(Locale.ROOT)); 128 first = false; 129 } else { 130 sb.append(Character.toUpperCase(word.charAt(0))); 131 if (word.length() > 1) { 132 sb.append(word.substring(1).toLowerCase(Locale.ROOT)); 133 } 134 } 135 } 136 } 137 return sb.length() > 0 ? ObjectUtils.notNull(sb.toString()) : name; 138 } 139 140 /** 141 * Splits a name into words based on common separators. 142 * <p> 143 * Handles hyphens, underscores, periods, and whitespace as word separators. 144 * Also splits on camelCase boundaries. 145 * 146 * @param name 147 * the name to split 148 * @return a list of words 149 */ 150 @NonNull 151 private static List<String> splitWords(@NonNull String name) { 152 List<String> words = new ArrayList<>(); 153 StringBuilder currentWord = new StringBuilder(); 154 155 for (int i = 0; i < name.length(); i++) { 156 char ch = name.charAt(i); 157 if (ch == '-' || ch == '_' || ch == '.' || Character.isWhitespace(ch)) { 158 // Separator found, end current word 159 if (currentWord.length() > 0) { 160 words.add(currentWord.toString()); 161 currentWord = new StringBuilder(); 162 } 163 } else if (Character.isUpperCase(ch) && currentWord.length() > 0 164 && Character.isLowerCase(currentWord.charAt(currentWord.length() - 1))) { 165 // CamelCase boundary 166 words.add(currentWord.toString()); 167 currentWord = new StringBuilder(); 168 currentWord.append(ch); 169 } else { 170 currentWord.append(ch); 171 } 172 } 173 if (currentWord.length() > 0) { 174 words.add(currentWord.toString()); 175 } 176 return words; 177 } 178 179 /** 180 * Converts a namespace URI to a Java package name. 181 * <p> 182 * Based on the standard URI to package name algorithm: 183 * <ol> 184 * <li>Extract the host and reverse it (e.g., "csrc.nist.gov" becomes 185 * "gov.nist.csrc")</li> 186 * <li>Append the path segments, replacing separators with dots</li> 187 * <li>Remove or escape invalid package name characters</li> 188 * </ol> 189 * 190 * @param namespace 191 * the namespace URI 192 * @return a valid Java package name 193 */ 194 @NonNull 195 private static String getPackageFromNamespace(@NonNull String namespace) { 196 try { 197 URI uri = new URI(namespace); 198 StringBuilder sb = new StringBuilder(); 199 200 // Process the host (reverse the domain name) 201 String host = uri.getHost(); 202 if (host != null && !host.isEmpty()) { 203 String[] hostParts = host.split("\\."); 204 for (int i = hostParts.length - 1; i >= 0; i--) { 205 if (sb.length() > 0) { 206 sb.append('.'); 207 } 208 sb.append(normalizePackagePart(ObjectUtils.notNull(hostParts[i]))); 209 } 210 } 211 212 // Process the path 213 String path = uri.getPath(); 214 if (path != null && !path.isEmpty()) { 215 String[] pathParts = path.split("/"); 216 for (String part : pathParts) { 217 if (!part.isEmpty()) { 218 if (sb.length() > 0) { 219 sb.append('.'); 220 } 221 sb.append(normalizePackagePart(part)); 222 } 223 } 224 } 225 226 return sb.length() > 0 ? ObjectUtils.notNull(sb.toString()) : "generated"; 227 } catch (@SuppressWarnings("unused") URISyntaxException ex) { 228 // Fall back to a simple approach if URI parsing fails 229 return normalizePackagePart(ObjectUtils.notNull(namespace.replaceAll("[^a-zA-Z0-9]", "_"))); 230 } 231 } 232 233 /** 234 * Normalizes a string to be a valid Java package name part. 235 * <p> 236 * Replaces or removes invalid characters and ensures the result is a valid 237 * identifier. 238 * 239 * @param part 240 * the package name part to normalize 241 * @return a valid package name part 242 */ 243 @NonNull 244 private static String normalizePackagePart(@NonNull String part) { 245 if (part.isEmpty()) { 246 return "_"; 247 } 248 249 StringBuilder sb = new StringBuilder(); 250 for (int i = 0; i < part.length(); i++) { 251 char ch = part.charAt(i); 252 if (i == 0) { 253 if (Character.isJavaIdentifierStart(ch)) { 254 sb.append(Character.toLowerCase(ch)); 255 } else if (Character.isDigit(ch)) { 256 sb.append('_'); 257 sb.append(ch); 258 } else { 259 sb.append('_'); 260 } 261 } else { 262 if (Character.isJavaIdentifierPart(ch)) { 263 sb.append(Character.toLowerCase(ch)); 264 } else if (ch == '-' || ch == '.') { 265 // Common separators become underscores 266 sb.append('_'); 267 } 268 // else skip invalid characters 269 } 270 } 271 272 String result = ObjectUtils.notNull(sb.toString()); 273 // Handle Java reserved words 274 if (isJavaReservedWord(result)) { 275 return "_" + result; 276 } 277 return result.isEmpty() ? "_" : result; 278 } 279 280 /** 281 * Java reserved words and contextual keywords that cannot be used as 282 * identifiers. 283 * <p> 284 * Includes both traditional reserved words and contextual keywords introduced 285 * in later Java versions (var, yield, record, sealed, permits) for forward 286 * compatibility. 287 */ 288 private static final Set<String> JAVA_RESERVED_WORDS = Set.of( 289 "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", 290 "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", 291 "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", 292 "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", 293 "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", 294 "volatile", "while", "true", "false", "null", 295 // Contextual keywords (Java 9+) 296 "module", "var", "yield", "record", "sealed", "permits"); 297 298 /** 299 * Checks if the given string is a Java reserved word. 300 * 301 * @param word 302 * the word to check 303 * @return {@code true} if the word is a Java reserved word 304 */ 305 private static boolean isJavaReservedWord(@NonNull String word) { 306 return JAVA_RESERVED_WORDS.contains(word); 307 } 308}