blob: 983b8f4e2588dec4fa69e35b25df5cf057e794c7 [file] [log] [blame]
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.lang.ant.dom;
import com.intellij.openapi.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author Eugene Zhuravlev
* Date: Apr 23, 2010
*/
public class PropertyExpander {
private static final Pattern $$_PATTERN = Pattern.compile("\\$\\$");
final List<PropertiesProvider> myProviders = new ArrayList<PropertiesProvider>();
final Resolver myResolver;
final Set<String> myNamesToSkip = new HashSet<String>();
private PropertyExpansionListener myPropertyExpansionListener;
public interface PropertyExpansionListener {
void onPropertyExpanded(String propName, String propValue);
}
public PropertyExpander(final @NotNull String str) {
this(str, Collections.<String>emptySet());
}
private PropertyExpander(final @NotNull String str, Set<String> namesToSkip) {
myResolver = new Resolver(str, namesToSkip);
myNamesToSkip.addAll(namesToSkip);
}
/**
* @param listener new listener implementation
* @return previous listener
*/
public PropertyExpansionListener setPropertyExpansionListener(PropertyExpansionListener listener) {
final PropertyExpansionListener prevListener = myPropertyExpansionListener;
myPropertyExpansionListener = listener;
return prevListener;
}
public boolean hasPropertiesToExpand() {
return myResolver.hasNext();
}
// true if should continue, false to stop
public void acceptProvider(PropertiesProvider provider) {
while (myResolver.hasNext()) {
final String propName = myResolver.next();
final String value = provider.getPropertyValue(propName);
if (value != null) {
myNamesToSkip.add(propName); // prevent infinite recursion
final String propValue;
if (provider instanceof PropertiesProvider.SkipPropertyExpansionInValues) {
propValue = value;
}
else {
final PropertyExpander propertyValueExpander = new PropertyExpander(value, myNamesToSkip);
propertyValueExpander.setPropertyExpansionListener(myPropertyExpansionListener);
if (propertyValueExpander.hasPropertiesToExpand()) {
for (PropertiesProvider p : myProviders) {
propertyValueExpander.acceptProvider(p);
if (!propertyValueExpander.hasPropertiesToExpand()) {
break;
}
}
if (propertyValueExpander.hasPropertiesToExpand()) {
propertyValueExpander.acceptProvider(provider);
}
}
propValue = propertyValueExpander.getResult();
}
myResolver.replace(propValue);
notifyPropertyExpanded(propName, propValue);
}
}
myProviders.add(provider);
myResolver.restart();
}
public void notifyPropertyExpanded(String propName, String propValue) {
final PropertyExpansionListener listener = myPropertyExpansionListener;
if (listener != null) {
listener.onPropertyExpanded(propName, propValue);
}
}
@NotNull
public String getResult() {
return myResolver.getResult();
}
private static class Resolver implements Iterator<String> {
private int myCurrentIndex = -1;
private List<Pair<String /*property name without ${} characters*/, Integer /*offset of property occurrence including '$' char*/>> myPropertyNames;
private StringBuilder myBuilder;
private Resolver(final String str, Set<String> namesToSkip) {
myBuilder = new StringBuilder(str);
int startProp = 0;
while ((startProp = str.indexOf("${", startProp)) >= 0) {
if (startProp > 0 && str.charAt(startProp - 1) == '$') {
// the '$' is escaped
startProp += 2;
continue;
}
final int endProp = str.indexOf('}', startProp + 2);
if (endProp <= startProp + 2) {
startProp += 2;
continue;
}
final String prop = str.substring(startProp + 2, endProp);
if (!namesToSkip.contains(prop)) {
if (myPropertyNames == null) {
myPropertyNames = new ArrayList<Pair<String, Integer>>();
}
myPropertyNames.add(new Pair<String, Integer>(prop, startProp));
}
startProp += 2;
}
if (myPropertyNames == null) {
myPropertyNames = Collections.emptyList();
}
}
void restart() {
myCurrentIndex = -1;
}
void replace(String newValue) {
final String name = getPropertyName(myCurrentIndex);
final int shift = newValue.length() - name.length() - 3/*property beginning and ending symbols '${}'*/;
// correct property offsets
for (int idx = myCurrentIndex + 1; idx < myPropertyNames.size(); idx++) {
final int currentOffset = getPropertyOffset(idx);
setPropertyOffset(idx, currentOffset + shift);
}
final int offset = getPropertyOffset(myCurrentIndex);
myBuilder.replace(offset, offset + name.length() + 3/*ending brace*/, newValue);
myPropertyNames.remove(myCurrentIndex);
myCurrentIndex--;
}
private String getPropertyName(int index) {
return myPropertyNames.get(index).getFirst();
}
private int getPropertyOffset(int index) {
return myPropertyNames.get(index).getSecond();
}
private void setPropertyOffset(int index, int value) {
final Pair<String, Integer> pair = myPropertyNames.get(index);
myPropertyNames.set(index, new Pair<String, Integer>(pair.getFirst(), value));
}
String getResult() {
final String value = myBuilder.toString();
if (value.indexOf("$$") >= 0) {
return $$_PATTERN.matcher(value).replaceAll("\\$");
}
return value;
}
public boolean hasNext() {
return (myCurrentIndex + 1) < myPropertyNames.size();
}
public String next() {
return getPropertyName(++myCurrentIndex);
}
public void remove() {
replace("");
}
}
}