001/*
002 * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclets.toolkit.builders;
027
028import static org.jdrupes.mdoclet.internal.doclets.toolkit.util.VisibleMemberTable.Kind.*;
029
030import java.util.Collection;
031import java.util.Comparator;
032import java.util.EnumMap;
033import java.util.HashMap;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037import java.util.Objects;
038import java.util.Optional;
039import java.util.SortedSet;
040import java.util.TreeSet;
041import javax.lang.model.element.Element;
042import javax.lang.model.element.ExecutableElement;
043import javax.lang.model.element.TypeElement;
044import javax.lang.model.element.VariableElement;
045import javax.lang.model.util.ElementFilter;
046import javax.tools.Diagnostic;
047
048import org.jdrupes.mdoclet.internal.doclets.toolkit.BaseConfiguration;
049import org.jdrupes.mdoclet.internal.doclets.toolkit.ClassWriter;
050import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.MemberSummaryWriter;
052import org.jdrupes.mdoclet.internal.doclets.toolkit.WriterFactory;
053import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFinder;
054import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils;
055import org.jdrupes.mdoclet.internal.doclets.toolkit.util.VisibleMemberTable;
056import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFinder.Result;
057
058import com.sun.source.doctree.DocCommentTree;
059import com.sun.source.doctree.DocTree;
060
061/**
062 * Builds the member summary.
063 * There are two anonymous subtype variants of this builder, created
064 * in the {@link #getInstance} methods. One is for general types;
065 * the other is for annotation types.
066 */
067public abstract class MemberSummaryBuilder extends AbstractMemberBuilder {
068
069    /*
070     * Comparator used to sort the members in the summary.
071     */
072    private final Comparator<Element> comparator;
073
074    /**
075     * The member summary writers for the given class.
076     */
077    private final EnumMap<VisibleMemberTable.Kind,
078            MemberSummaryWriter> memberSummaryWriters;
079
080    final PropertyHelper pHelper;
081
082    /**
083     * Construct a new MemberSummaryBuilder.
084     *
085     * @param context       the build context.
086     * @param typeElement   the type element.
087     */
088    private MemberSummaryBuilder(Context context, TypeElement typeElement) {
089        super(context, typeElement);
090        memberSummaryWriters = new EnumMap<>(VisibleMemberTable.Kind.class);
091        comparator = utils.comparators.makeIndexElementComparator();
092        pHelper = new PropertyHelper(this);
093    }
094
095    /**
096     * Construct a new MemberSummaryBuilder for a general type.
097     *
098     * @param classWriter   the writer for the class whose members are being
099     *                      summarized.
100     * @param context       the build context.
101     * @return              the instance
102     */
103    public static MemberSummaryBuilder getInstance(
104            ClassWriter classWriter, Context context) {
105        MemberSummaryBuilder builder
106            = new MemberSummaryBuilder(context, classWriter.getTypeElement()) {
107                @Override
108                public void build(Content target) {
109                    buildPropertiesSummary(target);
110                    buildNestedClassesSummary(target);
111                    buildEnumConstantsSummary(target);
112                    buildAnnotationTypeRequiredMemberSummary(target);
113                    buildAnnotationTypeOptionalMemberSummary(target);
114                    buildFieldsSummary(target);
115                    buildConstructorsSummary(target);
116                    buildMethodsSummary(target);
117                }
118
119                @Override
120                public boolean hasMembersToDocument() {
121                    return visibleMemberTable.hasVisibleMembers();
122                }
123            };
124        WriterFactory wf = context.configuration.getWriterFactory();
125        for (VisibleMemberTable.Kind kind : VisibleMemberTable.Kind.values()) {
126            MemberSummaryWriter msw
127                = builder.getVisibleMemberTable().hasVisibleMembers(kind)
128                    ? wf.getMemberSummaryWriter(classWriter, kind)
129                    : null;
130            builder.memberSummaryWriters.put(kind, msw);
131        }
132        return builder;
133    }
134
135    /**
136     * Return the specified visible member map.
137     *
138     * @return the specified visible member map.
139     * @throws ArrayIndexOutOfBoundsException when the type is invalid.
140     * @see VisibleMemberTable
141     */
142    public VisibleMemberTable getVisibleMemberTable() {
143        return visibleMemberTable;
144    }
145
146    /**.
147     * Return the specified member summary writer.
148     *
149     * @param kind the kind of member summary writer to return.
150     * @return the specified member summary writer.
151     * @throws ArrayIndexOutOfBoundsException when the type is invalid.
152     * @see VisibleMemberTable
153     */
154    public MemberSummaryWriter
155            getMemberSummaryWriter(VisibleMemberTable.Kind kind) {
156        return memberSummaryWriters.get(kind);
157    }
158
159    /**
160     * Returns a list of methods that will be documented for the given class.
161     * This information can be used for doclet specific documentation
162     * generation.
163     *
164     * @param kind the kind of elements to return.
165     * @return a list of methods that will be documented.
166     * @see VisibleMemberTable
167     */
168    public SortedSet<Element> members(VisibleMemberTable.Kind kind) {
169        TreeSet<Element> out = new TreeSet<>(comparator);
170        out.addAll(getVisibleMembers(kind));
171        return out;
172    }
173
174    /**
175     * Builds the summary for any optional members of an annotation type.
176     *
177     * @param summariesList the list of summaries to which the summary will be added
178     */
179    protected void
180            buildAnnotationTypeOptionalMemberSummary(Content summariesList) {
181        MemberSummaryWriter writer
182            = memberSummaryWriters.get(ANNOTATION_TYPE_MEMBER_OPTIONAL);
183        addSummary(writer, ANNOTATION_TYPE_MEMBER_OPTIONAL, false,
184            summariesList);
185    }
186
187    /**
188     * Builds the summary for any required members of an annotation type.
189     *
190     * @param summariesList the list of summaries to which the summary will be added
191     */
192    protected void
193            buildAnnotationTypeRequiredMemberSummary(Content summariesList) {
194        MemberSummaryWriter writer
195            = memberSummaryWriters.get(ANNOTATION_TYPE_MEMBER_REQUIRED);
196        addSummary(writer, ANNOTATION_TYPE_MEMBER_REQUIRED, false,
197            summariesList);
198    }
199
200    /**
201     * Builds the summary for any enum constants of an enum type.
202     *
203     * @param summariesList the list of summaries to which the summary will be added
204     */
205    protected void buildEnumConstantsSummary(Content summariesList) {
206        MemberSummaryWriter writer = memberSummaryWriters.get(ENUM_CONSTANTS);
207        addSummary(writer, ENUM_CONSTANTS, false, summariesList);
208    }
209
210    /**
211     * Builds the summary for any fields.
212     *
213     * @param summariesList the list of summaries to which the summary will be added
214     */
215    protected void buildFieldsSummary(Content summariesList) {
216        MemberSummaryWriter writer = memberSummaryWriters.get(FIELDS);
217        addSummary(writer, FIELDS, true, summariesList);
218    }
219
220    /**
221     * Builds the summary for any properties.
222     *
223     * @param summariesList the list of summaries to which the summary will be added
224     */
225    protected void buildPropertiesSummary(Content summariesList) {
226        MemberSummaryWriter writer = memberSummaryWriters.get(PROPERTIES);
227        addSummary(writer, PROPERTIES, true, summariesList);
228    }
229
230    /**
231     * Builds the summary for any nested classes.
232     *
233     * @param summariesList the list of summaries to which the summary will be added
234     */
235    protected void buildNestedClassesSummary(Content summariesList) {
236        MemberSummaryWriter writer = memberSummaryWriters.get(NESTED_CLASSES);
237        addSummary(writer, NESTED_CLASSES, true, summariesList);
238    }
239
240    /**
241     * Builds the summary for any methods.
242     *
243     * @param summariesList the content to which the documentation will be added
244     */
245    protected void buildMethodsSummary(Content summariesList) {
246        MemberSummaryWriter writer = memberSummaryWriters.get(METHODS);
247        addSummary(writer, METHODS, true, summariesList);
248    }
249
250    /**
251     * Builds the summary for any constructors.
252     *
253     * @param summariesList the content to which the documentation will be added
254     */
255    protected void buildConstructorsSummary(Content summariesList) {
256        MemberSummaryWriter writer = memberSummaryWriters.get(CONSTRUCTORS);
257        addSummary(writer, CONSTRUCTORS, false, summariesList);
258    }
259
260    /**
261     * Build the member summary for the given members.
262     *
263     * @param writer the summary writer to write the output.
264     * @param kind the kind of  members to summarize.
265     * @param summaryTreeList the list of contents to which the documentation will be added
266     */
267    private void buildSummary(MemberSummaryWriter writer,
268            VisibleMemberTable.Kind kind, LinkedList<Content> summaryTreeList) {
269        SortedSet<? extends Element> members
270            = asSortedSet(getVisibleMembers(kind));
271        if (!members.isEmpty()) {
272            for (Element member : members) {
273                final Element property = pHelper.getPropertyElement(member);
274                if (property != null
275                    && member instanceof ExecutableElement ee) {
276                    configuration.cmtUtils.updatePropertyMethodComment(ee,
277                        property);
278                }
279                if (utils.isMethod(member)) {
280                    var docFinder = utils.docFinder();
281                    Optional<List<? extends DocTree>> r
282                        = docFinder.search((ExecutableElement) member, (m -> {
283                            var firstSentenceTrees
284                                = utils.getFirstSentenceTrees(m);
285                            Optional<List<? extends DocTree>> optional
286                                = firstSentenceTrees.isEmpty()
287                                    ? Optional.empty()
288                                    : Optional.of(firstSentenceTrees);
289                            return Result.fromOptional(optional);
290                        })).toOptional();
291                    // The fact that we use `member` for possibly unrelated tags
292                    // is suspicious
293                    writer.addMemberSummary(typeElement, member,
294                        r.orElse(List.of()));
295                } else {
296                    writer.addMemberSummary(typeElement, member,
297                        utils.getFirstSentenceTrees(member));
298                }
299            }
300            summaryTreeList.add(writer.getSummaryTable(typeElement));
301        }
302    }
303
304    /**
305     * Build the inherited member summary for the given methods.
306     *
307     * @param writer the writer for this member summary.
308     * @param kind the kind of members to document.
309     * @param targets the list of contents to which the documentation will be added
310     */
311    private void buildInheritedSummary(MemberSummaryWriter writer,
312            VisibleMemberTable.Kind kind, LinkedList<Content> targets) {
313        VisibleMemberTable visibleMemberTable = getVisibleMemberTable();
314        SortedSet<? extends Element> inheritedMembersFromMap
315            = asSortedSet(visibleMemberTable.getAllVisibleMembers(kind));
316
317        for (TypeElement inheritedClass : visibleMemberTable
318            .getVisibleTypeElements()) {
319            if (!(utils.isPublic(inheritedClass)
320                || utils.isLinkable(inheritedClass))) {
321                continue;
322            }
323            if (Objects.equals(inheritedClass, typeElement)) {
324                continue;
325            }
326            if (utils.hasHiddenTag(inheritedClass)) {
327                continue;
328            }
329
330            List<? extends Element> members = inheritedMembersFromMap.stream()
331                .filter(e -> Objects.equals(utils.getEnclosingTypeElement(e),
332                    inheritedClass))
333                .toList();
334
335            if (!members.isEmpty()) {
336                SortedSet<Element> inheritedMembers = new TreeSet<>(comparator);
337                inheritedMembers.addAll(members);
338                Content inheritedHeader
339                    = writer.getInheritedSummaryHeader(inheritedClass);
340                Content links = writer.getInheritedSummaryLinks();
341                addSummaryFootNote(inheritedClass, inheritedMembers, links,
342                    writer);
343                inheritedHeader.add(links);
344                targets.add(inheritedHeader);
345            }
346        }
347    }
348
349    private void addSummaryFootNote(TypeElement inheritedClass,
350            Iterable<Element> inheritedMembers,
351            Content links, MemberSummaryWriter writer) {
352        boolean isFirst = true;
353        for (var iterator = inheritedMembers.iterator(); iterator.hasNext();) {
354            var member = iterator.next();
355            TypeElement t = utils.isUndocumentedEnclosure(inheritedClass)
356                ? typeElement
357                : inheritedClass;
358            writer.addInheritedMemberSummary(t, member, isFirst,
359                !iterator.hasNext(), links);
360            isFirst = false;
361        }
362    }
363
364    /**
365     * Adds the summary for the documentation.
366     *
367     * @param writer               the writer for this member summary
368     * @param kind                 the kind of members to document
369     * @param showInheritedSummary true if a summary of any inherited elements should be documented
370     * @param summariesList        the list of summaries to which the summary will be added
371     */
372    private void addSummary(MemberSummaryWriter writer,
373            VisibleMemberTable.Kind kind,
374            boolean showInheritedSummary,
375            Content summariesList) {
376        LinkedList<Content> summaryTreeList = new LinkedList<>();
377        buildSummary(writer, kind, summaryTreeList);
378        if (showInheritedSummary)
379            buildInheritedSummary(writer, kind, summaryTreeList);
380        if (!summaryTreeList.isEmpty()) {
381            Content member
382                = writer.getMemberSummaryHeader(typeElement, summariesList);
383            summaryTreeList.forEach(member::add);
384            writer.addSummary(summariesList, member);
385        }
386    }
387
388    private SortedSet<? extends Element>
389            asSortedSet(Collection<? extends Element> members) {
390        SortedSet<Element> out = new TreeSet<>(comparator);
391        out.addAll(members);
392        return out;
393    }
394
395    /**
396     * A utility class to manage the property-related methods that should be
397     * synthesized or updated.
398     *
399     * A property may comprise a field (that is typically private, if present),
400     * a {@code fooProperty()} method (which is the defining characteristic for
401     * a property), a {@code getFoo()} method and/or a {@code setFoo(Foo foo)} method.
402     *
403     * Either the field (if present) or the {@code fooProperty()} method should have a
404     * comment. If there is no field, or no comment on the field, the description for
405     * the property will be derived from the description of the {@code fooProperty()}
406     * method. If any method does not have a comment, one will be provided.
407     */
408    static class PropertyHelper {
409
410        private final Map<Element, Element> classPropertiesMap
411            = new HashMap<>();
412
413        private final MemberSummaryBuilder builder;
414
415        PropertyHelper(MemberSummaryBuilder builder) {
416            this.builder = builder;
417            computeProperties();
418        }
419
420        private void computeProperties() {
421            VisibleMemberTable vmt = builder.getVisibleMemberTable();
422            List<ExecutableElement> props
423                = ElementFilter.methodsIn(vmt.getVisibleMembers(PROPERTIES));
424            for (ExecutableElement propertyMethod : props) {
425                ExecutableElement getter
426                    = vmt.getPropertyGetter(propertyMethod);
427                ExecutableElement setter
428                    = vmt.getPropertySetter(propertyMethod);
429                VariableElement field = vmt.getPropertyField(propertyMethod);
430
431                addToPropertiesMap(propertyMethod, field, getter, setter);
432            }
433        }
434
435        private void addToPropertiesMap(ExecutableElement propertyMethod,
436                VariableElement field,
437                ExecutableElement getter,
438                ExecutableElement setter) {
439            // determine the preferred element from which to derive the property
440            // description
441            Element e = field == null || !builder.utils.hasDocCommentTree(field)
442                ? propertyMethod
443                : field;
444
445            if (e == field && builder.utils.hasDocCommentTree(propertyMethod)) {
446                BaseConfiguration configuration = builder.configuration;
447                configuration.getReporter().print(Diagnostic.Kind.WARNING,
448                    propertyMethod, configuration.getDocResources()
449                        .getText("doclet.duplicate.comment.for.property"));
450            }
451
452            addToPropertiesMap(propertyMethod, e);
453            addToPropertiesMap(getter, e);
454            addToPropertiesMap(setter, e);
455        }
456
457        private void addToPropertiesMap(Element propertyMethod,
458                Element commentSource) {
459            Objects.requireNonNull(commentSource);
460            if (propertyMethod == null) {
461                return;
462            }
463
464            Utils utils = builder.utils;
465            DocCommentTree docTree = utils.hasDocCommentTree(propertyMethod)
466                ? utils.getDocCommentTree(propertyMethod)
467                : null;
468
469            /*
470             * The second condition is required for the property buckets. In
471             * this case the comment is at the property method (not at the
472             * field)
473             * and it needs to be listed in the map.
474             */
475            if ((docTree == null) || propertyMethod.equals(commentSource)) {
476                classPropertiesMap.put(propertyMethod, commentSource);
477            }
478        }
479
480        /**
481         * Returns the element for the property documentation belonging to the given member.
482         * @param element the member for which the property documentation is needed.
483         * @return the element for the property documentation, null if there is none.
484         */
485        public Element getPropertyElement(Element element) {
486            return classPropertiesMap.get(element);
487        }
488    }
489}