001// Copyright 2006, 2007 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.ioc.internal.services;
016
017import org.apache.tapestry5.commons.util.CollectionFactory;
018import org.apache.tapestry5.ioc.services.SymbolProvider;
019import org.apache.tapestry5.ioc.services.SymbolSource;
020
021import static org.apache.tapestry5.commons.util.CollectionFactory.newLinkedList;
022
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026
027public class SymbolSourceImpl implements SymbolSource
028{
029    private final List<SymbolProvider> providers;
030
031    /**
032     * Cache of symbol name to fully expanded symbol value.
033     */
034    private final Map<String, String> cache = CollectionFactory.newConcurrentMap();
035
036    /**
037     * Contains execution data needed when performing an expansion (largely, to check for endless recursion).
038     */
039    private class SymbolExpansion
040    {
041        private final LinkedList<String> expandingSymbols = newLinkedList();
042
043        String expandSymbols(String input)
044        {
045            StringBuilder builder = null;
046
047            int startx = 0;
048
049            while (true)
050            {
051                int symbolx = input.indexOf("${", startx);
052
053                // Special case: if the string contains no symbols then return it as is.
054
055                if (startx == 0 && symbolx < 0) return input;
056
057                // The string has at least one symbol, so its OK to create the StringBuilder
058
059                if (builder == null) builder = new StringBuilder();
060
061                // No more symbols found, so add in the rest of the string.
062
063                if (symbolx < 0)
064                {
065                    builder.append(input.substring(startx));
066                    break;
067                }
068
069                builder.append(input.substring(startx, symbolx));
070
071                int endx = input.indexOf("}", symbolx);
072
073                if (endx < 0)
074                {
075                    String message = expandingSymbols.isEmpty()
076                            ? String.format("Input string '%s' is missing a symbol closing brace.", input)
077                            : String.format("Input string '%s' is missing a symbol closing brace (in %s).", input, path());
078
079                    throw new RuntimeException(message);
080                }
081
082                String symbolName = input.substring(symbolx + 2, endx);
083
084                builder.append(valueForSymbol(symbolName));
085
086                // Restart the search after the '}'
087
088                startx = endx + 1;
089            }
090
091            return builder.toString();
092        }
093
094        String valueForSymbol(String symbolName)
095        {
096            String value = cache.get(symbolName);
097
098            if (value == null)
099            {
100                value = expandSymbol(symbolName);
101
102                cache.put(symbolName, value);
103            }
104
105            return value;
106        }
107
108        String expandSymbol(String symbolName)
109        {
110            if (expandingSymbols.contains(symbolName))
111            {
112                expandingSymbols.add(symbolName);
113                throw new RuntimeException(String.format("Symbol '%s' is defined in terms of itself (%s).",
114                        symbolName,
115                        pathFrom(symbolName)));
116            }
117
118            expandingSymbols.addLast(symbolName);
119
120            String value = null;
121
122            for (SymbolProvider provider : providers)
123            {
124                value = provider.valueForSymbol(symbolName);
125
126                if (value != null) break;
127            }
128
129            if (value == null)
130            {
131
132                String message = expandingSymbols.size() == 1
133                        ? String.format("Symbol '%s' is not defined.", symbolName)
134                        : String.format("Symbol '%s' is not defined (in %s).", symbolName, path());
135
136                throw new RuntimeException(message);
137            }
138
139            // The value may have symbols that need expansion.
140
141            String result = expandSymbols(value);
142
143            // And we're done expanding this symbol
144
145            expandingSymbols.removeLast();
146
147            return result;
148
149        }
150
151        String path()
152        {
153            StringBuilder builder = new StringBuilder();
154
155            boolean first = true;
156
157            for (String symbolName : expandingSymbols)
158            {
159                if (!first) builder.append(" --> ");
160
161                builder.append(symbolName);
162
163                first = false;
164            }
165
166            return builder.toString();
167        }
168
169        String pathFrom(String startSymbolName)
170        {
171            StringBuilder builder = new StringBuilder();
172
173            boolean first = true;
174            boolean match = false;
175
176            for (String symbolName : expandingSymbols)
177            {
178                if (!match)
179                {
180                    if (symbolName.equals(startSymbolName))
181                        match = true;
182                    else
183                        continue;
184                }
185
186                if (!first) builder.append(" --> ");
187
188                builder.append(symbolName);
189
190                first = false;
191            }
192
193            return builder.toString();
194        }
195    }
196
197    public SymbolSourceImpl(final List<SymbolProvider> providers)
198    {
199        this.providers = providers;
200    }
201
202    @Override
203    public String expandSymbols(String input)
204    {
205        return new SymbolExpansion().expandSymbols(input);
206    }
207
208    @Override
209    public String valueForSymbol(String symbolName)
210    {
211        String value = cache.get(symbolName);
212
213        // If already in the cache, then return it. Otherwise, let the SE find the value and
214        // update the cache.
215
216        return value != null ? value : new SymbolExpansion().valueForSymbol(symbolName);
217    }
218
219}