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}