Como substituir um conjunto de tokens em uma string Java?

106

Eu tenho o seguinte modelo de string: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]" .

Também tenho variáveis ​​String para nome, número da fatura e data de vencimento - qual é a melhor maneira de substituir os tokens no modelo pelas variáveis?

(Observe que se uma variável contém um token, ela NÃO deve ser substituída).


EDITAR

Com agradecimentos a @laginimaineb e @ alan-moore, aqui está minha solução:

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();
}
Marca
fonte
Uma coisa a ser observada, entretanto, é que StringBuffer é igual a StringBuilder que acabou de sincronizar. No entanto, como neste exemplo você não precisa sincronizar a construção da String, talvez seja melhor usar StringBuilder (embora a aquisição de bloqueios seja quase uma operação de custo zero).
laginimaineb
1
Infelizmente, você deve usar StringBuffer neste caso; é o que os métodos appendXXX () esperam. Eles existem desde o Java 4, e StringBuilder não foi adicionado até o Java 5. Como você disse, porém, não é grande coisa, apenas irritante.
Alan Moore
4
Mais uma coisa: appendReplacement (), como os métodos replaceXXX (), procura por referências de grupo de captura como $ 1, $ 2, etc., e as substitui pelo texto dos grupos de captura associados. Se o texto de substituição puder conter cifrões ou barras invertidas (que são usados ​​para escapar dos cifrões), você pode ter um problema. A maneira mais fácil de lidar com isso é dividir a operação de acréscimo em duas etapas, como fiz no código acima.
Alan Moore
Alan - muito impressionado que você notou isso. Não pensei que um problema tão simples fosse tão difícil de resolver!
Marcar

Respostas:

65

A maneira mais eficiente seria usar um matcher para encontrar continuamente as expressões e substituí-las e, em seguida, anexar o texto a um construtor de string:

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
fonte
10
É assim que eu faria, exceto que usaria os métodos appendReplacement () e appendTail () do Matcher para copiar o texto sem correspondência; não há necessidade de fazer isso manualmente.
Alan Moore
5
Na verdade, os métodos appendReplacement () e appentTail () requerem um StringBuffer, que é sincronizado (que não tem uso aqui). A resposta dada usa um StringBuilder, que é 20% mais rápido em meus testes.
dube
103

Eu realmente não acho que você precisa usar um mecanismo de modelagem ou qualquer coisa assim para isso. Você pode usar o String.formatmétodo, assim:

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

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
fonte
4
Uma desvantagem disso é que você deve colocar os parâmetros na ordem correta
gerrytan
Outra é que você não pode especificar seu próprio formato de token de substituição.
Franz D.
outra é que ele não funciona dinamicamente, sendo capaz de ter um conjunto de dados de chaves / valores e aplicá-lo a qualquer string
Brad Parks,
43

Infelizmente, o método confortável String.format mencionado acima está disponível apenas a partir do Java 1.5 (que deve ser bastante padrão hoje em dia, mas você nunca sabe). Em vez disso, você também pode usar a classe MessageFormat do Java para substituir os espaços reservados.

Ele suporta marcadores de posição no formato '{número}', então sua mensagem seria parecida com "Olá {0} Encontre em anexo {1} que vence em {2}". Essas Strings podem ser facilmente externalizadas usando ResourceBundles (por exemplo, para localização com vários locais). A substituição seria feita usando o método static'format 'da classe 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));
kit de ferramentas
fonte
3
Eu não conseguia lembrar o nome de MessageFormat, e é meio bobo o quanto eu tive que fazer no Google para encontrar até mesmo essa resposta. Todo mundo age como se fosse String.format ou use um terceiro, esquecendo esse utilitário incrivelmente útil.
Patrick
1
Isso está disponível desde 2004 - por que estou aprendendo agora, em 2017? Estou refatorando um código que está coberto em StringBuilder.append()se pensei "Certamente há uma maneira melhor ... algo mais Pythônico ..." - e puta merda, acho que esse método pode ser anterior aos métodos de formatação do Python. Na verdade ... isso pode ser anterior a 2002 ... Não consigo descobrir quando isso realmente passou a existir ...
ArtOfWarfare
42

Você pode tentar usar uma biblioteca de modelos como o Apache Velocity.

http://velocity.apache.org/

Aqui está um exemplo:

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);
    }
}

A saída seria:

Olá Mark. Veja em anexo a fatura 42123 com vencimento em 6 de junho de 2009.
Hallidave
fonte
Já usei velocidade no passado. Funciona bem.
Hardwareguy
4
concordo, por que reinventar a roda
objetos
6
É um pouco exagero usar uma biblioteca inteira para uma tarefa simples como essa. O Velocity tem muitos outros recursos e acredito fortemente que não é adequado para uma tarefa simples como essa.
Andrei Ciobanu
24

