| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.internal.util; |
| |
| import java.lang.reflect.Array; |
| import java.util.Formatter; |
| import java.util.FormattableFlags; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.util.Classes; |
| import org.apache.sis.util.Characters; |
| import org.apache.sis.util.CharSequences; |
| |
| |
| /** |
| * Miscellaneous utilities which should not be put in public API. |
| * Most of those methods are for {@link Object#toString()} implementations. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.0 |
| * @since 0.3 |
| * @module |
| */ |
| public final class Strings extends Static { |
| /** |
| * The character to write at the beginning of lines that are continuation of a single log record. |
| * This constant is defined here only for a little bit more uniform {@code toString()} in SIS. |
| */ |
| public static final char CONTINUATION_MARK = '┃', CONTINUATION_END = '╹'; |
| |
| /** |
| * Characters for a new item in a block illustrated by {@link #CONTINUATION_MARK}. |
| * This constant is defined here only for a little bit more uniform {@code toString()} in SIS. |
| */ |
| public static final String CONTINUATION_ITEM = "▶ "; |
| |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private Strings() { |
| } |
| |
| /** |
| * Appends to the given buffer only the characters that are valid for a Unicode identifier. |
| * The given separator character is append before the given {@code text} only if the buffer |
| * is not empty and at least one {@code text} character is valid. |
| * |
| * <div class="section">Relationship with {@code gml:id}</div> |
| * This method may be invoked for building {@code gml:id} values. Strictly speaking this is not appropriate |
| * since the {@code xs:ID} type defines valid identifiers as containing only letters, digits, underscores, |
| * hyphens, and periods. This differ from Unicode identifier in two ways: |
| * |
| * <ul> |
| * <li>Unicode identifiers accept Japanese or Chinese ideograms for instance, which are considered as letters.</li> |
| * <li>Unicode identifiers do not accept the {@code '-'} and {@code ':'} characters. However this restriction |
| * fits well our need, since those characters are typical values for the {@code separator} argument.</li> |
| * <li>Note that {@code '_'} is valid both in {@code xs:ID} and Unicode identifier.</li> |
| * </ul> |
| * |
| * @param appendTo the buffer where to append the valid characters. |
| * @param separator the separator to append before the valid characters, or 0 if none. |
| * @param text the text from which to get the valid character to append in the given buffer. |
| * @param accepted additional characters to accept (e.g. {@code "-."}), or an empty string if none. |
| * @param toLowerCase {@code true} for converting the characters to lower case. |
| * @return {@code true} if at least one character has been added to the buffer. |
| */ |
| public static boolean appendUnicodeIdentifier(final StringBuilder appendTo, final char separator, |
| final String text, final String accepted, final boolean toLowerCase) |
| { |
| boolean added = false; |
| boolean toUpperCase = false; |
| if (text != null) { |
| for (int i=0; i<text.length();) { |
| final int c = text.codePointAt(i); |
| final boolean isFirst = appendTo.length() == 0; |
| if ((isFirst ? Character.isUnicodeIdentifierStart(c) |
| : Character.isUnicodeIdentifierPart(c)) || accepted.indexOf(c) >= 0) |
| { |
| if (!isFirst && !added && separator != 0) { |
| appendTo.append(separator); |
| } |
| appendTo.appendCodePoint(toLowerCase ? Character.toLowerCase(c) : |
| toUpperCase ? Character.toUpperCase(c) : c); |
| added = true; |
| toUpperCase = false; |
| } else { |
| toUpperCase = true; |
| } |
| i += Character.charCount(c); |
| } |
| } |
| return added; |
| } |
| |
| /** |
| * Appends {@code "[index]"} to the given name. This is used for formatting error messages. |
| * |
| * @param name the variable name to which to append "[index]". |
| * @param index value to write between brackets. |
| * @return {@code "name[index]"}. |
| */ |
| public static String toIndexed(final String name, final int index) { |
| return name + '[' + index + ']'; |
| } |
| |
| /** |
| * Formats {@code "name[index]"}. |
| * |
| * @param name the variable name to which to append "[index]". |
| * @param index value to write between brackets. |
| * @return {@code "name[index]"}. |
| */ |
| public static String bracket(final String name, final Object index) { |
| if (index instanceof CharSequence) { |
| return name + "[“" + index + "”]"; |
| } else { |
| return name + '[' + index + ']'; |
| } |
| } |
| |
| /** |
| * Formats {@code "classname[index]"}. |
| * |
| * @param type the type to which to append "[index]". |
| * @param index value to write between brackets. |
| * @return {@code "classname[index]"}. |
| */ |
| public static String bracket(final Class<?> type, final Object index) { |
| return bracket(Classes.getShortName(type), index); |
| } |
| |
| /** |
| * Formats {@code "classname[lower … upper]"}. |
| * |
| * @param type the type to which to append "[lower … upper]". |
| * @param lower first value to write between brackets. |
| * @param upper second value to write between brackets. |
| * @return {@code "classname[lower … upper]"}. |
| */ |
| public static String range(final Class<?> type, final Object lower, final Object upper) { |
| return Classes.getShortName(type) + '[' + lower + " … " + upper + ']'; |
| } |
| |
| /** |
| * Returns a string with the same content than the given string, but in upper case and containing only the |
| * filtered characters. If the given string already matches the criterion, then it is returned unchanged |
| * without creation of any temporary object. |
| * |
| * <p>This method is useful before call to an {@code Enum.valueOf(String)} method, for making the search |
| * a little bit more tolerant.</p> |
| * |
| * <p>This method is not in public API because conversion to upper-cases should be locale-dependent.</p> |
| * |
| * @param text the text to filter. |
| * @param filter the filter to apply. |
| * @return the filtered text. |
| */ |
| public static String toUpperCase(final String text, final Characters.Filter filter) { |
| final int length = text.length(); |
| int c, i = 0; |
| while (true) { |
| if (i >= length) { |
| return text; |
| } |
| c = text.codePointAt(i); |
| if (!filter.contains(c) || Character.toUpperCase(c) != c) { |
| break; |
| } |
| i += Character.charCount(c); |
| } |
| /* |
| * At this point we found that characters starting from index i does not match the criterion. |
| * Copy what we have checked so far in the buffer, then add next characters one-by-one. |
| */ |
| final StringBuilder buffer = new StringBuilder(length).append(text, 0, i); |
| while (i < length) { |
| c = text.codePointAt(i); |
| if (filter.contains(c)) { |
| buffer.appendCodePoint(Character.toUpperCase(c)); |
| } |
| i += Character.charCount(c); |
| } |
| return buffer.toString(); |
| } |
| |
| /** |
| * Inserts a continuation character after each line separator except the last one. |
| * The intent is to show that a block of lines are part of the same element. |
| * The characters are the same than {@link org.apache.sis.util.logging.MonolineFormatter}. |
| * |
| * @param buffer the buffer where to insert a continuation character in the left margin. |
| * @param lineSeparator the line separator. |
| */ |
| public static void insertLineInLeftMargin(final StringBuilder buffer, final String lineSeparator) { |
| char c = CONTINUATION_END; |
| int i = CharSequences.skipTrailingWhitespaces(buffer, 0, buffer.length()); |
| while ((i = buffer.lastIndexOf(lineSeparator, i - 1)) >= 0) { |
| buffer.insert(i + lineSeparator.length(), c); |
| c = CONTINUATION_MARK; |
| } |
| } |
| |
| /** |
| * Returns a string representation of an instance of the given class having the given properties. |
| * This is a convenience method for implementation of {@link Object#toString()} methods that are |
| * used mostly for debugging purpose. |
| * |
| * <p>The content is specified by (<var>key</var>=<var>value</var>) pairs. If a value is {@code null}, |
| * the whole entry is omitted. If a key is {@code null}, the value is written without the {@code "key="} |
| * part. The later happens typically when the first value is the object name.</p> |
| * |
| * @param classe the class to format. |
| * @param properties the (<var>key</var>=<var>value</var>) pairs. |
| * @return a string representation of an instance of the given class having the given properties. |
| */ |
| public static String toString(final Class<?> classe, final Object... properties) { |
| final StringBuilder buffer = new StringBuilder(32).append(Classes.getShortName(classe)).append('['); |
| boolean isNext = false; |
| for (int i=0; i<properties.length; i++) { |
| final Object value = properties[++i]; |
| if (value != null) { |
| if (isNext) { |
| buffer.append(", "); |
| } |
| final Object name = properties[i-1]; |
| if (name != null) { |
| buffer.append(name).append('='); |
| } |
| if (value.getClass().isArray()) { |
| final int n = Array.getLength(value); |
| if (n != 1) buffer.append('{'); |
| for (int j=0; j<n; j++) { |
| if (j != 0) buffer.append(", "); |
| append(Array.get(value, j), buffer); |
| } |
| if (n != 1) buffer.append('}'); |
| } else { |
| append(value, buffer); |
| } |
| isNext = true; |
| } |
| } |
| return buffer.append(']').toString(); |
| } |
| |
| /** |
| * Appends the given value in the given buffer, using quotes if the value is a character sequence. |
| */ |
| private static void append(final Object value, final StringBuilder buffer) { |
| final boolean isText = (value instanceof CharSequence); |
| if (isText) buffer.append('“'); |
| buffer.append(value); |
| if (isText) buffer.append('”'); |
| } |
| |
| /** |
| * Formats the given character sequence to the given formatter. This method takes in account |
| * the {@link FormattableFlags#UPPERCASE} and {@link FormattableFlags#LEFT_JUSTIFY} flags. |
| * |
| * @param formatter the formatter in which to format the value. |
| * @param flags the formatting flags. |
| * @param width minimal number of characters to write, padding with {@code ' '} if necessary. |
| * @param precision number of characters to keep before truncation, or -1 if no limit. |
| * @param value the text to format. |
| */ |
| public static void formatTo(final Formatter formatter, final int flags, int width, int precision, String value) { |
| /* |
| * Converting to upper cases may change the string length in some locales. |
| * So we need to perform this conversion before to check the length. |
| */ |
| boolean isUpperCase = (flags & FormattableFlags.UPPERCASE) != 0; |
| if (isUpperCase && (width > 0 || precision >= 0)) { |
| value = value.toUpperCase(formatter.locale()); |
| isUpperCase = false; // Because conversion has already been done. |
| } |
| /* |
| * If the string is longer than the specified "precision", truncate |
| * and add "…" for letting user know that there is missing characters. |
| * This loop counts the number of Unicode code points rather than characters. |
| */ |
| int length = value.length(); |
| if (precision >= 0) { |
| for (int i=0,n=0; i<length; i += n) { |
| if (--precision < 0) { |
| /* |
| * Found the amount of characters to keep. The 'n' variable can be |
| * zero only if precision == 0, in which case the string is empty. |
| */ |
| if (n == 0) { |
| value = ""; |
| } else { |
| length = (i -= n) + 1; |
| final StringBuilder buffer = new StringBuilder(length); |
| value = buffer.append(value, 0, i).append('…').toString(); |
| } |
| break; |
| } |
| n = Character.charCount(value.codePointAt(i)); |
| } |
| } |
| /* |
| * If the string is shorter than the minimal width, add spaces on the left or right side. |
| * We double check with `width > length` since it is faster than codePointCount(…). |
| */ |
| final String format; |
| final Object[] args; |
| if (width > length && (width -= value.codePointCount(0, length)) > 0) { |
| format = "%s%s"; |
| args = new Object[] {value, value}; |
| args[(flags & FormattableFlags.LEFT_JUSTIFY) != 0 ? 1 : 0] = CharSequences.spaces(width); |
| } else { |
| format = isUpperCase ? "%S" : "%s"; |
| args = new Object[] {value}; |
| } |
| formatter.format(format, args); |
| } |
| } |