1   /*
2    * Copyright 2021 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package com.linecorp.centraldogma.common;
17  
18  import static com.google.common.base.Preconditions.checkArgument;
19  
20  import java.util.Set;
21  import java.util.regex.Pattern;
22  import java.util.stream.Collectors;
23  
24  import javax.annotation.Nullable;
25  
26  import com.google.common.annotations.VisibleForTesting;
27  import com.google.common.base.MoreObjects;
28  import com.google.common.math.IntMath;
29  
30  final class DefaultPathPattern implements PathPattern {
31  
32      private static final Pattern PATH_PATTERN_PATTERN = Pattern.compile("^[- /*_.0-9a-zA-Z]+$");
33  
34      static final String ALL = "/**";
35  
36      static final DefaultPathPattern allPattern = new DefaultPathPattern(ALL, ALL);
37  
38      private final String patterns;
39  
40      @Nullable
41      private String encoded;
42  
43      DefaultPathPattern(Set<String> patterns) {
44          this.patterns = patterns.stream()
45                                  .peek(DefaultPathPattern::validatePathPattern)
46                                  .filter(pattern -> !pattern.isEmpty())
47                                  .map(pattern -> {
48                                      if (pattern.charAt(0) != '/') {
49                                          return "/**/" + pattern;
50                                      }
51                                      return pattern;
52                                  }).collect(Collectors.joining(","));
53      }
54  
55      private DefaultPathPattern(String patterns, String encoded) {
56          this.patterns = patterns;
57          this.encoded = encoded;
58      }
59  
60      @Override
61      public String patternString() {
62          return patterns;
63      }
64  
65      @Override
66      public String encoded() {
67          if (encoded != null) {
68              return encoded;
69          }
70          return encoded = encodePathPattern(patterns);
71      }
72  
73      @VisibleForTesting
74      static String encodePathPattern(String pathPattern) {
75          // We do not need full escaping because we validated the path pattern already and thus contains only
76          // -, ' ', /, *, _, ., ',', a-z, A-Z, 0-9.
77          int spacePos = pathPattern.indexOf(' ');
78          if (spacePos < 0) {
79              return pathPattern;
80          }
81  
82          final StringBuilder buf = new StringBuilder(IntMath.saturatedMultiply(pathPattern.length(), 2));
83          for (int pos = 0;;) {
84              buf.append(pathPattern, pos, spacePos);
85              buf.append("%20");
86              pos = spacePos + 1;
87              spacePos = pathPattern.indexOf(' ', pos);
88              if (spacePos < 0) {
89                  buf.append(pathPattern, pos, pathPattern.length());
90                  break;
91              }
92          }
93  
94          return buf.toString();
95      }
96  
97      private static String validatePathPattern(String pattern) {
98          checkArgument(PATH_PATTERN_PATTERN.matcher(pattern).matches(),
99                        "pattern: %s (expected: %s)", pattern, PATH_PATTERN_PATTERN);
100         return pattern;
101     }
102 
103     @Override
104     public String toString() {
105         return MoreObjects.toStringHelper(this)
106                           .add("patterns", patterns)
107                           .add("encoded", encoded)
108                           .toString();
109     }
110 }