001/*
002 * Copyright (c) 2003, 2022, 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.util;
027
028import java.util.ArrayList;
029import java.util.Iterator;
030import java.util.Objects;
031import java.util.Optional;
032import java.util.function.BiFunction;
033import java.util.function.Function;
034
035import javax.lang.model.element.ExecutableElement;
036
037public class DocFinder {
038
039    /*
040     * A specialized, possibly stateful, function that accepts a method in the
041     * hierarchy and returns a value that controls the search or throws an
042     * exception, which terminates the search and transparently bubbles
043     * up the stack.
044     */
045    @FunctionalInterface
046    public interface Criterion<T, X extends Throwable> {
047        Result<T> apply(ExecutableElement method) throws X;
048    }
049
050    private final Function<ExecutableElement,
051            ExecutableElement> overriddenMethodLookup;
052    private final BiFunction<ExecutableElement, ExecutableElement,
053            Iterable<ExecutableElement>> implementedMethodsLookup;
054
055    DocFinder(
056            Function<ExecutableElement,
057                    ExecutableElement> overriddenMethodLookup,
058            BiFunction<ExecutableElement, ExecutableElement,
059                    Iterable<ExecutableElement>> implementedMethodsLookup) {
060        this.overriddenMethodLookup = overriddenMethodLookup;
061        this.implementedMethodsLookup = implementedMethodsLookup;
062    }
063
064    @SuppressWarnings("serial")
065    public static final class NoOverriddenMethodsFound extends Exception {
066
067        // only DocFinder should instantiate this exception
068        private NoOverriddenMethodsFound() {
069        }
070    }
071
072    public <T, X extends Throwable> Result<T> search(ExecutableElement method,
073            Criterion<T, X> criterion)
074            throws X {
075        return search(method, true, criterion);
076    }
077
078    public <T, X extends Throwable> Result<T> search(ExecutableElement method,
079            boolean includeMethod,
080            Criterion<T, X> criterion)
081            throws X {
082        try {
083            return search0(method, includeMethod, false, criterion);
084        } catch (NoOverriddenMethodsFound e) {
085            // should not happen because the exception flag is unset
086            throw new AssertionError(e);
087        }
088    }
089
090    public <T, X extends Throwable> Result<T> trySearch(
091            ExecutableElement method,
092            Criterion<T, X> criterion)
093            throws NoOverriddenMethodsFound, X {
094        return search0(method, false, true, criterion);
095    }
096
097    /*
098     * Searches through the overridden methods hierarchy of the provided method.
099     *
100     * Depending on how it is instructed, the search begins from either the
101     * given
102     * method or the first method that the given method overrides. The search
103     * then applies the given criterion to methods it encounters, in the
104     * hierarchy order, until either of the following happens:
105     *
106     * - the criterion concludes the search
107     * - the criterion throws an exception
108     * - the hierarchy is exhausted
109     *
110     * If the search succeeds, the returned result is of type Conclude.
111     * Otherwise, the returned result is generally that of the most
112     * recent call to Criterion::apply.
113     *
114     * If the given method overrides no methods (i.e. hierarchy consists of the
115     * given method only) and the search is instructed to detect that, the
116     * search terminates with an exception.
117     */
118    private <T, X extends Throwable> Result<T> search0(ExecutableElement method,
119            boolean includeMethodInSearch,
120            boolean throwExceptionIfDoesNotOverride,
121            Criterion<T, X> criterion)
122            throws NoOverriddenMethodsFound, X {
123        // if the "overrides" check is requested and does not pass, throw the
124        // exception
125        // first so that it trumps the result that the search would otherwise
126        // had
127        Iterator<ExecutableElement> methods = methodsOverriddenBy(method);
128        if (throwExceptionIfDoesNotOverride && !methods.hasNext()) {
129            throw new NoOverriddenMethodsFound();
130        }
131        Result<T> r = includeMethodInSearch ? criterion.apply(method)
132            : Result.CONTINUE();
133        if (!(r instanceof Result.Continue<T>)) {
134            return r;
135        }
136        while (methods.hasNext()) {
137            ExecutableElement m = methods.next();
138            r = search0(m, true, false /* don't check for overrides */,
139                criterion);
140            if (r instanceof Result.Conclude<T>) {
141                return r;
142            }
143        }
144        return r;
145    }
146
147    // We see both overridden and implemented methods as overridden
148    // (see JLS 8.4.8.1. Overriding (by Instance Methods))
149    private Iterator<ExecutableElement>
150            methodsOverriddenBy(ExecutableElement method) {
151        // TODO: create a lazy iterator if required
152        var list = new ArrayList<ExecutableElement>();
153        ExecutableElement overridden = overriddenMethodLookup.apply(method);
154        if (overridden != null) {
155            list.add(overridden);
156        }
157        implementedMethodsLookup.apply(method, method).forEach(list::add);
158        return list.iterator();
159    }
160
161    private static final Result<?> SKIP = new Skipped<>();
162    private static final Result<?> CONTINUE = new Continued<>();
163
164    /*
165     * Use static factory methods to get the desired result to return from
166     * Criterion. Use instanceof to check for a result type returned from
167     * a search. If a use case permits and you prefer Optional API, use
168     * the fromOptional/toOptional convenience methods to get and
169     * check for the result respectively.
170     */
171    public sealed interface Result<T> {
172
173        sealed interface Skip<T> extends Result<T> permits Skipped {
174        }
175
176        sealed interface Continue<T> extends Result<T> permits Continued {
177        }
178
179        sealed interface Conclude<T> extends Result<T> permits Concluded {
180
181            T value();
182        }
183
184        /*
185         * Skips the search on the part of the hierarchy above the method for
186         * which this result is returned and continues the search from that
187         * method sibling, if any.
188         */
189        @SuppressWarnings("unchecked")
190        static <T> Result<T> SKIP() {
191            return (Result<T>) SKIP;
192        }
193
194        /*
195         * Continues the search.
196         */
197        @SuppressWarnings("unchecked")
198        static <T> Result<T> CONTINUE() {
199            return (Result<T>) CONTINUE;
200        }
201
202        /*
203         * Concludes the search with the given result.
204         */
205        static <T> Result<T> CONCLUDE(T value) {
206            return new Concluded<>(value);
207        }
208
209        /*
210         * Translates this Result into Optional.
211         *
212         * Convenience method. Call on the result of a search if you are only
213         * interested in whether the search succeeded or failed and you
214         * prefer the Optional API.
215         */
216        default Optional<T> toOptional() {
217            return Optional.empty();
218        }
219
220        /*
221         * Translates the given Optional into a binary decision whether to
222         * conclude the search or continue it.
223         *
224         * Convenience method. Use in Criterion that can easily provide
225         * suitable Optional. Don't use if Criterion needs to skip.
226         */
227        static <T> Result<T> fromOptional(Optional<T> optional) {
228            return optional.map(Result::CONCLUDE).orElseGet(Result::CONTINUE);
229        }
230    }
231
232    // Note: we hide records behind interfaces, as implementation detail.
233    // We don't directly implement Result with these records because it
234    // would require more exposure and commitment than is desired. For
235    // example, there would need to be public constructors, which
236    // would circumvent static factory methods.
237
238    private record Skipped<T>() implements DocFinder.Result.Skip<T> {
239    }
240
241    private record Continued<T>() implements DocFinder.Result.Continue<T> {
242    }
243
244    private record Concluded<T>(T value)
245            implements DocFinder.Result.Conclude<T> {
246
247        Concluded {
248            Objects.requireNonNull(value);
249        }
250
251        @Override
252        public Optional<T> toOptional() {
253            return Optional.of(value);
254        }
255    }
256}