Comment remplacer un ensemble de jetons dans une chaîne Java?

106

J'ai le modèle suivant chaîne: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

J'ai également des variables String pour le nom, le numéro de facture et la date d'échéance - quel est le meilleur moyen de remplacer les jetons du modèle par les variables?

(Notez que si une variable contient un jeton, elle ne doit PAS être remplacée).


ÉDITER

Merci à @laginimaineb et @ alan-moore, voici ma solution:

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();
}
marque
la source
Une chose à noter, cependant, est que StringBuffer est le même que StringBuilder juste synchronisé. Cependant, comme dans cet exemple, vous n'avez pas besoin de synchroniser la construction de String, vous feriez peut-être mieux d'utiliser StringBuilder (même si l'acquisition de verrous est presque une opération à coût nul).
laginimaineb
1
Malheureusement, vous devez utiliser StringBuffer dans ce cas; c'est ce à quoi s'attendent les méthodes appendXXX (). Ils existent depuis Java 4, et StringBuilder n'a été ajouté qu'à Java 5. Comme vous l'avez dit, ce n'est pas grave, juste ennuyeux.
Alan Moore
4
Encore une chose: appendReplacement (), comme les méthodes replaceXXX (), recherche des références de groupe de capture comme $ 1, $ 2, etc., et les remplace par le texte des groupes de capture associés. Si votre texte de remplacement peut contenir des signes dollar ou des barres obliques inverses (qui sont utilisés pour échapper les signes dollar), vous pouvez avoir un problème. Le moyen le plus simple de gérer cela est de diviser l'opération d'ajout en deux étapes, comme je l'ai fait dans le code ci-dessus.
Alan Moore
Alan - très impressionné que vous ayez remarqué cela. Je ne pensais pas qu'un problème aussi simple serait si difficile à résoudre!
Mark

Réponses:

65

Le moyen le plus efficace serait d'utiliser un matcher pour rechercher continuellement les expressions et les remplacer, puis d'ajouter le texte à un générateur de chaîne:

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
la source
10
C'est comme ça que je le ferais, sauf que j'utiliserais les méthodes appendReplacement () et appendTail () de Matcher pour copier le texte sans correspondance; il n'est pas nécessaire de le faire à la main.
Alan Moore
5
En fait, les méthodes appendReplacement () et appentTail () nécessitent un StringBuffer, qui est snychronisé (ce qui n'est d'aucune utilité ici). La réponse donnée utilise un StringBuilder, qui est 20% plus rapide dans mes tests.
dube
103

Je ne pense vraiment pas que vous ayez besoin d'utiliser un moteur de création de modèles ou quelque chose du genre pour cela. Vous pouvez utiliser la String.formatméthode, comme ceci:

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

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
la source
4
Un inconvénient est que vous devez mettre les paramètres dans le bon ordre
gerrytan
Un autre est que vous ne pouvez pas spécifier votre propre format de jeton de remplacement.
Franz D.
une autre est que cela ne fonctionne pas de manière dynamique, étant capable d'avoir un ensemble de données de clés / valeurs, puis de l'appliquer à n'importe quelle chaîne
Brad Parks
43

Malheureusement, la méthode confortable String.format mentionnée ci-dessus n'est disponible qu'à partir de Java 1.5 (qui devrait être assez standard de nos jours, mais on ne sait jamais). Au lieu de cela, vous pouvez également utiliser la classe MessageFormat de Java pour remplacer les espaces réservés.

Il prend en charge les espaces réservés sous la forme "{numéro}". Votre message ressemblerait donc à "Bonjour {0} Veuillez trouver la pièce jointe {1} qui est due le {2}". Ces chaînes peuvent facilement être externalisées à l'aide de ResourceBundles (par exemple pour la localisation avec plusieurs locales). Le remplacement se ferait en utilisant la méthode statique'format 'de la 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));
boîte à outils
la source
3
Je ne me souvenais pas du nom de MessageFormat, et c'est un peu ridicule combien j'ai dû faire des recherches sur Google pour trouver même cette réponse. Tout le monde agit comme si c'était String.format ou utiliser un tiers, oubliant cet utilitaire incroyablement utile.
Patrick
1
Cela est disponible depuis 2004 - pourquoi est-ce que je ne fais que découvrir maintenant, en 2017? Je refactore du code qui est couvert dans StringBuilder.append()s et je pensais "Il y a sûrement une meilleure façon ... quelque chose de plus Pythonique ..." - et merde, je pense que cette méthode peut être antérieure aux méthodes de formatage de Python. En fait ... cela peut être plus ancien que 2002 ... Je ne peux pas trouver quand cela a réellement vu le jour ...
ArtOfWarfare
42

Vous pouvez essayer d'utiliser une bibliothèque de modèles comme Apache Velocity.

http://velocity.apache.org/

Voici un exemple:

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

Le résultat serait:

Bonjour Mark. Veuillez trouver ci-joint la facture 42123 qui est due le 6 juin 2009.
hallidave
la source
J'ai utilisé la vélocité dans le passé. Fonctionne très bien.
Hardwareguy
4
d'accord, pourquoi réinventer la roue
objets
6
C'est un peu exagéré d'utiliser une bibliothèque entière pour une tâche simple comme celle-ci. Velocity a beaucoup d'autres fonctionnalités, et je crois fermement que cela ne convient pas à une tâche simple comme celle-ci.
Andrei Ciobanu
24

Vous pouvez utiliser la bibliothèque de modèles pour le remplacement de modèles complexes.

FreeMarker est un très bon choix.

http://freemarker.sourceforge.net/

Mais pour une tâche simple, il existe une classe utilitaire simple qui peut vous aider.

org.apache.commons.lang3.text.StrSubstitutor

Il est très puissant, personnalisable et facile à utiliser.

Cette classe prend un morceau de texte et remplace toutes les variables qu'il contient. La définition par défaut d'une variable est $ {variableName}. Le préfixe et le suffixe peuvent être modifiés via des constructeurs et des méthodes set.

Les valeurs de variable sont généralement résolues à partir d'une carte, mais peuvent également être résolues à partir des propriétés système ou en fournissant un résolveur de variable personnalisé.

Par exemple, si vous souhaitez remplacer la variable d'environnement système dans une chaîne de modèle, voici le code:

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
la source
2
org.apache.commons.lang3.text.StrSubstitutor a très bien fonctionné pour moi
ps0604
17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Sortie: Bonjour Rejoignez! Vous avez 10 messages "

user2845137
la source
2
John vérifie clairement ses messages aussi souvent que je vérifie mon dossier "spam" étant donné qu'il est long.
Hemmels
9

Cela dépend de l'emplacement des données réelles que vous souhaitez remplacer. Vous pourriez avoir une carte comme celle-ci:

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

contenant toutes les données pouvant être remplacées. Ensuite, vous pouvez parcourir la carte et tout modifier dans la chaîne comme suit:

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

Vous pouvez également parcourir la chaîne et trouver les éléments dans la carte. Mais c'est un peu plus compliqué car vous devez analyser la chaîne à la recherche du []. Vous pouvez le faire avec une expression régulière en utilisant Pattern et Matcher.

Ricardo Marimon
la source
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
la source
1
Merci - mais dans mon cas, la chaîne du modèle peut être modifiée par l'utilisateur, donc je ne peux pas être sûr de l'ordre des jetons
Mark
3

Ma solution pour remplacer les jetons de style $ {variable} (inspirée des réponses ici et du 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
la source
1

Avec Apache Commons Library, vous pouvez simplement utiliser Stringutils.replaceEach :

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

De la documentation :

Remplace toutes les occurrences de chaînes dans une autre chaîne.

Une référence nulle transmise à cette méthode est un no-op, ou si une "chaîne de recherche" ou "chaîne à remplacer" est nulle, ce remplacement sera ignoré. Cela ne se répétera pas. Pour répéter les remplacements, appelez la méthode surchargée.

 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
la source
1

FYI

Dans le nouveau langage Kotlin, vous pouvez utiliser des "Modèles de chaînes" directement dans votre code source, aucune bibliothèque tierce ou moteur de modèle n'a besoin de faire le remplacement de variable.

C'est une caractéristique de la langue elle-même.

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

Li Ying
la source
0

Dans le passé, j'ai résolu ce genre de problème avec StringTemplate et Groovy Templates .

En fin de compte, la décision d'utiliser ou non un moteur de création de modèles doit être basée sur les facteurs suivants:

  • Aurez-vous beaucoup de ces modèles dans l'application?
  • Avez-vous besoin de la possibilité de modifier les modèles sans redémarrer l'application?
  • Qui gérera ces modèles? Un programmeur Java ou un analyste métier impliqué sur le projet?
  • Aurez-vous besoin de la possibilité de mettre de la logique dans vos modèles, comme du texte conditionnel basé sur des valeurs dans les variables?
  • Aurez-vous besoin de la possibilité d'inclure d'autres modèles dans un modèle?

Si l'une des situations ci-dessus s'applique à votre projet, j'envisagerais d'utiliser un moteur de création de modèles, dont la plupart fournissent cette fonctionnalité, et plus encore.

François Gravel
la source
0

j'ai utilisé

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

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
la source
2
Cela fonctionnerait, mais dans mon cas, la chaîne de modèle est personnalisable par l'utilisateur, donc je ne sais pas dans quel ordre les jetons apparaîtront.
Mark le
0

Ce qui suit remplace les variables du formulaire <<VAR>>, par des valeurs recherchées à partir d'une carte. Vous pouvez le tester en ligne ici

Par exemple, avec la chaîne d'entrée suivante

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

et les valeurs de variable suivantes

Weight, 42
Height, HEIGHT 51

produit ce qui suit

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

Hi there 42 was here

Voici le code

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

et bien que cela ne soit pas demandé, vous pouvez utiliser une approche similaire pour remplacer les variables d'une chaîne par les propriétés de votre fichier application.properties, bien que cela puisse déjà être fait:

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
la source