¿Cómo reemplazar un conjunto de tokens en una cadena Java?

106

Tengo la siguiente cadena de plantilla: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]" .

También tengo variables de cadena para el nombre, el número de factura y la fecha de vencimiento: ¿cuál es la mejor manera de reemplazar los tokens en la plantilla con las variables?

(Tenga en cuenta que si una variable contiene un token, NO debe reemplazarse).


EDITAR

Gracias a @laginimaineb y @ alan-moore, aquí está mi solución:

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
fuente
Sin embargo, una cosa a tener en cuenta es que StringBuffer es lo mismo que StringBuilder recién sincronizado. Sin embargo, dado que en este ejemplo no necesita sincronizar la construcción de String, es mejor que use StringBuilder (aunque adquirir bloqueos es una operación de costo casi cero).
laginimaineb
1
Desafortunadamente, debe usar StringBuffer en este caso; es lo que esperan los métodos appendXXX (). Han existido desde Java 4, y StringBuilder no se agregó hasta Java 5. Sin embargo, como dijiste, no es gran cosa, solo es molesto.
Alan Moore
4
Una cosa más: appendReplacement (), como los métodos replaceXXX (), busca referencias de grupos de captura como $ 1, $ 2, etc., y las reemplaza con el texto de los grupos de captura asociados. Si el texto de reemplazo puede contener signos de dólar o barras invertidas (que se usan para escapar de los signos de dólar), podría tener un problema. La forma más fácil de lidiar con eso es dividir la operación de adición en dos pasos como lo hice en el código anterior.
Alan Moore
Alan: muy impresionado que lo hayas visto. ¡No pensé que un problema tan simple fuera tan difícil de resolver!
Mark

Respuestas:

65

La forma más eficiente sería usar un comparador para buscar continuamente las expresiones y reemplazarlas, luego agregar el texto a un generador de cadenas:

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
fuente
10
Así es como lo haría, excepto que usaría los métodos appendReplacement () y appendTail () de Matcher para copiar el texto no coincidente; no es necesario hacerlo a mano.
Alan Moore
5
En realidad, los métodos appendReplacement () y appentTail () requieren un StringBuffer, que está sincronizado (que no sirve de nada aquí). La respuesta dada usa un StringBuilder, que es un 20% más rápido en mis pruebas.
dube
103

Realmente no creo que necesites usar un motor de plantillas ni nada por el estilo para esto. Puedes usar el String.formatmétodo, así:

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

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
fuente
4
Una desventaja de esto es que tienes que poner los parámetros en el orden correcto
gerrytan
Otra es que no puede especificar su propio formato de token de reemplazo.
Franz D.
otra es que no funciona dinámicamente, pudiendo tener un conjunto de datos de claves / valores y luego aplicarlo a cualquier cadena
Brad Parks
43

Desafortunadamente, el cómodo método String.format mencionado anteriormente solo está disponible a partir de Java 1.5 (que debería ser bastante estándar hoy en día, pero nunca se sabe). En lugar de eso, también puede usar la clase MessageFormat de Java para reemplazar los marcadores de posición.

Admite marcadores de posición en el formulario '{número}', por lo que su mensaje se vería como "Hola, {0} se adjunta {1} que vence el {2}". Estas cadenas se pueden externalizar fácilmente utilizando ResourceBundles (por ejemplo, para la localización con múltiples configuraciones regionales). El reemplazo se haría usando el método static'format 'de la clase 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));
caja de herramientas
fuente
3
No podía recordar el nombre de MessageFormat, y es un poco tonto lo mucho que tuve que buscar en Google para encontrar incluso esta respuesta. Todo el mundo actúa como si fuera String.format o utilizara un tercero, olvidando esta utilidad increíblemente útil.
Patrick
1
Esto ha estado disponible desde 2004. ¿Por qué solo estoy aprendiendo ahora, en 2017? Estoy refactorizando un código que está cubierto en StringBuilder.append()sy estaba pensando "Seguramente hay una mejor manera ... algo más Pythonic ..." - y mierda, creo que este método puede ser anterior a los métodos de formato de Python. En realidad ... esto puede ser más antiguo que 2002 ... no puedo encontrar cuando esto realmente llegó a existir ...
ArtOfWarfare
42

Podría intentar usar una biblioteca de plantillas como Apache Velocity.

http://velocity.apache.org/

Aquí hay un ejemplo:

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

La salida sería:

