Matcher.java

/*
 * 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.apache.rat.documentation.velocity;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.rat.analysis.IHeaderMatcher;
import org.apache.rat.analysis.matchers.AbstractMatcherContainer;
import org.apache.rat.config.parameters.ComponentType;
import org.apache.rat.config.parameters.Description;
import org.apache.rat.config.parameters.DescriptionBuilder;
import org.apache.rat.configuration.XMLConfig;

/**
 * The Matcher representation for documentation.
 */
public class Matcher {
    /**
     * The description for this matcher.
     */
    private final Description desc;

    /**
     * The header matcher we are wrapping. May be {@code null}.
     */
    private final IHeaderMatcher self;

    /**
     * The description of the enclosed attribute. May be null.
     */
    private final Enclosed enclosed;

    /**
     * The set of attributes for this matcher.
     */
    private final Set<Attribute> attributes;

    /**
     * Copy constructor.
     * @param matcher the matcher to copy.
     */
    Matcher(final Matcher matcher) {
        this.desc = matcher.desc;
        this.self = matcher.self;
        this.enclosed = matcher.enclosed;
        this.attributes = matcher.attributes;
    }

    /**
     * Creates from a system Matcher.
     * @param self the IHeaderMatcher to wrap.
     */
    Matcher(final IHeaderMatcher self) {
        this(DescriptionBuilder.buildMap(self.getClass()), self);
    }

    /**
     * Creates from a description and a system matcher.
     * @param desc the description of the matcher.
     * @param self the matcher to wrap. May be {@code null}.
     */
    Matcher(final Description desc, final IHeaderMatcher self) {
        Objects.requireNonNull(desc);
        this.desc = desc;
        this.self = self;
        Enclosed enclosed = null;
        attributes = new TreeSet<>(Comparator.comparing(Attribute::getName));
        for (Description child : desc.childrenOfType(ComponentType.PARAMETER)) {
            if (XMLConfig.isInlineNode(desc.getCommonName(), child.getCommonName())) {
                enclosed = new Enclosed(child);
            } else {
                attributes.add(new Attribute(child));
            }
        }
        this.enclosed = enclosed;
    }

    /**
     * Get the name of the matcher type.
     * @return the name of the matcher type.
     */
    public String getName() {
        return desc.getCommonName();
    }

    /**
     * Gets the description of the matcher type.
     * @return The description of the matcher type or an empty string.
     */
    public String getDescription() {
        return StringUtils.defaultIfEmpty(desc.getDescription(), "");
    }

    /**
     * If the matcher encloses another matcher return the definition of that enclosure.
     * @return the description of the enclose matcher. May be {@code null}.
     */
    public Enclosed getEnclosed() {
        return enclosed;
    }

    /**
     * Gets the attributes of this matcher.
     * @return a collection of attributes for this matcher. May be empty but not {@code null}.
     */
    public Collection<Attribute> getAttributes() {
        return attributes;
    }

    /**
     * Gets the direct children of this matcher.
     * @return A collection of the direct children of this matcher. May be empty but not {@code null}.
     */
    Collection<Matcher> getChildren() {
        if (self != null && enclosed != null && IHeaderMatcher.class.equals(enclosed.desc.getChildType())) {
            if (self instanceof AbstractMatcherContainer matcherContainer) {
                return matcherContainer.getEnclosed().stream().map(Matcher::new).collect(Collectors.toList());
            }
            try {
                IHeaderMatcher matcher = (IHeaderMatcher) enclosed.desc.getter(self.getClass()).invoke(self);
                return Collections.singleton(new Matcher(matcher));
            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
        return Collections.emptyList();
    }

    /**
     * The description of an enclosed matcher.
     */
    public final class Enclosed {
        /** The description for the enclosed item */
        private final Description desc;

        /**
         * Constructor.
         * @param desc the description of the enclosed matcher.
         */
        Enclosed(final Description desc) {
            this.desc = desc;
        }

        /**
         * Gets the required flag.
         * @return the word "required" or "optional" depending on the state of the required flag.
         */
        public String getRequired() {
            return desc.isRequired() ? "required" : "optional";
        }

        /**
         * Gets the phrase "or more " if the enclosed matcher may be multiple.
         * @return the phrase "or more " or an empty string.
         */
        public String getCollection() {
            return desc.isCollection() ? "or more " : "";
        }

        /**
         * Get the type of the enclosed matcher.
         * @return the type of the enclosed matcher.
         */
        public String getType() {
            return desc.getChildType().getSimpleName();
        }

        /**
         * Gets the value of the enclosed matcher.
         *  <ul>
         *  <li>If the Matcher does not have an {@link IHeaderMatcher} defined this method returns {@code null}.</li>
         *  <li>If the value is a string it is normalized before being returned.</li>
         *  </ul>
         * @return the value of the enclosed matcher in the provided {@link IHeaderMatcher}.
         * @see StringUtils#normalizeSpace(String)
         */
        public Object getValue() {
            if (self != null) {
                try {
                    Object value = desc.getter(self.getClass()).invoke(self);
                    return value instanceof String ? StringUtils.normalizeSpace((String) value) : value;
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            }
            return null;
        }
        Class<?> getChildType() {
            return desc.getChildType();
        }
    }

    /**
     * The definition of an attribute.
     */
    public final class Attribute {
        /**
         * The description for the attribute.
         */
        private final Description description;

        /**
         * Constructor
         * @param description the description for this attribute.
         */
        Attribute(final Description description) {
            this.description = description;
        }

        /**
         * Gets the attribute name.
         * @return the attribut name.
         */
        public String getName() {
            return description.getCommonName();
        }

        /**
         * Gets the required flag.
         * @return the word "required" or "optional" depending on the state of the required flag.
         */
        public String getRequired() {
            return description.isRequired() ? "required" : "optional";
        }

        /**
         * Get the type of the attribute.
         * @return the type of the enclosed matcher.
         */
        public String getType() {
            return description.getChildType().getSimpleName();
        }

        /**
         * Gets the description of the attribute or an empty string.
         * @return the description of the attribute or an empty string.
         */
        public String getDescription() {
            return StringUtils.defaultIfEmpty(description.getDescription(), "");
        }

        /**
         * Gets the value of the enclosed matcher.
         * <ul>
         * <li>If the Matcher does not have an {@link IHeaderMatcher} defined this method returns {@code null}.</li>
         * <li>If the value of the attribute (self) is {@code null}, this method returns {@code null}.</li>
         * <li>If the value is a string, it is normalized before being returned.</li>
         * <li>If the attribute is an "id" and the value is a UUID, {@code null} is returned.</li>
         * <li>otherwise, the string value is returned.</li>
         * </ul>
         * @return the value of the enclosed matcher in the provided {@link IHeaderMatcher}.
         * @see StringUtils#normalizeSpace(String)
         */
        public String getValue() {
            if (self != null) {
                try {
                    Object value = description.getter(self.getClass()).invoke(self);
                    if (value != null) {
                        String result = value.toString();
                        if (description.getCommonName().equals("id")) {
                            try {
                                UUID.fromString(result);
                                return null;
                            } catch (IllegalArgumentException e) {
                                // do nothing.
                            }
                        }
                        return result;
                    }
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            }
            return null;
        }
    }
}