Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import software.amazon.awssdk.codegen.internal.DocumentationUtils;
import software.amazon.awssdk.codegen.model.intermediate.DocumentationModel;
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
Expand Down Expand Up @@ -86,6 +87,14 @@ String getDocs() {
if (!crosslink.isEmpty()) {
docBuilder.see(crosslink);
}

String exampleMetaPath = "software/amazon/awssdk/codegen/example-meta.json";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a constant?

String codeExampleLink = DocumentationUtils.createLinkToCodeExample(model.getMetadata(),
opModel.getOperationName(),
exampleMetaPath);
if (!codeExampleLink.isEmpty()) {
docBuilder.see(codeExampleLink);
}
return docBuilder.build().replace("$", "&#36");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

import java.util.Collections;
import java.util.List;
import java.util.Map;
import software.amazon.awssdk.codegen.emitters.GeneratorTask;
import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams;
import software.amazon.awssdk.codegen.emitters.SimpleGeneratorTask;
import software.amazon.awssdk.codegen.internal.DocumentationUtils;
import software.amazon.awssdk.codegen.model.intermediate.Metadata;

/**
Expand All @@ -38,17 +40,100 @@ public final class PackageInfoGeneratorTasks extends BaseGeneratorTasks {
@Override
protected List<GeneratorTask> createTasks() throws Exception {
Metadata metadata = model.getMetadata();
String packageInfoContents =
String.format("/**%n"
+ " * %s%n"
+ "*/%n"
+ "package %s;",
metadata.getDocumentation(),
metadata.getFullClientPackageName());

String baseDocumentation = metadata.getDocumentation();

String codeExamples = getCodeExamples(metadata);

StringBuilder sb = new StringBuilder();
sb.append(String.format("/**%n * %s%n", baseDocumentation));
if (!codeExamples.isEmpty()) {
sb.append(String.format(" *%n * %s%n", codeExamples));
}
sb.append(String.format("*/%npackage %s;", metadata.getFullClientPackageName()));
String packageInfoContents = sb.toString();
return Collections.singletonList(new SimpleGeneratorTask(baseDirectory,
"package-info.java",
model.getFileHeader(),
() -> packageInfoContents));
}

String getCodeExamples(Metadata metadata) {
String exampleMetaPath = "software/amazon/awssdk/codegen/example-meta.json";
List<DocumentationUtils.ExampleData> examples =
DocumentationUtils.getServiceCodeExamples(metadata, exampleMetaPath);

if (examples.isEmpty()) {
return "";
}

String codeExamplesJavadoc = generateCodeExamplesJavadoc(examples);

StringBuilder result = new StringBuilder();
String[] lines = codeExamplesJavadoc.split("\n");
for (String line : lines) {
if (!line.trim().isEmpty()) {
result.append(line);
if (!line.equals(lines[lines.length - 1])) {
result.append(System.lineSeparator()).append(" * ");
}
}
}
Comment on lines +72 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not straighforward what this block is trying to do. Can we create a separate method with documentation?


return result.toString();
}


private String generateCodeExamplesJavadoc(List<DocumentationUtils.ExampleData> examples) {
Map<String, List<DocumentationUtils.ExampleData>> categorizedExamples = new java.util.LinkedHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's replace qualified name with import. new java.util.LinkedHashMap<>(); -> new LinkedHashMap<>())

Can be shortened to the following:

        Map<String, List<DocumentationUtils.ExampleData>> categorizedExamples = 
            examples.stream().collect(Collectors.groupingBy(DocumentationUtils.ExampleData::getCategory, 
                                                            LinkedHashMap::new, 
                                                            Collectors.toList()));

for (DocumentationUtils.ExampleData example : examples) {
categorizedExamples.computeIfAbsent(example.getCategory(), k -> new java.util.ArrayList<>()).add(example);
}

StringBuilder javadoc = new StringBuilder();
javadoc.append("<h2>Code Examples</h2>").append("\n");
javadoc.append("<p>The following code examples show how to use this service with the AWS SDK for Java v2:</p>")
.append("\n");

Map<String, String> categoryMapping = new java.util.LinkedHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we initialize this map in ctor?

categoryMapping.put("Hello", "Getting Started");
categoryMapping.put("Basics", "Basics");
categoryMapping.put("Api", "API Actions");
categoryMapping.put("Scenarios", "Scenarios");
categoryMapping.put("Serverless examples", "Serverless Examples");

for (Map.Entry<String, String> entry : categoryMapping.entrySet()) {
String category = entry.getKey();
String displayName = entry.getValue();
List<DocumentationUtils.ExampleData> categoryExamples = categorizedExamples.get(category);
if (categoryExamples != null && !categoryExamples.isEmpty()) {
appendCategorySection(javadoc, displayName, categoryExamples);
}
}

for (Map.Entry<String, List<DocumentationUtils.ExampleData>> entry : categorizedExamples.entrySet()) {
String category = entry.getKey();
if (!categoryMapping.containsKey(category)) {
List<DocumentationUtils.ExampleData> categoryExamples = entry.getValue();
if (!categoryExamples.isEmpty()) {
appendCategorySection(javadoc, category, categoryExamples);
}
}
}
Comment on lines +105 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not very straighforward what those two loop do. Can we create separat methods with descriptive method names (ideally with javadocs).


return javadoc.toString();
}

