001// Copyright 2004, 2005 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.hivemind.impl;
016
017import org.apache.commons.logging.Log;
018import org.apache.commons.logging.LogFactory;
019import org.apache.hivemind.ErrorHandler;
020import org.apache.hivemind.Location;
021import org.apache.hivemind.SymbolSource;
022
023/**
024 * A simple parser used to identify symbols in a string and expand them via a
025 * {@link org.apache.hivemind.SymbolSource}.
026 * 
027 * @author Howard Lewis Ship
028 */
029public class SymbolExpander
030{
031    private ErrorHandler _errorHandler;
032
033    private SymbolSource _source;
034
035    public SymbolExpander(ErrorHandler handler, SymbolSource source)
036    {
037        _errorHandler = handler;
038        _source = source;
039    }
040
041    private static final Log LOG = LogFactory.getLog(SymbolExpander.class);
042
043    private static final int STATE_START = 0;
044
045    private static final int STATE_DOLLAR = 1;
046
047    private static final int STATE_COLLECT_SYMBOL_NAME = 2;
048
049    /**
050     * <p>
051     * Identifies symbols in the text and expands them, using the {@link SymbolSource}. Returns the
052     * modified text. May return text if text does not contain any symbols.
053     * 
054     * @param text
055     *            the text to scan
056     * @param location
057     *            the location to report errors (undefined symbols)
058     */
059    public String expandSymbols(String text, Location location)
060    {
061        StringBuffer result = new StringBuffer(text.length());
062        char[] buffer = text.toCharArray();
063        int state = STATE_START;
064        int blockStart = 0;
065        int blockLength = 0;
066        int symbolStart = -1;
067        int symbolLength = 0;
068        int i = 0;
069        int braceDepth = 0;
070        boolean anySymbols = false;
071
072        while (i < buffer.length)
073        {
074            char ch = buffer[i];
075
076            switch (state)
077            {
078                case STATE_START:
079
080                    if (ch == '$')
081                    {
082                        state = STATE_DOLLAR;
083                        i++;
084                        continue;
085                    }
086
087                    blockLength++;
088                    i++;
089                    continue;
090
091                case STATE_DOLLAR:
092
093                    if (ch == '{')
094                    {
095                        state = STATE_COLLECT_SYMBOL_NAME;
096                        i++;
097
098                        symbolStart = i;
099                        symbolLength = 0;
100                        braceDepth = 1;
101
102                        continue;
103                    }
104
105                    // Any time two $$ appear, it is collapsed down to a single $,
106                    // but the next character is passed through un-interpreted (even if it
107                    // is a brace).
108
109                    if (ch == '$')
110                    {
111                        // This is effectively a symbol, meaning that the input string
112                        // will not equal the output string.
113
114                        anySymbols = true;
115
116                        if (blockLength > 0)
117                            result.append(buffer, blockStart, blockLength);
118
119                        result.append(ch);
120
121                        i++;
122                        blockStart = i;
123                        blockLength = 0;
124                        state = STATE_START;
125
126                        continue;
127                    }
128
129                    // The '$' was just what it was, not the start of a ${} expression
130                    // block, so include it as part of the static text block.
131
132                    blockLength++;
133
134                    state = STATE_START;
135                    continue;
136
137                case STATE_COLLECT_SYMBOL_NAME:
138
139                    if (ch != '}')
140                    {
141                        if (ch == '{')
142                            braceDepth++;
143
144                        i++;
145                        symbolLength++;
146                        continue;
147                    }
148
149                    braceDepth--;
150
151                    if (braceDepth > 0)
152                    {
153                        i++;
154                        symbolLength++;
155                        continue;
156                    }
157
158                    // Hit the closing brace of a symbol.
159
160                    // Degenerate case: the string "${}".
161
162                    if (symbolLength == 0)
163                        blockLength += 3;
164
165                    // Append anything up to the start of the sequence (this is static
166                    // text between symbol references).
167
168                    if (blockLength > 0)
169                        result.append(buffer, blockStart, blockLength);
170
171                    if (symbolLength > 0)
172                    {
173                        String variableName = text.substring(symbolStart, symbolStart
174                                + symbolLength);
175
176                        result.append(expandSymbol(variableName, location));
177
178                        anySymbols = true;
179                    }
180
181                    i++;
182                    blockStart = i;
183                    blockLength = 0;
184
185                    // And drop into state start
186
187                    state = STATE_START;
188
189                    continue;
190            }
191
192        }
193
194        // If get this far without seeing any variables, then just pass
195        // the input back.
196
197        if (!anySymbols)
198            return text;
199
200        // OK, to handle the end. Couple of degenerate cases where
201        // a ${...} was incomplete, so we adust the block length.
202
203        if (state == STATE_DOLLAR)
204            blockLength++;
205
206        if (state == STATE_COLLECT_SYMBOL_NAME)
207            blockLength += symbolLength + 2;
208
209        if (blockLength > 0)
210            result.append(buffer, blockStart, blockLength);
211
212        return result.toString();
213    }
214
215    private String expandSymbol(String name, Location location)
216    {
217        String value = _source.valueForSymbol(name);
218
219        if (value != null)
220            return value;
221
222        _errorHandler.error(LOG, ImplMessages.noSuchSymbol(name), location, null);
223
224        return "${" + name + "}";
225    }
226
227}