Hola Mark. Se adjunta la factura 42123 que vence el 6 de junio de 2009.
hallidave
fuente
He usado la velocidad en el pasado. Funciona genial.
Hardwareguy
4
de acuerdo, por qué reinventar la rueda
objetos
6
Es un poco exagerado usar una biblioteca completa para una tarea tan simple como esta. Velocity tiene muchas otras características, y creo firmemente que no es adecuado para una tarea tan simple como esta.
Andrei Ciobanu
24

Puede utilizar la biblioteca de plantillas para reemplazar plantillas complejas.

FreeMarker es una muy buena opción.

http://freemarker.sourceforge.net/

Pero para una tarea simple, hay una clase de utilidad simple que puede ayudarlo.

org.apache.commons.lang3.text.StrSubstitutor

Es muy potente, personalizable y fácil de usar.

Esta clase toma un fragmento de texto y sustituye todas las variables que contiene. La definición predeterminada de una variable es $ {variableName}. El prefijo y el sufijo se pueden cambiar mediante constructores y métodos establecidos.

Los valores de las variables se resuelven normalmente a partir de un mapa, pero también se pueden resolver a partir de las propiedades del sistema o proporcionando un solucionador de variables personalizado.

Por ejemplo, si desea sustituir la variable de entorno del sistema en una cadena de plantilla, aquí está el 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
fuente
2
org.apache.commons.lang3.text.StrSubstitutor funcionó muy bien para mí
ps0604
17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Salida: ¡Hola, únete! Tienes 10 mensajes "

usuario2845137
fuente
2
John revisa claramente sus mensajes tan a menudo como yo reviso mi carpeta de "spam" dado que es Long.
Hemmels
9

Depende de dónde se encuentren los datos reales que desee reemplazar. Es posible que tenga un mapa como este:

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

que contiene todos los datos que se pueden reemplazar. Luego, puede iterar sobre el mapa y cambiar todo en la Cadena de la siguiente manera:

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

También puede iterar sobre la Cadena y encontrar los elementos en el mapa. Pero eso es un poco más complicado porque necesita analizar la Cadena buscando []. Puede hacerlo con una expresión regular usando Pattern y Matcher.

Ricardo Marimón
fuente
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
fuente
1
Gracias, pero en mi caso, el usuario puede modificar la cadena de la plantilla, por lo que no puedo estar seguro del orden de los tokens
Mark
3

Mi solución para reemplazar los tokens de estilo $ {variable} (inspirado en las respuestas aquí y en 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
fuente
1

Con Apache Commons Library, simplemente puede usar Stringutils.replaceEach :

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

De la documentación :

Reemplaza todas las apariciones de cadenas dentro de otra cadena.

Una referencia nula pasada a este método es una operación no operativa, o si cualquier "cadena de búsqueda" o "cadena para reemplazar" es nula, ese reemplazo será ignorado. Esto no se repetirá. Para repetir reemplazos, llame al método sobrecargado.

 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
fuente
0

En el pasado, resolví este tipo de problema con StringTemplate y Groovy Templates .

En última instancia, la decisión de utilizar un motor de plantillas o no debe basarse en los siguientes factores:

  • ¿Tendrá muchas de estas plantillas en la aplicación?
  • ¿Necesita la capacidad de modificar las plantillas sin reiniciar la aplicación?
  • ¿Quién mantendrá estas plantillas? ¿Un programador de Java o un analista de negocios involucrado en el proyecto?
  • ¿Necesitará la capacidad de poner lógica en sus plantillas, como texto condicional basado en valores en las variables?
  • ¿Necesitará la capacidad de incluir otras plantillas en una plantilla?

Si algo de lo anterior se aplica a su proyecto, consideraría usar un motor de plantillas, la mayoría de los cuales brindan esta funcionalidad y más.

Francois Gravel
fuente
0

solía

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

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
fuente
2
Eso funcionaría, pero en mi caso, el usuario puede personalizar la cadena de la plantilla, por lo que no sé en qué orden aparecerán los tokens.
Mark
0

Lo siguiente reemplaza las variables del formulario <<VAR>>con valores buscados en un mapa. Puedes probarlo online aquí

Por ejemplo, con la siguiente cadena de entrada

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

y los siguientes valores de variable

Weight, 42
Height, HEIGHT 51

produce lo siguiente

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

Hi there 42 was here

Aqui esta el codigo

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

y aunque no se solicite, puede usar un enfoque similar para reemplazar variables en una cadena con propiedades de su archivo application.properties, aunque es posible que esto ya se esté haciendo:

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
fuente