private void appendCategorySection(StringBuilder javadoc, String displayName,
List<DocumentationUtils.ExampleData> categoryExamples) {
javadoc.append("<h3>").append(displayName).append("</h3>").append("\n");
javadoc.append("<ul>").append("\n");

for (DocumentationUtils.ExampleData example : categoryExamples) {
javadoc.append("<li><a href=\"").append(example.getUrl()).append("\" target=\"_top\">")
.append(example.getTitle()).append("</a></li>").append("\n");
}
javadoc.append("</ul>").append("\n");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@
import static software.amazon.awssdk.codegen.model.intermediate.ShapeType.Request;
import static software.amazon.awssdk.codegen.model.intermediate.ShapeType.Response;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import software.amazon.awssdk.codegen.model.intermediate.Metadata;
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
import software.amazon.awssdk.utils.Logger;

public final class DocumentationUtils {

Expand Down Expand Up @@ -54,6 +64,11 @@ public final class DocumentationUtils {
"iot", "data.iot", "machinelearning", "rekognition", "s3", "sdb", "swf"
));
private static final Pattern COMMENT_DELIMITER = Pattern.compile("\\*\\/");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private static final Logger log = Logger.loggerFor(DocumentationUtils.class);
private static Map<String, JsonNode> serviceNodeCache;
private static Map<String, String> normalizedServiceKeyMap;
Comment on lines +70 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem thread safe. Suggesting creating a class instead of using static util methods for the new functionalities.


private DocumentationUtils() {
}
Expand Down Expand Up @@ -140,6 +155,39 @@ public static String createLinkToServiceDocumentation(Metadata metadata, ShapeMo
: "";
}

/**
* Create a link to a code example for the given operation.
*
* @param metadata the service metadata containing service name information
* @param operationName the name of the operation to find an example for
* @param exampleMetaPath the path to the example metadata JSON file
* @return a '@see also' HTML link to the code example, or empty string if no example found
*/
public static String createLinkToCodeExample(Metadata metadata, String operationName, String exampleMetaPath) {
try {
String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT);
Map<String, String> normalizedMap = getNormalizedServiceKeyMap(exampleMetaPath);
String actualServiceKey = normalizedMap.get(normalizedServiceName);

if (actualServiceKey != null) {
String targetExampleId = actualServiceKey + "_" + operationName;
JsonNode serviceNode = getServiceNode(actualServiceKey, exampleMetaPath);

if (serviceNode != null) {
String url = findOperationUrl(serviceNode, targetExampleId);
if (url != null) {
return String.format("<a href=\"%s\" target=\"_top\">Code Example</a>", url);
}
}
}

return "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just use null to indicate there's no example, so here, we'd return Optional.empty()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we're not using null here. we're returning an empty string ("") right, so based on the check we have in OperationDocProvider.java, it wont show example when its empty string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was suggesting using null to denote lack of example instead of empty string

} catch (Exception e) {
log.debug(() -> "Failed to create code example link for " + metadata.getServiceName() + "." + operationName, e);
return "";
}
}

