001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.data.iri.template;
019
020import com.github.fge.uritemplate.URITemplate;
021import com.google.common.base.MoreObjects;
022import com.google.common.collect.ImmutableList;
023import com.google.common.collect.ImmutableMap;
024
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.regex.Pattern;
029import java.util.regex.PatternSyntaxException;
030
031/**
032 * Splits an URI based on an RFC 6570 Template. This is the "inverse" of {@link URITemplate}.
033 * Reliably parsing URIs correctly is hard, see <a href="https://urlpattern.spec.whatwg.org">WHATWG
034 * URL Pattern</a>. This class is intentionally limited, and only fulfills the current needs of this
035 * project. It still has a lot of gaps. If you read this and need it to do more and better, please
036 * improve it, along with its coverage in URITemplateSplitterTest and URITemplateTest! (Or find some
037 * existing library which does this? E.g.
038 * [teaconmc/urlpattern](https://github.com/teaconmc/urlpattern/issues/1)).
039 */
040public class URITemplateSplitter {
041
042    // TODO Must escape certain characters that are reserved in RegExp! TDD.
043
044    private static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("\\{([^{}]+)\\}");
045
046    private final String template;
047    private final List<String> keys;
048    private final Pattern pattern;
049    private final int length;
050
051    /** Transforms a RFC 6570 URI Template into a Regular Expression usable to "match" it. */
052    public URITemplateSplitter(String template) {
053        var lengther = new StringBuilder();
054        var pattern = new StringBuilder("^");
055        var pmatcher = URI_TEMPLATE_PATTERN.matcher(template);
056        var lmatcher = URI_TEMPLATE_PATTERN.matcher(template);
057        var keysBuilder = ImmutableList.<String>builder();
058        while (pmatcher.find()) {
059            lmatcher.find();
060            var name = pmatcher.group(1);
061            keysBuilder.add(name);
062
063            String group;
064            var p = pmatcher.end();
065            if (p < template.length()) {
066                var nextCharacter = template.subSequence(p, p + 1).charAt(0);
067                group = "(?<" + name + ">[^" + nextCharacter + "]+)";
068            } else {
069                group = "(?<" + name + ">.+)";
070            }
071            pmatcher.appendReplacement(pattern, group);
072            lmatcher.appendReplacement(lengther, "*");
073        }
074        pmatcher.appendTail(pattern);
075        lmatcher.appendTail(lengther);
076        pattern.append('$');
077
078        this.template = template;
079        this.keys = keysBuilder.build();
080        try {
081            this.pattern = Pattern.compile(pattern.toString());
082        } catch (PatternSyntaxException e) {
083            throw new IllegalArgumentException(template, e);
084        }
085        this.length = lengther.length();
086    }
087
088    public Optional<Map<String, String>> fromString(String uri) {
089        var map = ImmutableMap.<String, String>builder();
090        var matcher = pattern.matcher(uri);
091
092        if (matcher.find()) {
093            for (var name : keys) {
094                String value = matcher.group(name);
095                map.put(name, value);
096            }
097            // System.out.println("URITemplateSplitter matched '" + uri + "' to: " + pattern);
098            return Optional.of(map.build());
099        } else {
100            return Optional.empty();
101        }
102    }
103
104    public String getTemplate() {
105        return template;
106    }
107
108    public List<String> getKeys() {
109        return keys;
110    }
111
112    public Pattern getPattern() {
113        return pattern;
114    }
115
116    public int getLength() {
117        return length;
118    }
119
120    @Override
121    public String toString() {
122        return MoreObjects.toStringHelper(this)
123                .add("template", template)
124                .add("keys", keys)
125                .add("pattern", pattern)
126                .add("length", length)
127                .toString();
128    }
129}