3 文字列テンプレート

文字列テンプレートは、リテラル・テキストと埋込み式およびテンプレート・プロセッサを組み合せて特殊な結果を生成することにより、Javaの既存の文字列リテラルおよびテキスト・ブロックを補完します。埋込み式はJava式ですが、文字列テンプレートのリテラル・テキストと区別するための追加の構文があります。テンプレート・プロセッサは、テンプレートのリテラル・テキストを埋込み式の値と組み合せて結果を生成します。

詳細は、Java SE API仕様のStringTemplateインタフェースを参照してください。

ノート:

これはプレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。

文字列テンプレートの背景情報は、JEP 430を参照してください。

文字列テンプレートの基本的な使用方法

次の例では、テンプレート・プロセッサSTRを使用し、1つの埋込み式nameを含むテンプレート式を宣言します。

String name = "Duke";
String info = STR."My name is \{name}";
System.out.println(info);

出力は次のようになります。

My name is Duke

テンプレート・プロセッサSTRは、JDKに含まれているテンプレート・プロセッサの1つです。文字列補間は、テンプレート内の各埋込み式を文字列に変換された値で置き換えることによって自動的に実行されます。JDKには、他の2つのテンプレート・プロセッサが含まれています。

  • FMTテンプレート・プロセッサ: STRテンプレート・プロセッサと似ていますが、java.util.Formatterで定義されている書式指定子およびロケール情報を(printfメソッド呼出しと同様の方法で)受け入れる点が異なります。
  • RAWテンプレート・プロセッサ: STRテンプレート・プロセッサのような文字列テンプレートは自動的に処理されません。これを使用すると、独自のテンプレート・プロセッサを作成できます。StringTemplate.Processorインタフェースを実装してテンプレート・プロセッサを作成することもできます。「テンプレート・プロセッサの作成」を参照してください。

テンプレート・プロセッサの後にはドット(.)が続きます。これにより、テンプレート式の外観は、オブジェクトのフィールドへのアクセスやオブジェクトのメソッドの呼出しのようになります。

ドット文字の後には、1つ以上の埋込み式を含む文字列である文字列テンプレートが続きます。埋込み式は、バックスラッシュと左中カッコ(\{)および右中カッコ}で囲まれたJava式です。その他の例は、「文字列テンプレートの埋込み式」を参照してください。

文字列テンプレートの埋込み式

文字列テンプレートの埋込み式として、任意のJava式を使用できます。

文字列および文字を埋込み式として使用できます。

String user = "Duke";   
char option = 'b';
String choice  = STR."\{user} has chosen option \{option}";
System.out.println(choice);

この例の出力は次のとおりです。

Duke has chosen option b

埋込み式は算術演算を実行できます。

double x = 10.5, y = 20.6;
String p = STR."\{x} * \{y} = \{x * y}";
System.out.println(p);

この例の出力は次のとおりです。

10.5 * 20.6 = 216.3

Java式と同様に、埋込み式は左から右に評価されます。また、接頭辞演算子と接尾辞演算子を含めることもできます。

int index = 0;
String data = STR."\{index++}, \{index++}, \{++index}, \{index++}, \{index}";
System.out.println(data);

この例の出力は次のとおりです。

0, 1, 3, 3, 4

埋込み式では、メソッドを呼び出し、フィールドにアクセスできます。

String time = STR."Today is \{java.time.LocalDate.now()}";
System.out.println(time);
String canLang = STR."The language code of \{
    Locale.CANADA_FRENCH} is \{
    Locale.CANADA_FRENCH.getLanguage()}";
System.out.println(canLang);

この例では、次のような出力が表示されます:

Today is 2023-07-11
The language code of fr_CA is fr

埋込み式に改行を挿入でき、前の例のように結果に改行は挿入されないことに注意してください。また、埋込み式の引用符をエスケープする必要はありません。

Path filePath = Paths.get("Stemp.java");
String msg = STR."The file \{filePath} \{
    // The Files class is in the package java.nio.file
    Files.exists(filePath) ? "does" : "does not"} exist";
System.out.println(msg);

String currentTime = STR."The time is \{
    DateTimeFormatter
        .ofPattern("HH:mm:ss")
        .format(LocalTime.now())
} right now";
System.out.println(currentTime);