public static String removeFromEnd(String string, String stringToRemove) {
return string.endsWith(stringToRemove) ? string.substring(0, string.length() - stringToRemove.length()) : string;
}
Expand Down Expand Up @@ -177,4 +225,174 @@ public static String defaultFluentReturn() {
public static String defaultExistenceCheck() {
return DEFAULT_EXISTENCE_CHECK;
}


/**
* Gets the cached service node
*/
private static JsonNode getServiceNode(String serviceKey, String exampleMetaPath) {
if (serviceNodeCache == null) {
buildServiceCache(exampleMetaPath);
}
return serviceNodeCache != null ? serviceNodeCache.get(serviceKey) : null;
}

/**
* Gets the cached normalized service key map for service name matching.
*/
private static Map<String, String> getNormalizedServiceKeyMap(String exampleMetaPath) {
if (normalizedServiceKeyMap == null) {
buildServiceCache(exampleMetaPath);
}
return normalizedServiceKeyMap != null ? normalizedServiceKeyMap : new HashMap<>();
}

/**
* Builds the service node cache and normalized service key mapping from the specified example metadata file.
*/
private static void buildServiceCache(String exampleMetaPath) {
Map<String, JsonNode> nodeCache = new HashMap<>();
Map<String, String> normalizedMap = new HashMap<>();

try (InputStream inputStream = DocumentationUtils.class.getClassLoader()
.getResourceAsStream(exampleMetaPath)) {

if (inputStream == null) {
log.debug(() -> exampleMetaPath + " not found in classpath");
} else {
JsonNode root = OBJECT_MAPPER.readTree(inputStream);
JsonNode servicesNode = root.get("services");

if (servicesNode != null) {
servicesNode.fieldNames().forEachRemaining(serviceKey -> {
buildNormalizedMapping(serviceKey, normalizedMap);
nodeCache.put(serviceKey, servicesNode.get(serviceKey));
});
}
}

} catch (IOException e) {
log.warn(() -> "Failed to load " + exampleMetaPath, e);
}

serviceNodeCache = nodeCache;
normalizedServiceKeyMap = normalizedMap;
}

/**
* Builds normalized mapping for a service key (e.g., "medical-imaging" -> "medicalimaging").
*/
private static void buildNormalizedMapping(String serviceKey, Map<String, String> normalizedMap) {
String normalizedKey = serviceKey.replace("-", "").toLowerCase(Locale.ROOT);
normalizedMap.put(normalizedKey, serviceKey);
}

/**
* Finds the URL for a specific operation ID within a service node.
*/
private static String findOperationUrl(JsonNode serviceNode, String targetExampleId) {
JsonNode examplesNode = serviceNode.get("examples");
if (examplesNode != null && examplesNode.isArray()) {
for (JsonNode example : examplesNode) {
JsonNode idNode = example.get("id");
JsonNode urlNode = example.get("url");

if (idNode != null && urlNode != null) {
String id = idNode.asText();
if (targetExampleId.equals(id)) {
return urlNode.asText();
}
}
}
}
return null;
}

/**
* Gets all code examples for a specific service.
*
* @param metadata the service metadata containing service name information
* @param exampleMetaPath the path to the example metadata JSON file
* @return a list of examples for the service
*/
public static List<ExampleData> getServiceCodeExamples(Metadata metadata, String exampleMetaPath) {
List<ExampleData> examples = new ArrayList<>();

try {
String normalizedServiceName = metadata.getServiceName().toLowerCase(Locale.ROOT);
Map<String, String> normalizedMap = getNormalizedServiceKeyMap(exampleMetaPath);
String actualServiceKey = normalizedMap.get(normalizedServiceName);

if (actualServiceKey != null) {
JsonNode serviceNode = getServiceNode(actualServiceKey, exampleMetaPath);
if (serviceNode != null) {
examples = parseServiceExamples(serviceNode);
}
}
} catch (Exception e) {
log.debug(() -> "Failed to load examples for " + metadata.getServiceName(), e);
}

return examples;
}

/**
* Parses examples from a service node in the JSON.
*/
private static List<ExampleData> parseServiceExamples(JsonNode serviceNode) {
List<ExampleData> examples = new ArrayList<>();
JsonNode examplesNode = serviceNode.get("examples");

if (examplesNode != null && examplesNode.isArray()) {
for (JsonNode example : examplesNode) {
JsonNode idNode = example.get("id");
JsonNode titleNode = example.get("title");
JsonNode categoryNode = example.get("category");
JsonNode urlNode = example.get("url");

if (idNode != null && titleNode != null && urlNode != null) {
String id = idNode.asText();
String title = titleNode.asText();
String category = categoryNode != null ? categoryNode.asText() : "Api";
String url = urlNode.asText();

if (!id.isEmpty() && !title.isEmpty() && !url.isEmpty()) {
examples.add(new ExampleData(id, title, category, url));
}
}
}
}

return examples;
}

public static final class ExampleData {
private final String id;
private final String title;
private final String category;
private final String url;

public ExampleData(String id, String title, String category, String url) {
this.id = id;
this.title = title;
this.category = category;
this.url = url;
}

public String getId() {
return id;
}

public String getTitle() {
return title;
}

public String getCategory() {
return category;
}

public String getUrl() {
return url;
}
}
}
Loading
Loading