/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.internal.spec; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jooby.Status; import org.slf4j.Logger; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.comments.Comment; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.StringLiteralExpr; import com.github.javaparser.ast.stmt.ExpressionStmt; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import com.google.common.base.Splitter; public class DocCollector extends VoidVisitorAdapter<Context> { private static final Pattern SPLITTER = Pattern.compile("\\s+\\*"); private static final String RETURNS = "@return"; private static final String PARAM = "@param"; private static final String THROWS = "@throws"; private static final Pattern CODE = Pattern .compile("<code>\\s*(\\d+)\\s*(=\\s*([^<]+))?\\s*</code>"); private Map<String, Object> doc = new HashMap<>(); private Logger log; public DocCollector(final Logger log) { this.log = log; } public Map<String, Object> accept(final Node node, final String method, final Context ctx) { try { node.accept(this, ctx); if (!doc.containsKey("@statusCodes")) { Map<Object, Object> codes = new LinkedHashMap<>(); Status status = Status.OK; if ("DELETE".equals(method)) { status = Status.NO_CONTENT; } codes.put(status.value(), status.reason()); doc.put("@statusCodes", codes); } } catch (Exception x) { log.debug("Doc collector resulted in exception", x); } return doc; } @Override public void visit(final MethodCallExpr n, final Context ctx) { Map<String, Object> doc = doc(n, ctx); if (doc != null) { this.doc.putAll(doc); this.doc.put("@summary", summary(n, ctx)); } } @Override public void visit(final MethodDeclaration m, final Context ctx) { ClassOrInterfaceDeclaration clazz = clazz(m); Map<String, Object> doc = doc(m, ctx); if (doc != null) { this.doc.putAll(doc); this.doc.put("@summary", doc(clazz, ctx).get("@text")); } } private ClassOrInterfaceDeclaration clazz(final MethodDeclaration method) { Node node = method.getParentNode(); while (!(node instanceof ClassOrInterfaceDeclaration)) { node = node.getParentNode(); } return (ClassOrInterfaceDeclaration) node; } private Map<String, Object> doc(final MethodCallExpr expr, final Context ctx) { if (expr.getScope() == null) { return doc(expr.getParentNode(), ctx); } else { List<Expression> args = expr.getArgs(); if (args.size() > 0) { return doc(args.get(0), ctx); } } return null; } private Map<String, Object> doc(final Node node, final Context ctx) { Map<String, Object> hash = new HashMap<>(); Comment comment = node.getComment(); if (comment != null) { String doc = comment.getContent().trim(); String clean = Splitter.on(SPLITTER) .trimResults() .omitEmptyStrings() .splitToList(doc) .stream() .map(l -> l.charAt(0) == '*' ? l.substring(1).trim() : l) .collect(Collectors.joining("\n")); int at = clean.indexOf('@'); String text = at == 0 ? null : (at > 0 ? clean.substring(0, at) : clean).trim(); Map<Integer, String> codes = Collections.emptyMap(); String tail = clean.substring(Math.max(0, at)); // params params(tail, hash::put); // returns String returnText = returnText(tail); codes = new LinkedHashMap<>(); if (returnText != null) { hash.put("@return", returnText); Matcher cmatcher = CODE.matcher(returnText); while (cmatcher.find()) { Status status = Status.valueOf(Integer.parseInt(cmatcher.group(1).trim())); String message = Optional.ofNullable(cmatcher.group(3)).orElse(status.reason()).trim(); codes.put(status.value(), message); } TypeFromDoc.parse(node, ctx, returnText).ifPresent(type -> hash.put("@type", type)); } hash.put("@statusCodes", codes); hash.put("@text", text); } return hash; } private void params(final String text, final BiConsumer<String, String> callback) { int at = text.indexOf(PARAM); while (at != -1) { int start = at + PARAM.length(); int end = firstOf(text, start, PARAM, RETURNS, THROWS); String raw = text.substring(start, end).trim(); int space = raw.indexOf(" "); if (space != -1) { String name = raw.substring(0, space).trim(); String desc = raw.substring(space).trim(); callback.accept(name, desc); } at = text.indexOf(PARAM, end); } } private int firstOf(final String text, final int start, final String... tokens) { for (String token : tokens) { int pos = text.indexOf(token, start); if (pos != -1) { return pos; } } return text.length(); } private String returnText(final String doc) { int retIdx = doc.indexOf(RETURNS); if (retIdx >= 0) { String ret = doc.substring(retIdx + RETURNS.length()).trim(); ret = Splitter.on(Pattern.compile("[^\\{]@[a-zA-Z]")) .trimResults().omitEmptyStrings() .splitToList(ret) .stream() .findFirst() .get(); return ret; } return null; } private String summary(final MethodCallExpr it, final Context ctx) { return usePath(it) .map(use -> { Node node = use; while (!(node instanceof ExpressionStmt)) { node = node.getParentNode(); } return node == null ? null : (String) doc(node, ctx).get("@text"); }).orElse(null); } private Optional<Node> usePath(final MethodCallExpr it) { MethodCallExpr expr = AST.scopeOf(it); String name = expr.getName(); List<Expression> args = expr.getArgs(); if (name.equals("use") && args.size() == 1 && args.get(0) instanceof StringLiteralExpr) { return Optional.of(expr); } return Optional.empty(); } }