この例では、次のような出力が表示されます:

The file Stemp.java does exist
The time is 11:32:38 right now

テンプレート式を文字列テンプレートに埋め込むことができます。

String[] a = { "X", "Y", "Z" };
String letters = STR."\{a[0]}, \{STR."\{a[1]}, \{a[2]}"}";
System.out.println(letters);

この例の出力は次のとおりです。

X, Y, Z

この例をより明確にするために、埋込みテンプレート式を変数に置き換えることができます。

String temp     = STR."\{a[1]}, \{a[2]}";
String letters2 = STR."\{a[0]}, \{temp}";
System.out.println(letters2);

複数行文字列テンプレート

文字列テンプレートのテンプレートを指定する場合は、通常の文字列のかわりにテキスト・ブロックを使用できます。詳細は、「テキスト・ブロック」を参照してください。これにより、次の例に示すように、文字列テンプレートを使用してHTMLドキュメントおよびJSONデータをより簡単に使用できます。

String title = "My Web Page";
String text = "Hello, world";
String webpage = STR."""
    <html>
      <head>
        <title>\{title}</title>
      </head>
      <body>
        <p>\{text}</p>
      </body>
    </html>
    """;
System.out.println(webpage);

この例の出力は次のとおりです。

<html>
  <head>
    <title>My Web Page</title>
  </head>
  <body>
    <p>Hello, world</p>
  </body>
</html>

次の例では、JSONデータを作成しています。

String customerName    = "Java Duke";
String phone           = "555-123-4567";
String address         = "1 Maple Drive, Anytown";
String json = STR."""
{
    "name":    "\{customerName}",
    "phone":   "\{phone}",
    "address": "\{address}"
}
""";

出力は次のようになります。

{
    "name":    "Java Duke",
    "phone":   "555-123-4567",
    "address": "1 Maple Drive, Anytown"
}

テキスト・ブロックを使用すると、表形式データをより明確に操作できます。

record Rectangle(String name, double width, double height) {
    double area() {
        return width * height;
    }
}

Rectangle[] zone = new Rectangle[] {
    new Rectangle("First",  17.8, 31.4),
    new Rectangle("Second",  9.6, 12.2),
};
    
String table = STR."""
    Description\tWidth\tHeight\tArea
    \{zone[0].name}\t\t\{zone[0].width}\t\{zone[0].height}\t\{zone[0].area()}
    \{zone[1].name}\t\t\{zone[1].width}\t\{zone[1].height}\t\{zone[1].area()}
    Total \{zone[0].area() + zone[1].area()}
    """;
    
System.out.println(table);

この例の出力は次のとおりです。

Description     Width   Height  Area
First           17.8    31.4    558.92
Second          9.6     12.2    117.11999999999999
Total 676.04

FMTテンプレート・プロセッサ

FMTテンプレート・プロセッサは、埋込み式の左側に表示される書式指定子を使用できる点を除き、STRテンプレート・プロセッサと同様です。これらの書式指定子は、クラスjava.util.Formatterで定義されているものと同じです。

次の例は、「複数行文字列テンプレート」の表形式データの例と同じですが、より適切に書式設定されています。