Você pode usar a biblioteca de modelos para substituição de modelos complexos.

O FreeMarker é uma escolha muito boa.

http://freemarker.sourceforge.net/

Mas para tarefas simples, existe uma classe de utilitário simples que pode ajudá-lo.

org.apache.commons.lang3.text.StrSubstitutor

É muito poderoso, personalizável e fácil de usar.

Esta classe pega um pedaço de texto e substitui todas as variáveis ​​dentro dele. A definição padrão de uma variável é $ {variableName}. O prefixo e o sufixo podem ser alterados por meio de construtores e métodos de conjunto.

Os valores das variáveis ​​são normalmente resolvidos a partir de um mapa, mas também podem ser resolvidos a partir das propriedades do sistema ou fornecendo um resolvedor de variável personalizado.

Por exemplo, se você deseja substituir a variável de ambiente do sistema em uma string de modelo, aqui está o código:

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
fonte
2
org.apache.commons.lang3.text.StrSubstitutor funcionou muito bem para mim
ps0604
17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Resultado: Olá, Junte-se! Você tem 10 mensagens "

user2845137
fonte
2
John verifica claramente suas mensagens com a mesma frequência que eu verifico minha pasta de "spam", uma vez que é um Long.
Hemmels,
9

Depende de onde os dados reais que você deseja substituir estão localizados. Você pode ter um mapa como este:

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

contendo todos os dados que podem ser substituídos. Em seguida, você pode iterar no mapa e alterar tudo na String da seguinte maneira:

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

Você também pode iterar sobre a String e encontrar os elementos no mapa. Mas isso é um pouco mais complicado porque você precisa analisar a String procurando por []. Você poderia fazer isso com uma expressão regular usando Pattern and Matcher.

Ricardo Marimon
fonte
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
fonte
1
Obrigado - mas no meu caso a string do modelo pode ser modificada pelo usuário, então não posso ter certeza da ordem dos tokens
Marcos
3

Minha solução para substituir os tokens de estilo $ {variable} (inspirado nas respostas aqui e no 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
fonte
1

Com a Apache Commons Library, você pode simplesmente usar Stringutils.replaceEach :

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

Da documentação :

Substitui todas as ocorrências de Strings em outra String.

Uma referência nula passada a este método é autônomo ou, se qualquer "string de pesquisa" ou "string a ser substituída" for nula, essa substituição será ignorada. Isso não se repetirá. Para repetir substituições, chame o método sobrecarregado.

 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
fonte
1

Para sua informação

Na nova linguagem Kotlin, você pode usar "Modelos de string" em seu código-fonte diretamente, nenhuma biblioteca de terceiros ou mecanismo de modelo precisa fazer a substituição de variáveis.

É uma característica da própria linguagem.

Consulte: https://kotlinlang.org/docs/reference/basic-types.html#string-templates

Li Ying
fonte
0

No passado, resolvi esse tipo de problema com os modelos StringTemplate e Groovy .

Em última análise, a decisão de usar um mecanismo de modelagem ou não deve ser baseada nos seguintes fatores:

  • Você terá muitos desses modelos no aplicativo?
  • Você precisa poder modificar os modelos sem reiniciar o aplicativo?
  • Quem manterá esses modelos? Um programador Java ou um analista de negócios envolvido no projeto?
  • Você precisará ter a capacidade de colocar lógica em seus modelos, como texto condicional baseado em valores nas variáveis?
  • Você precisará incluir outros modelos em um modelo?

Se alguma das situações acima se aplicar ao seu projeto, consideraria o uso de um mecanismo de modelos, a maioria dos quais fornece essa funcionalidade e muito mais.

Francois Gravel
fonte
0

eu usei

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

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
fonte
2
Isso funcionaria, mas no meu caso a string do modelo é personalizável pelo usuário, então não sei em que ordem os tokens aparecerão.
Marcos
0

O seguinte substitui variáveis ​​do formulário <<VAR>>, com valores pesquisados ​​em um mapa. Você pode testá-lo online aqui

Por exemplo, com a seguinte string de entrada

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

e os seguintes valores de variáveis

Weight, 42
Height, HEIGHT 51

produz o seguinte

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

Hi there 42 was here

Aqui está o código

  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));
  }

e, embora não seja solicitado, você pode usar uma abordagem semelhante para substituir variáveis ​​em uma string por propriedades de seu arquivo application.properties, embora isso já possa estar sendo feito:

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
fonte