1/**
2 * Copyright 2007 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.tonicsystems.jarjar;
18
19import java.util.regex.Matcher;
20import java.util.regex.Pattern;
21import java.util.ArrayList;
22import java.util.Arrays;
23
24class Wildcard
25{
26    private static Pattern dstar = Pattern.compile("\\*\\*");
27    private static Pattern star  = Pattern.compile("\\*");
28    private static Pattern estar = Pattern.compile("\\+\\??\\)\\Z");
29    private static Pattern dollar = Pattern.compile("\\$");
30
31    private final Pattern pattern;
32    private final int count;
33    private final ArrayList<Object> parts = new ArrayList<Object>(16); // kept for debugging
34    private final String[] strings;
35    private final int[] refs;
36
37    public Wildcard(String pattern, String result) {
38        if (pattern.equals("**"))
39            throw new IllegalArgumentException("'**' is not a valid pattern");
40        if (!checkIdentifierChars(pattern, "/*"))
41            throw new IllegalArgumentException("Not a valid package pattern: " + pattern);
42        if (pattern.indexOf("***") >= 0)
43            throw new IllegalArgumentException("The sequence '***' is invalid in a package pattern");
44
45        String regex = pattern;
46        regex = replaceAllLiteral(dstar, regex, "(.+?)");
47        regex = replaceAllLiteral(star, regex, "([^/]+)");
48        regex = replaceAllLiteral(estar, regex, "*)");
49        regex = replaceAllLiteral(dollar, regex, "\\$");
50        this.pattern = Pattern.compile("\\A" + regex + "\\Z");
51        this.count = this.pattern.matcher("foo").groupCount();
52
53        // TODO: check for illegal characters
54        char[] chars = result.toCharArray();
55        int max = 0;
56        for (int i = 0, mark = 0, state = 0, len = chars.length; i < len + 1; i++) {
57            char ch = (i == len) ? '@' : chars[i];
58            if (state == 0) {
59                if (ch == '@') {
60                    parts.add(new String(chars, mark, i - mark));
61                    mark = i + 1;
62                    state = 1;
63                }
64            } else {
65                switch (ch) {
66                case '0': case '1': case '2': case '3': case '4':
67                case '5': case '6': case '7': case '8': case '9':
68                    break;
69                default:
70                    if (i == mark)
71                        throw new IllegalArgumentException("Backslash not followed by a digit");
72                    int n = Integer.parseInt(new String(chars, mark, i - mark));
73                    if (n > max)
74                        max = n;
75                    parts.add(new Integer(n));
76                    mark = i--;
77                    state = 0;
78                }
79            }
80        }
81        int size = parts.size();
82        strings = new String[size];
83        refs = new int[size];
84        Arrays.fill(refs, -1);
85        for (int i = 0; i < size; i++) {
86            Object v = parts.get(i);
87            if (v instanceof String) {
88                strings[i] = ((String)v).replace('.', '/');
89            } else {
90                refs[i] = ((Integer)v).intValue();
91            }
92        }
93        if (count < max)
94            throw new IllegalArgumentException("Result includes impossible placeholder \"@" + max + "\": " + result);
95        // System.err.println(this);
96    }
97
98    public boolean matches(String value) {
99        return getMatcher(value) != null;
100    }
101
102    public String replace(String value) {
103        Matcher matcher = getMatcher(value);
104        if (matcher != null) {
105            StringBuilder sb = new StringBuilder();
106            for (int i = 0; i < strings.length; i++)
107                sb.append((refs[i] >= 0) ? matcher.group(refs[i]) : strings[i]);
108            return sb.toString();
109        }
110        return null;
111    }
112
113    private Matcher getMatcher(String value) {
114        Matcher matcher = pattern.matcher(value);
115        if (matcher.matches() && checkIdentifierChars(value, "/"))
116            return matcher;
117        return null;
118    }
119
120    private static boolean checkIdentifierChars(String expr, String extra) {
121      // package-info violates the spec for Java Identifiers.
122      // Nevertheless, expressions that end with this string are still legal.
123      // See 7.4.1.1 of the Java language spec for discussion.
124      if (expr.endsWith("package-info")) {
125          expr = expr.substring(0, expr.length() - "package-info".length());
126      }
127      for (int i = 0, len = expr.length(); i < len; i++) {
128          char c = expr.charAt(i);
129          if (extra.indexOf(c) >= 0)
130              continue;
131          if (!Character.isJavaIdentifierPart(c))
132              return false;
133      }
134      return true;
135    }
136
137    private static String replaceAllLiteral(Pattern pattern, String value, String replace) {
138        replace = replace.replaceAll("([$\\\\])", "\\\\$0");
139        return pattern.matcher(value).replaceAll(replace);
140    }
141
142    public String toString() {
143        return "Wildcard{pattern=" + pattern + ",parts=" + parts + "}";
144    }
145}
146