String formattedTable = FormatProcessor.FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{zone[0].height}     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{zone[1].height}     %7.2f\{zone[1].area()}
    \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area()}
    """;    
    
System.out.println(formattedTable);

出力は次のようになります。

Description     Width    Height     Area
First           17.80    31.40      558.92
Second           9.60    12.20      117.12
                             Total  676.04

RAWテンプレート・プロセッサ

RAWテンプレート・プロセッサは、テンプレートの処理を遅延させます。したがって、テンプレートの文字列リテラルおよび埋込み式の結果を処理前に取得できます。

テンプレートの文字列リテラルおよび埋込み式の結果を取得するには、それぞれStringTemplate::fragmentsおよびStringTemplate::valuesを呼び出します。文字列テンプレートを処理するには、StringTemplate.process(StringTemplate.Processor)またはStringTemplate.Processor.process(StringTemplate)を呼び出します。STRテンプレート・プロセッサと同じ結果を返すメソッドStringTemplate::interpolateを呼び出すこともできます。

次の例にこれらのメソッドを示します。

int v = 10, w = 20;
StringTemplate rawST = StringTemplate.RAW."\{v} plus \{w} equals \{v + w}";
java.util.List<String> fragments = rawST.fragments();
java.util.List<Object> values = rawST.values();
    
System.out.println(rawST.toString());
    
fragments.stream().forEach(f -> System.out.print("[" + f + "]"));
System.out.println();
    
values.stream().forEach(val -> System.out.print("[" + val + "]"));
System.out.println();
    
System.out.println(rawST.process(STR));
System.out.println(STR.process(rawST));
System.out.println(rawST.interpolate());

出力は次のようになります。

StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
[][ plus ][ equals ][]
[10][20][30]
10 plus 20 equals 30
10 plus 20 equals 30
10 plus 20 equals 30

メソッドStringTemplate::fragmentsは、フラグメント・リテラルのリストを返します。これは、各埋込み式の前の文字シーケンスに、最後の埋込み式の後の文字シーケンスを加えたものです。この例では、テンプレートの最初と最後に埋込み式が出現するため、最初と最後のフラグメント・リテラルは長さゼロの文字列です。

メソッドStringTemplate::valuesは、埋込み式の結果のリストを返します。これらの値は、文字列テンプレートが評価されるたびに計算されます。ただし、フラグメント・リテラルは、テンプレート式のすべての評価で一定です。これを示す例を次に示します。

int t = 20;
for (int u = 0; u < 3; u++) {
    StringTemplate stLoop = StringTemplate.RAW."\{t} plus \{u} equals \{t + u}";
    System.out.println("Fragments: " + stLoop.fragments());
    System.out.println("Values:    " + stLoop.values());
}

出力は次のようになります。

Fragments: [,  plus ,  equals , ]
Values:    [20, 0, 20]
Fragments: [,  plus ,  equals , ]
Values:    [20, 1, 21]
Fragments: [,  plus ,  equals , ]
Values:    [20, 2, 22]

テンプレート・プロセッサの作成

StringTemplate.Processorインタフェースを実装することで、独自のテンプレート・プロセッサを作成できます。このプロセッサは、Stringだけでなく任意のタイプのオブジェクトを返し、処理が失敗した場合はチェック例外をスローできます。

JSONオブジェクトを返すテンプレート・プロセッサの作成

次に、JSONオブジェクトを返すテンプレート・プロセッサの例を示します。

ノート:

これらの例では、Jakarta JSON Processing APIを含むjakarta.jsonパッケージのクラスJsonJsonExceptionJsonObjectおよびJsonReaderを使用します。また、Jakarta JSON Processing仕様の実装であるEclipse Parssonのorg.eclipse.parsson.JsonProviderImplクラスも間接的に使用します。これらのAPIのライブラリをEclipse GlassFishから取得します。
var JSON = StringTemplate.Processor.of(
    (StringTemplate stJSON) -> {
        try (JsonReader jsonReader = Json.createReader(new StringReader(
            stJSON.interpolate()))) {
            return jsonReader.readObject();
        }    
    }
);

String accountType = "user";
String userName = "Duke";
String pw = "my_password";

JsonObject newAccount = JSON."""
{
    "account":    "\{accountType}",
    "user":       "\{userName}",
    "password":   "\{pw}"
}
""";

System.out.println(newAccount);

userName = "Duke\", \"account\": \"administrator";

newAccount = JSON."""
{
    "account":    "\{accountType}",
    "user":       "\{userName}",
    "password":   "\{pw}"
}
""";

System.out.println(newAccount);

出力は次のようになります。

{"account":"user","user":"Duke","password":"my_password"}
{"account":"administrator","user":"Duke","password":"my_password"}

この例には問題があります。あるタイプのJSONインジェクション攻撃の影響を受ける可能性があります。このタイプの攻撃では、引用符を含む悪意のあるデータをJSON文字列に挿入し、JSON文字列自体を変更します。この例では、ユーザー名を"Duke\", \"account\": \"administrator"に変更します。結果のJSON文字列は次のようになります。

{
    "account":    "user",
    "user":       "Duke",
    "account":    "administrator",
    "password":   "my_password"
}

JSONパーサーが同じ名前のエントリを検出した場合、最後のエントリが使用されます。そのため、ユーザーDukeには管理者権限があります。

次の例では、この種のJSONインジェクション攻撃に対処します。テンプレートに引用符付きの文字列が含まれている場合は、例外がスローされます。その値のいずれかが数値またはブール値でない場合も例外がスローされます。

StringTemplate.Processor<JsonObject, JsonException> JSON_VALIDATE =
    (StringTemplate stJVAL) -> {
    String[] invalidStrings = new String[] { "\"", "'" };
    List<Object> filtered = new ArrayList<>();
    for (Object value : stJVAL.values()) {
        if (value instanceof String str) {
            if (Arrays.stream(invalidStrings).anyMatch(str::contains)) {
                throw new JsonException("Injection vulnerability");
            }
            filtered.add(str);
        } else if (value instanceof Number ||
                   value instanceof Boolean) {
            filtered.add(value);
        } else {
            throw new JsonException("Invalid value type");
        }
    }
    
    String jsonSource =
        StringTemplate.interpolate(stJVAL.fragments(), filtered);

    try (JsonReader jsonReader = Json.createReader(new StringReader(
        jsonSource))) {
        return jsonReader.readObject();
    }             
};        
    
String accountType = "user";
String userName = "Duke";
String pw = "my_password";

try {
    JsonObject newAccount = JSON_VALIDATE."""
    {
        "account":    "\{accountType}",
        "user":       "\{userName}",
        "password":   "\{pw}"
    }
    """;

    System.out.println(newAccount);

    userName = "Duke\", \"account\": \"administrator";

    newAccount = JSON_VALIDATE."""
    {
        "account":    "\{accountType}",
        "user":       "\{userName}",
        "password":   "\{pw}"
    }
    """;

    System.out.println(newAccount);
    
} catch (JsonException ex) {
    System.out.println(ex.getMessage());
}

出力は次のようになります。

{"account":"user","user":"Duke","password":"my_password"}
Injection vulnerability

データベース問合せを安全に作成および実行するテンプレート・プロセッサの作成

準備された文を返すテンプレート・プロセッサを作成できます。

SUPPLIERS表から特定のコーヒー・サプライヤ(SUP_NAMEで識別)に関する情報を取得する次の例を考えてみます。このデータベース表の作成と移入の方法など、このデータベース表の詳細は、Javaチュートリアル(Java SE 8以前)のJDBCチュートリアルのSUPPLIERS表に関する項を参照してください。

  public static void getSupplierInfo(Connection con, String supName) throws SQLException {
    String query = "SELECT * from SUPPLIERS s WHERE s.SUP_NAME = '" + supName + "'";
      try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        System.out.println("ID   " +
                           "Name                       " +
                           "Street               " +
                           "City          State  Zip");
        while (rs.next()) {
          int supplierID = rs.getInt("SUP_ID");
          String supplierName = rs.getString("SUP_NAME");
          String street = rs.getString("STREET");
          String city = rs.getString("CITY");
          String state = rs.getString("STATE");
          String zip = rs.getString("ZIP");
          String supRow = FormatProcessor.FMT."%-4s\{supplierID} %-26s\{
            supplierName} %-20s\{street} %-13s\{city} %-6s\{state} \{zip}";
          System.out.println(supRow);
        }
      } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
  }

ノート:

このメソッドをSuppliersTable.javaに追加できます。

このメソッドを次のように呼び出すとします。

SuppliersTable.getSupplierInfoST(myConnection, "Superior Coffee");

このメソッドの出力は次のようになります。

ID   Name                       Street               City          State  Zip
49   Superior Coffee            1 Party Place        Mendocino     CA     95460

この例には問題があります。SQLインジェクション攻撃の影響を受ける可能性があります。このメソッドを次のように呼び出すとします。

SuppliersTable.getSupplierInfo(
  myConnection, "Superior Coffee' OR s.SUP_NAME <> 'Superior Coffee");

SQL問合せは次のようになります。

SELECT * from SUPPLIERS s WHERE
  s.SUP_NAME = 'Superior Coffee' OR
  s.SUP_NAME <> 'Superior Coffee'

Statementオブジェクトは、「無効」なサプライヤ名をSQL文の一部として処理します。その結果、メソッドによりSUPPLIERS表のすべてのエントリが出力され、機密情報が公開される可能性があります。

プリペアド文は、SQLインジェクション攻撃の防止に役立ちます。クライアントが提供するデータは常にパラメータの内容として扱われ、SQL文の一部としては扱われません。次の例では、文字列テンプレートからSQL問合せ文字列を作成し、問合せ文字列からJDBC PreparedStatementを作成し、そのパラメータを埋込み式の値に設定します。

  record QueryBuilder(Connection conn)
    implements StringTemplate.Processor<PreparedStatement, SQLException> {

      public PreparedStatement process(StringTemplate st) throws SQLException {
        // 1. Replace StringTemplate placeholders with PreparedStatement placeholders
        String query = String.join("?", st.fragments());

        // 2. Create the PreparedStatement on the connection
        PreparedStatement ps = conn.prepareStatement(query);

        // 3. Set parameters of the PreparedStatement
        int index = 1;
        for (Object value : st.values()) {
            switch (value) {
                case Integer i -> ps.setInt(index++, i);
                case Float f   -> ps.setFloat(index++, f);
                case Double d  -> ps.setDouble(index++, d);
                case Boolean b -> ps.setBoolean(index++, b);
                default        -> ps.setString(index++, String.valueOf(value));
            }
        }
        return ps;
    }
  }     

次の例は、QueryBuilderテンプレート・プロセッサを使用する点を除き、getSupplierInfoの例と同様です。

  public static void getSupplierInfoST(Connection con, String supName) throws SQLException {
      var DB = new QueryBuilder(con);
      try (PreparedStatement ps = DB."SELECT * from SUPPLIERS s WHERE s.SUP_NAME = \{supName}") {
        ResultSet rs = ps.executeQuery();
        System.out.println("ID   " +
                           "Name                       " +
                           "Street               " +
                           "City          State  Zip");
        while (rs.next()) {
          int supplierID = rs.getInt("SUP_ID");
          String supplierName = rs.getString("SUP_NAME");
          String street = rs.getString("STREET");
          String city = rs.getString("CITY");
          String state = rs.getString("STATE");
          String zip = rs.getString("ZIP");
          String supRow = FormatProcessor.FMT."%-4s\{supplierID} %-26s\{
            supplierName} %-20s\{street} %-13s\{city} %-6s\{state} \{zip}";
          System.out.println(supRow);
        }
      } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
  } 

リソース・バンドルの使用を簡略化するテンプレート・プロセッサの作成

次のテンプレート・プロセッサLocalizationProcessorは、文字列をリソース・バンドルの対応するプロパティにマップします。このテンプレート・プロセッサを使用する場合、リソース・バンドルでは、プロパティ名はアプリケーション内の文字列テンプレートであり、埋込み式はアンダースコア(_)で置換され、スペースはピリオド(.)で置換されます。

record LocalizationProcessor(Locale locale)
    implements StringTemplate.Processor<String, RuntimeException> {
    
    public String process(StringTemplate st) {
        ResourceBundle resource = ResourceBundle.getBundle("resources", locale);
        String stencil = String.join("_", st.fragments());
        String msgFormat = resource.getString(stencil.replace(' ', '.'));
        return MessageFormat.format(msgFormat, st.values().toArray());
    }
}

次のリソース・バンドルがあるとします。

図3-1 resources_en_US.properties

# resources_en_US.properties file
_.chose.option._=\
    {0} chose option {1}

図3-2 resources_fr_CA.properties

# resources_fr_CA.properties file
_.chose.option._=\
    {0} a choisi l''option {1}

次の例では、LocalizationProcessorおよびこれらの2つのリソース・バンドルを使用します:

var userLocale = new Locale("en", "US");
var LOCALIZE = new LocalizationProcessor(userLocale);

String user = "Duke", option = "b";
System.out.println(LOCALIZE."\{user} chose option \{option}");  
    
userLocale = new Locale("fr", "CA");
LOCALIZE = new LocalizationProcessor(userLocale);
System.out.println(LOCALIZE."\{user} chose option \{option}"); 

出力は次のようになります。

Duke chose option b
Duke a choisi l'option b