Jak zamienić zestaw tokenów w Java String?

106

Mam następujący ciąg szablonu: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Mam też zmienne typu String na imię i nazwisko, numer faktury i termin płatności - jak najlepiej zastąpić tokeny w szablonie zmiennymi?

(Zwróć uwagę, że jeśli zmienna zawiera token, NIE powinna być zastępowana).


EDYTOWAĆ

Dzięki @laginimaineb i @ alan-moore, oto moje rozwiązanie:

public static String replaceTokens(String text, 
                                   Map<String, String> replacements) {
    Pattern pattern = Pattern.compile("\\[(.+?)\\]");
    Matcher matcher = pattern.matcher(text);
    StringBuffer buffer = new StringBuffer();

    while (matcher.find()) {
        String replacement = replacements.get(matcher.group(1));
        if (replacement != null) {
            // matcher.appendReplacement(buffer, replacement);
            // see comment 
            matcher.appendReplacement(buffer, "");
            buffer.append(replacement);
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
znak
źródło
Należy jednak zauważyć, że StringBuffer jest tym samym, co właśnie zsynchronizowane StringBuilder. Jednak ponieważ w tym przykładzie nie ma potrzeby synchronizowania budowania String, może być lepiej przy użyciu StringBuilder (nawet jeśli pozyskiwanie blokad jest operacją prawie zerową).
laginimaineb
1
Niestety w tym przypadku musisz użyć StringBuffer; tego oczekują metody appendXXX (). Są na rynku od czasów Java 4, a StringBuilder nie został dodany do Java 5. Jak powiedziałeś, to nic wielkiego, po prostu denerwujące.
Alan Moore,
4
Jeszcze jedno: metoda appendReplacement (), podobnie jak metody replaceXXX (), szuka odniesień do grup przechwytywania, takich jak $ 1, $ 2 itd., I zastępuje je tekstem z powiązanych grup przechwytywania. Jeśli tekst zastępczy może zawierać znaki dolara lub ukośniki odwrotne (używane do zmiany znaczenia znaków dolara), możesz mieć problem. Najłatwiejszym sposobem rozwiązania tego problemu jest podzielenie operacji dołączania na dwa etapy, tak jak to zrobiłem w powyższym kodzie.
Alan Moore
Alan - jestem pod wrażeniem, że to zauważyłeś. Nie sądziłem, że tak prosty problem będzie tak trudny do rozwiązania!
Mark

Odpowiedzi:

65

Najskuteczniejszym sposobem byłoby użycie dopasowania do ciągłego znajdowania wyrażeń i zastępowania ich, a następnie dołączania tekstu do konstruktora ciągów:

Pattern pattern = Pattern.compile("\\[(.+?)\\]");
Matcher matcher = pattern.matcher(text);
HashMap<String,String> replacements = new HashMap<String,String>();
//populate the replacements map ...
StringBuilder builder = new StringBuilder();
int i = 0;
while (matcher.find()) {
    String replacement = replacements.get(matcher.group(1));
    builder.append(text.substring(i, matcher.start()));
    if (replacement == null)
        builder.append(matcher.group(0));
    else
        builder.append(replacement);
    i = matcher.end();
}
builder.append(text.substring(i, text.length()));
return builder.toString();
laginimaineb
źródło
10
Zrobiłbym to w ten sposób, z wyjątkiem tego, że użyłbym metod appendReplacement () i appendTail () Matchera do skopiowania niedopasowanego tekstu; nie trzeba tego robić ręcznie.
Alan Moore
5
Właściwie metody appendReplacement () i appentTail () wymagają StringBuffer, który jest snychronizowany (co nie jest tutaj przydatne). Podana odpowiedź wykorzystuje StringBuilder, który w moich testach jest o 20% szybszy.
dube
103

Naprawdę nie sądzę, abyś musiał używać do tego silnika szablonów lub czegoś podobnego. Możesz użyć tej String.formatmetody, na przykład:

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
źródło
4
Jedną z wad jest to, że musisz ustawić parametry we właściwej kolejności
gerrytan
Innym jest to, że nie możesz określić własnego formatu tokenu zastępczego.
Franz D.
innym jest to, że nie działa dynamicznie, będąc w stanie mieć zestaw danych kluczy / wartości, a następnie zastosować go do dowolnego ciągu
Brad Parks,
43

Niestety, wspomniana powyżej wygodna metoda String.format jest dostępna dopiero od wersji Java 1.5 (która w dzisiejszych czasach powinna być dość standardowa, ale nigdy nie wiadomo). Zamiast tego możesz również użyć klasy MessageFormat języka Java do zastąpienia symboli zastępczych.

Obsługuje symbole zastępcze w postaci „{numer}”, więc Twoja wiadomość będzie wyglądać następująco: „Witaj {0} Proszę znaleźć załączony {1}, którego termin upływa {2}”. Te ciągi można łatwo udostępniać na zewnątrz przy użyciu pakietów zasobów (np. W celu lokalizacji z wieloma lokalizacjami). Zastąpienie byłoby dokonane przy użyciu statycznej metody 'format' klasy MessageFormat:

String msg = "Hello {0} Please find attached {1} which is due on {2}";
String[] values = {
  "John Doe", "invoice #123", "2009-06-30"
};
System.out.println(MessageFormat.format(msg, values));
zestaw narzędzi
źródło
3
Nie mogłem sobie przypomnieć nazwy MessageFormat i to trochę głupie, ile musiałem googlować, aby znaleźć nawet tę odpowiedź. Każdy zachowuje się tak, jakby był w formacie String.format lub korzysta z innej firmy, zapominając o tym niezwykle przydatnym narzędziu.
Patrick
1
To jest dostępne od 2004 roku - dlaczego dopiero teraz dowiaduję się o tym, w 2017 roku? Robię refaktoryzację kodu, który jest omówiony w StringBuilder.append()si myślałem: „Na pewno jest lepszy sposób ... coś bardziej Pythonowego ...” - i cholera, myślę, że ta metoda może być starsza niż metody formatowania Pythona. Właściwie ... to może być starsze niż 2002 ... Nie mogę znaleźć, kiedy to faktycznie powstało ...
ArtOfWarfare
42

Możesz spróbować użyć biblioteki szablonów, takiej jak Apache Velocity.

http://velocity.apache.org/

Oto przykład:

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.StringWriter;

public class TemplateExample {
    public static void main(String args[]) throws Exception {
        Velocity.init();

        VelocityContext context = new VelocityContext();
        context.put("name", "Mark");
        context.put("invoiceNumber", "42123");
        context.put("dueDate", "June 6, 2009");

        String template = "Hello $name. Please find attached invoice" +
                          " $invoiceNumber which is due on $dueDate.";
        StringWriter writer = new StringWriter();
        Velocity.evaluate(context, writer, "TemplateName", template);

        System.out.println(writer);
    }
}

Wynik byłby następujący:

Cześć Mark. W załączniku przesyłamy fakturę 42123, której termin płatności przypada 6 czerwca 2009 roku.
hallidave
źródło
W przeszłości używałem prędkości. Działa świetnie.
Hardwareguy
4
zgadzam się, dlaczego odkrywać koło na nowo
obiekty
6
Używanie całej biblioteki do takiego prostego zadania to trochę przesada. Velocity ma wiele innych funkcji i jestem przekonany, że nie nadaje się do takiego prostego zadania.
Andrei Ciobanu
24

Możesz użyć biblioteki szablonów do złożonej wymiany szablonów.

FreeMarker to bardzo dobry wybór.

http://freemarker.sourceforge.net/

Ale w przypadku prostego zadania pomocna może być prosta klasa narzędziowa.

org.apache.commons.lang3.text.StrSubstitutor

Jest bardzo potężny, konfigurowalny i łatwy w użyciu.

Ta klasa pobiera fragment tekstu i zastępuje wszystkie zawarte w nim zmienne. Domyślna definicja zmiennej to $ {variableName}. Prefiks i sufiks można zmienić za pomocą konstruktorów i ustawionych metod.

Wartości zmiennych są zwykle rozpoznawane z mapy, ale można je również rozpoznać z właściwości systemu lub przez dostarczenie niestandardowego narzędzia do rozpoznawania zmiennych.

Na przykład, jeśli chcesz zamienić systemową zmienną środowiskową na ciąg szablonu, oto kod:

public class SysEnvSubstitutor {
    public static final String replace(final String source) {
        StrSubstitutor strSubstitutor = new StrSubstitutor(
                new StrLookup<Object>() {
                    @Override
                    public String lookup(final String key) {
                        return System.getenv(key);
                    }
                });
        return strSubstitutor.replace(source);
    }
}
Li Ying
źródło
2
org.apache.commons.lang3.text.StrSubstitutor działał świetnie dla mnie
ps0604
18
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Wynik: Hello Join! Masz 10 wiadomości ”

user2845137
źródło
3
John wyraźnie sprawdza swoje wiadomości tak często, jak ja sprawdzam mój folder „spam”, biorąc pod uwagę, że jest długi.
Hemmels,
9

Zależy to od tego, gdzie znajdują się rzeczywiste dane, które chcesz zastąpić. Możesz mieć taką mapę:

Map<String, String> values = new HashMap<String, String>();

zawierający wszystkie dane, które można zastąpić. Następnie możesz iterować po mapie i zmienić wszystko w ciągu w następujący sposób:

String s = "Your String with [Fields]";
for (Map.Entry<String, String> e : values.entrySet()) {
  s = s.replaceAll("\\[" + e.getKey() + "\\]", e.getValue());
}

Możesz także iterować po łańcuchu i znaleźć elementy na mapie. Ale to jest trochę bardziej skomplikowane, ponieważ musisz przeanalizować String w poszukiwaniu []. Możesz to zrobić za pomocą wyrażenia regularnego używając Pattern i Matcher.

Ricardo Marimon
źródło
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
źródło
2
Dzięki - ale w moim przypadku ciąg szablonu może być modyfikowany przez użytkownika, więc nie jestem pewien kolejności tokenów
Zaznacz
3

Moje rozwiązanie do zamiany tokenów w stylu $ {variable} (zainspirowane odpowiedziami tutaj i Spring UriTemplate):

public static String substituteVariables(String template, Map<String, String> variables) {
    Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}");
    Matcher matcher = pattern.matcher(template);
    // StringBuilder cannot be used here because Matcher expects StringBuffer
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
        if (variables.containsKey(matcher.group(1))) {
            String replacement = variables.get(matcher.group(1));
            // quote to work properly with $ and {,} signs
            matcher.appendReplacement(buffer, replacement != null ? Matcher.quoteReplacement(replacement) : "null");
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
mihu86
źródło
1

Dzięki Apache Commons Library możesz po prostu użyć Stringutils.replace Każdy :

public static String replaceEach(String text,
                             String[] searchList,
                             String[] replacementList)

Z dokumentacji :

Zastępuje wszystkie wystąpienia ciągów w innym łańcuchu.

Odwołanie o wartości null przekazane do tej metody jest brakiem operacji lub jeśli jakikolwiek „ciąg wyszukiwania” lub „ciąg do zastąpienia” ma wartość null, zastąpienie zostanie zignorowane. To się nie powtórzy. W przypadku powtarzających się zamian wywołaj przeciążoną metodę.

 StringUtils.replaceEach(null, *, *)        = null

  StringUtils.replaceEach("", *, *)          = ""

  StringUtils.replaceEach("aba", null, null) = "aba"

  StringUtils.replaceEach("aba", new String[0], null) = "aba"

  StringUtils.replaceEach("aba", null, new String[0]) = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"

  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"

  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
  (example of how it does not repeat)

StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
AR1
źródło
0

W przeszłości rozwiązałem ten problem za pomocą StringTemplate i Groovy Templates .

Ostatecznie decyzja o użyciu silnika szablonów lub nie powinna być oparta na następujących czynnikach:

  • Czy będziesz mieć wiele z tych szablonów w aplikacji?
  • Czy potrzebujesz możliwości modyfikowania szablonów bez ponownego uruchamiania aplikacji?
  • Kto będzie utrzymywał te szablony? Programista Java lub analityk biznesowy zaangażowany w projekt?
  • Czy będziesz potrzebować możliwości umieszczania logiki w swoich szablonach, na przykład tekstu warunkowego opartego na wartościach zmiennych?
  • Czy będziesz potrzebować możliwości uwzględnienia innych szablonów w szablonie?

Jeśli którekolwiek z powyższych dotyczy twojego projektu, rozważę użycie silnika szablonów, z których większość zapewnia tę funkcjonalność i nie tylko.

Francois Gravel
źródło
0

użyłem

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
źródło
2
To by zadziałało, ale w moim przypadku ciąg szablonu jest konfigurowalny przez użytkownika, więc nie wiem, w jakiej kolejności pojawią się tokeny.
Mark
0

Poniższy tekst zastępuje zmienne w formularzu <<VAR>>wartościami wyszukanymi z mapy. Możesz to przetestować online tutaj

Na przykład z następującym ciągiem wejściowym

BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70
Hi there <<Weight>> was here

i następujące wartości zmiennych

Weight, 42
Height, HEIGHT 51

wyprowadza następujące

BMI=(42/(HEIGHT 51*HEIGHT 51)) * 70

Hi there 42 was here

Oto kod

  static Pattern pattern = Pattern.compile("<<([a-z][a-z0-9]*)>>", Pattern.CASE_INSENSITIVE);

  public static String replaceVarsWithValues(String message, Map<String,String> varValues) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = pattern.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = varValues.get(keyName)+"";
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }


  public static void main(String args[]) throws Exception {
      String testString = "BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70\n\nHi there <<Weight>> was here";
      HashMap<String,String> values = new HashMap<>();
      values.put("Weight", "42");
      values.put("Height", "HEIGHT 51");
      System.out.println(replaceVarsWithValues(testString, values));
  }

i chociaż nie jest to wymagane, możesz użyć podobnego podejścia do zastąpienia zmiennych w łańcuchu właściwościami z pliku application.properties, chociaż może to już być zrobione:

private static Pattern patternMatchForProperties =
      Pattern.compile("[$][{]([.a-z0-9_]*)[}]", Pattern.CASE_INSENSITIVE);

protected String replaceVarsWithProperties(String message) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = patternMatchForProperties.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = System.getProperty(keyName);
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }
Brad Parks
źródło