1 /*
   2  * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.css;
  27 
  28 import javafx.css.converter.BooleanConverter;
  29 import javafx.css.converter.ColorConverter;
  30 import javafx.css.converter.DeriveColorConverter;
  31 import javafx.css.converter.DeriveSizeConverter;
  32 import javafx.css.converter.DurationConverter;
  33 import javafx.css.converter.EffectConverter;
  34 import javafx.css.converter.EnumConverter;
  35 import javafx.css.converter.FontConverter;
  36 import javafx.css.converter.InsetsConverter;
  37 import javafx.css.converter.LadderConverter;
  38 import javafx.css.converter.PaintConverter;
  39 import javafx.css.converter.SizeConverter;
  40 import javafx.css.converter.StopConverter;
  41 import javafx.css.converter.StringConverter;
  42 import javafx.css.converter.URLConverter;
  43 import javafx.geometry.Insets;
  44 import javafx.scene.effect.Effect;
  45 import javafx.scene.paint.Color;
  46 import javafx.scene.paint.Paint;
  47 import javafx.scene.text.Font;
  48 import javafx.util.Duration;
  49 
  50 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
  51 import com.sun.javafx.util.Logging;
  52 import com.sun.javafx.logging.PlatformLogger;
  53 import com.sun.javafx.logging.PlatformLogger.Level;
  54 
  55 import java.io.DataInputStream;
  56 import java.io.DataOutputStream;
  57 import java.io.IOException;
  58 import java.util.ArrayList;
  59 import java.util.HashMap;
  60 import java.util.List;
  61 import java.util.Map;
  62 import java.util.WeakHashMap;
  63 
  64 
  65 /**
  66  * StyleConverter converts {@code ParsedValue<F,T>}
  67  * from type {@code F} to type {@code T}. The
  68  * {@link CssMetaData} API requires a {@code StyleConverter} which is used
  69  * when computing a value for the {@code StyleableProperty}. There are
  70  * a number of predefined converters which are accessible by the static
  71  * methods of this class.
  72  *
  73  * {@code F} is the type of the parsed value and {@code T} is the converted type of
  74  * the ParsedValueImpl. For example, a converter from String to Color would
  75  * be declared
  76  * <p>&nbsp;&nbsp;&nbsp;&nbsp;
  77  * {@code public Color convert(ParsedValueImpl<String,Color> value, Font font)}
  78  * </p>
  79  *
  80  * @param <F> the type of the parsed value
  81  * @param <T> the converted type of the ParsedValueImpl
  82  *
  83  * @see ParsedValue
  84  * @see StyleableProperty
  85  * @since JavaFX 8.0
  86  */
  87 public class StyleConverter<F, T> {
  88 
  89     /**
  90      * Convert from the parsed CSS value to the target property type.
  91      *
  92      * @param value        The {@link ParsedValue} to convert
  93      * @param font         The {@link Font} to use when converting a
  94      * <a href="http://www.w3.org/TR/css3-values/#relative-lengths">relative</a>
  95      * value.
  96      * @return the converted target property type
  97      */
  98     @SuppressWarnings("unchecked")
  99     public T convert(ParsedValue<F,T> value, Font font) {
 100         // unchecked!
 101         return (T) value.getValue();
 102     }
 103 
 104     /**
 105      * Return a {@code StyleConverter} that converts {@literal "true" or "false"} to {@code Boolean}.
 106      * @return A {@code StyleConverter} that converts {@literal "true" or "false"} to {@code Boolean}
 107      * @see Boolean#valueOf(java.lang.String)
 108      */
 109     public static StyleConverter<String,Boolean> getBooleanConverter() {
 110         return BooleanConverter.getInstance();
 111     }
 112 
 113     /**
 114      * Return a {@code StyleConverter} that converts a String representation of
 115      * a duration to a {@link Duration}.
 116      * @return A {@code StyleConverter} that converts a String
 117      * representation of a duration to a {@link Duration}
 118      *
 119      * @since JavaFX 8u40
 120      */
 121     public static StyleConverter<?,Duration> getDurationConverter() {
 122         return DurationConverter.getInstance();
 123     }
 124 
 125     /**
 126      * Return a {@code StyleConverter} that converts a String representation of
 127      * a web color to a {@code Color}.
 128      * @return A {@code StyleConverter} that converts a String
 129      * representation of a web color to a {@code Color}
 130      * @see Color#web(java.lang.String)
 131      */
 132     public static StyleConverter<String,Color> getColorConverter() {
 133         return ColorConverter.getInstance();
 134     }
 135 
 136     /**
 137      * Return a {@code StyleConverter} that converts a parsed representation
 138      * of an {@code Effect} to an {@code Effect}
 139      * @return A {@code StyleConverter} that converts a parsed representation
 140      * of an {@code Effect} to an {@code Effect}
 141      * @see Effect
 142      */
 143     public static StyleConverter<ParsedValue[], Effect> getEffectConverter() {
 144         return EffectConverter.getInstance();
 145     }
 146 
 147     /**
 148      * Return a {@code StyleConverter} that converts a String representation
 149      * of an {@code Enum} to an {@code Enum}.
 150      * @param <E> the type of the {@code Enum}
 151      * @param enumClass the enum Class
 152      * @return A {@code StyleConverter} that converts a String representation
 153      * of an {@code Enum} to an {@code Enum}
 154      * @see Enum#valueOf(java.lang.Class, java.lang.String)
 155      */
 156     public static <E extends Enum<E>> StyleConverter<String, ? extends Enum<?>> getEnumConverter(Class<E> enumClass) {
 157         // TODO: reuse EnumConverter instances
 158         EnumConverter<E> converter;
 159         converter = new EnumConverter<>(enumClass);
 160         return converter;
 161     }
 162 
 163     /**
 164      * Return a {@code StyleConverter} that converts a parsed representation
 165      * of a {@code Font} to an {@code Font}.
 166      * @return A {@code StyleConverter} that converts a parsed representation
 167      * of a {@code Font} to an {@code Font}
 168      * @see Font#font(java.lang.String, javafx.scene.text.FontWeight, javafx.scene.text.FontPosture, double)
 169      */
 170     public static StyleConverter<ParsedValue[], Font> getFontConverter() {
 171         return FontConverter.getInstance();
 172     }
 173 
 174     /**
 175      * Return a {@code StyleConverter} that converts a {@literal [<length> |
 176      * <percentage>]}{1,4} to an {@code Insets}.
 177      * @return A {@code StyleConverter} that converts a {@literal [<length> |
 178      * <percentage>]}{1,4} to an {@code Insets}
 179      */
 180     public static StyleConverter<ParsedValue[], Insets> getInsetsConverter() {
 181         return InsetsConverter.getInstance();
 182     }
 183 
 184     /**
 185      * Return a {@code StyleConverter} that converts a parsed representation
 186      * of a {@code Paint} to a {@code Paint}.
 187      * @return A {@code StyleConverter} that converts a parsed representation
 188      * of a {@code Paint} to a {@code Paint}
 189      */
 190     public static StyleConverter<ParsedValue<?, Paint>, Paint> getPaintConverter() {
 191         return PaintConverter.getInstance();
 192     }
 193 
 194     /**
 195      * CSS length and number values are parsed into a Size object that is
 196      * converted to a Number before the value is applied. If the property is
 197      * a {@code Number} type other than {@code Double}, the set method
 198      * of ({@code CssMetaData} can be overridden to convert the {@code Number}
 199      * to the correct type. For example, if the property is an {@code IntegerProperty}:
 200      * <pre><code>
 201      *     {@literal @}Override public void set(MyNode node, Number value, Origin origin) {
 202      *         if (value != null) {
 203      *             super.set(node, value.intValue(), origin);
 204      *         } else {
 205      *             super.set(node, value, origin);
 206      *         }
 207      *     }
 208      * </code></pre>
 209      * @return A {@code StyleConverter} that converts a parsed representation
 210      * of a CSS length or number value to a {@code Number} that is an instance
 211      * of {@code Double}
 212      */
 213     public static StyleConverter<?, Number> getSizeConverter() {
 214         return SizeConverter.getInstance();
 215     }
 216 
 217     /**
 218      * A converter for quoted strings which may have embedded unicode characters.
 219      * @return A {@code StyleConverter} that converts a representation of a
 220      * CSS string value to a {@code String}
 221      */
 222     public static StyleConverter<String,String> getStringConverter() {
 223         return StringConverter.getInstance();
 224     }
 225 
 226     /**
 227      * A converter for URL strings.
 228      * @return A {@code StyleConverter} that converts a representation of a
 229      * CSS URL value to a {@code String}
 230      */
 231     public static StyleConverter<ParsedValue[], String> getUrlConverter() {
 232         return URLConverter.getInstance();
 233     }
 234 
 235 
 236 
 237 
 238     /**
 239      * Convert from the constituent values to the target property type.
 240      * Implemented by Types that have Keys with subKeys.
 241      *
 242      * @param convertedValues the constituent values
 243      * @return the target property type
 244      * @since 9
 245      */
 246     public T convert(Map<CssMetaData<? extends Styleable, ?>,Object> convertedValues) {
 247         return null;
 248     }
 249 
 250     /**
 251      * Write binary data.
 252      * @param os the data output stream
 253      * @param sstore the string store
 254      * @throws java.io.IOException the exception
 255      * @since 9
 256      */
 257     public void writeBinary(DataOutputStream os, StringStore sstore)
 258             throws IOException {
 259 
 260         String cname = getClass().getName();
 261         int index = sstore.addString(cname);
 262         os.writeShort(index);
 263     }
 264 
 265     private static Map<ParsedValue, Object> cache;
 266 
 267     /**
 268      * Clear the cache.
 269      * @since 9
 270      */
 271     public static void clearCache() {
 272         if (cache != null) {
 273             cache.clear();
 274         }
 275     }
 276 
 277     /**
 278      * Get the cached value for the specified key.
 279      * @param key the key
 280      * @return the cached value
 281      * @since 9
 282      */
 283     protected T getCachedValue(ParsedValue key) {
 284         if (cache != null) {
 285             return (T)cache.get(key);
 286         }
 287         return null;
 288     }
 289 
 290     /**
 291      * Cache the value for the specified key.
 292      * @param key the key
 293      * @param value the value
 294      * @since 9
 295      */
 296     protected void cacheValue(ParsedValue key, Object value) {
 297         if (cache == null) cache = new WeakHashMap<>();
 298         cache.put(key, value);
 299     }
 300 
 301     // map of StyleConverter class name to StyleConverter
 302     private static Map<String,StyleConverter<?, ?>> tmap;
 303 
 304     /**
 305      * Read binary data stream.
 306      * @param is the data input stream
 307      * @param strings the strings
 308      * @return the style converter
 309      * @throws java.io.IOException the exception
 310      * @since 9
 311      */
 312     @SuppressWarnings("rawtypes")
 313     public static StyleConverter<?,?> readBinary(DataInputStream is, String[] strings)
 314             throws IOException {
 315 
 316         int index = is.readShort();
 317         String cname = strings[index];
 318 
 319         if (cname == null || cname.isEmpty()) return null;
 320 
 321         if (cname.startsWith("com.sun.javafx.css.converters.")) {
 322             // JavaFX 9: converter classes were moved from
 323             // com.sun.javafx.css.converters.* to javafx.css.converter.*
 324             // Note: the word 'converters' has become 'converter'.
 325             cname = "javafx.css.converter." + cname.substring("com.sun.javafx.css.converters.".length());
 326         }
 327         if (cname.startsWith("javafx.css.converter.EnumConverter")) {
 328             return (StyleConverter)javafx.css.converter.EnumConverter.readBinary(is, strings);
 329         }
 330 
 331         // Make a new entry in tmap, if necessary
 332         if (tmap == null || !tmap.containsKey(cname)) {
 333             StyleConverter<?,?> converter = getInstance(cname);
 334             if (converter == null) {
 335                 final PlatformLogger logger = Logging.getCSSLogger();
 336                 if (logger.isLoggable(Level.SEVERE)) {
 337                     logger.severe("could not deserialize " + cname);
 338                 }
 339             }
 340             if (converter == null) {
 341                 System.err.println("could not deserialize " + cname);
 342             }
 343             if (tmap == null) tmap = new HashMap<String,StyleConverter<?,?>>();
 344             tmap.put(cname, converter);
 345             return converter;
 346         }
 347         return tmap.get(cname);
 348     }
 349 
 350     // package for unit test purposes
 351     static StyleConverter<?,?> getInstance(final String converterClass) {
 352 
 353         StyleConverter<?,?> styleConverter = null;
 354 
 355         switch(converterClass) {
 356         case "javafx.css.converter.BooleanConverter" :
 357             styleConverter = javafx.css.converter.BooleanConverter.getInstance();
 358             break;
 359         case "javafx.css.converter.ColorConverter" :
 360             styleConverter = javafx.css.converter.ColorConverter.getInstance();
 361             break;
 362         case "javafx.css.converter.CursorConverter" :
 363             styleConverter = javafx.css.converter.CursorConverter.getInstance();
 364             break;
 365         case "javafx.css.converter.EffectConverter" :
 366             styleConverter = javafx.css.converter.EffectConverter.getInstance();
 367             break;
 368         case "javafx.css.converter.EffectConverter$DropShadowConverter" :
 369             styleConverter = javafx.css.converter.EffectConverter.DropShadowConverter.getInstance();
 370             break;
 371         case "javafx.css.converter.EffectConverter$InnerShadowConverter" :
 372             styleConverter = javafx.css.converter.EffectConverter.InnerShadowConverter.getInstance();
 373             break;
 374         case "javafx.css.converter.FontConverter" :
 375             styleConverter = javafx.css.converter.FontConverter.getInstance();
 376             break;
 377         case "javafx.css.converter.FontConverter$FontStyleConverter" :
 378         case "javafx.css.converter.FontConverter$StyleConverter" :
 379             styleConverter = javafx.css.converter.FontConverter.FontStyleConverter.getInstance();
 380             break;
 381         case "javafx.css.converter.FontConverter$FontWeightConverter" :
 382         case "javafx.css.converter.FontConverter$WeightConverter" :
 383             styleConverter = javafx.css.converter.FontConverter.FontWeightConverter.getInstance();
 384             break;
 385         case "javafx.css.converter.FontConverter$FontSizeConverter" :
 386         case "javafx.css.converter.FontConverter$SizeConverter" :
 387             styleConverter = javafx.css.converter.FontConverter.FontSizeConverter.getInstance();
 388             break;
 389 
 390         case "javafx.css.converter.InsetsConverter" :
 391             styleConverter = javafx.css.converter.InsetsConverter.getInstance();
 392             break;
 393         case "javafx.css.converter.InsetsConverter$SequenceConverter" :
 394             styleConverter = javafx.css.converter.InsetsConverter.SequenceConverter.getInstance();
 395             break;
 396 
 397         case "javafx.css.converter.PaintConverter" :
 398             styleConverter = javafx.css.converter.PaintConverter.getInstance();
 399             break;
 400         case "javafx.css.converter.PaintConverter$SequenceConverter" :
 401             styleConverter = javafx.css.converter.PaintConverter.SequenceConverter.getInstance();
 402             break;
 403         case "javafx.css.converter.PaintConverter$LinearGradientConverter" :
 404             styleConverter = javafx.css.converter.PaintConverter.LinearGradientConverter.getInstance();
 405             break;
 406         case "javafx.css.converter.PaintConverter$RadialGradientConverter" :
 407             styleConverter = javafx.css.converter.PaintConverter.RadialGradientConverter.getInstance();
 408             break;
 409 
 410         case "javafx.css.converter.SizeConverter" :
 411             styleConverter = javafx.css.converter.SizeConverter.getInstance();
 412             break;
 413         case "javafx.css.converter.SizeConverter$SequenceConverter" :
 414             styleConverter = javafx.css.converter.SizeConverter.SequenceConverter.getInstance();
 415             break;
 416 
 417         case "javafx.css.converter.StringConverter" :
 418             styleConverter = javafx.css.converter.StringConverter.getInstance();
 419             break;
 420         case "javafx.css.converter.StringConverter$SequenceConverter" :
 421             styleConverter = javafx.css.converter.StringConverter.SequenceConverter.getInstance();
 422             break;
 423         case "javafx.css.converter.URLConverter" :
 424             styleConverter = javafx.css.converter.URLConverter.getInstance();
 425             break;
 426         case "javafx.css.converter.URLConverter$SequenceConverter" :
 427             styleConverter = javafx.css.converter.URLConverter.SequenceConverter.getInstance();
 428             break;
 429 
 430         // Region stuff  - including 2.x class names
 431         case "com.sun.javafx.scene.layout.region.BackgroundPositionConverter" :
 432         case "com.sun.javafx.scene.layout.region.BackgroundImage$BackgroundPositionConverter" :
 433             styleConverter = com.sun.javafx.scene.layout.region.BackgroundPositionConverter.getInstance();
 434             break;
 435         case "com.sun.javafx.scene.layout.region.BackgroundSizeConverter" :
 436         case "com.sun.javafx.scene.layout.region.BackgroundImage$BackgroundSizeConverter" :
 437             styleConverter = com.sun.javafx.scene.layout.region.BackgroundSizeConverter.getInstance();
 438             break;
 439         case "com.sun.javafx.scene.layout.region.BorderImageSliceConverter" :
 440         case "com.sun.javafx.scene.layout.region.BorderImage$SliceConverter" :
 441             styleConverter = com.sun.javafx.scene.layout.region.BorderImageSliceConverter.getInstance();
 442             break;
 443         case "com.sun.javafx.scene.layout.region.BorderImageWidthConverter" :
 444             styleConverter = com.sun.javafx.scene.layout.region.BorderImageWidthConverter.getInstance();
 445             break;
 446         case "com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter" :
 447             styleConverter = com.sun.javafx.scene.layout.region.BorderImageWidthsSequenceConverter.getInstance();
 448             break;
 449         case "com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter" :
 450         case "com.sun.javafx.scene.layout.region.StrokeBorder$BorderStyleSequenceConverter" :
 451             styleConverter = com.sun.javafx.scene.layout.region.BorderStrokeStyleSequenceConverter.getInstance();
 452             break;
 453         case "com.sun.javafx.scene.layout.region.BorderStyleConverter" :
 454         case "com.sun.javafx.scene.layout.region.StrokeBorder$BorderStyleConverter" :
 455             styleConverter = com.sun.javafx.scene.layout.region.BorderStyleConverter.getInstance();
 456             break;
 457         case "com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter" :
 458         case "com.sun.javafx.scene.layout.region.BackgroundImage$LayeredBackgroundPositionConverter" :
 459             styleConverter = com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter.getInstance();
 460             break;
 461         case "com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter" :
 462         case "com.sun.javafx.scene.layout.region.BackgroundImage$LayeredBackgroundSizeConverter" :
 463             styleConverter = com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter.getInstance();
 464             break;
 465         case "com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter" :
 466         case "com.sun.javafx.scene.layout.region.StrokeBorder$LayeredBorderPaintConverter" :
 467            styleConverter = com.sun.javafx.scene.layout.region.LayeredBorderPaintConverter.getInstance();
 468             break;
 469         case "com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter" :
 470         case "com.sun.javafx.scene.layout.region.StrokeBorder$LayeredBorderStyleConverter" :
 471             styleConverter = com.sun.javafx.scene.layout.region.LayeredBorderStyleConverter.getInstance();
 472             break;
 473         case "com.sun.javafx.scene.layout.region.RepeatStructConverter" :
 474         case "com.sun.javafx.scene.layout.region.BackgroundImage$BackgroundRepeatConverter" :
 475         case "com.sun.javafx.scene.layout.region.BorderImage$RepeatConverter" :
 476             styleConverter = com.sun.javafx.scene.layout.region.RepeatStructConverter.getInstance();
 477             break;
 478         case "com.sun.javafx.scene.layout.region.SliceSequenceConverter" :
 479         case "com.sun.javafx.scene.layout.region.BorderImage$SliceSequenceConverter" :
 480             styleConverter = com.sun.javafx.scene.layout.region.SliceSequenceConverter.getInstance();
 481             break;
 482         case "com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter" :
 483         case "com.sun.javafx.scene.layout.region.StrokeBorder$BorderPaintConverter" :
 484             styleConverter = com.sun.javafx.scene.layout.region.StrokeBorderPaintConverter.getInstance();
 485             break;
 486         case "com.sun.javafx.scene.layout.region.Margins$Converter" :
 487             styleConverter = com.sun.javafx.scene.layout.region.Margins.Converter.getInstance();
 488             break;
 489         case "com.sun.javafx.scene.layout.region.Margins$SequenceConverter" :
 490             styleConverter = com.sun.javafx.scene.layout.region.Margins.SequenceConverter.getInstance();
 491             break;
 492         case "javafx.scene.layout.CornerRadiiConverter" :  // Fix for RT-39665
 493         case "com.sun.javafx.scene.layout.region.CornerRadiiConverter" :
 494             styleConverter = CornerRadiiConverter.getInstance();
 495             break;
 496 
 497         // parser stuff
 498         case "javafx.css.converter.DeriveColorConverter":
 499         case "com.sun.javafx.css.parser.DeriveColorConverter" :
 500             styleConverter = DeriveColorConverter.getInstance();
 501             break;
 502         case "javafx.css.converter.DeriveSizeConverter":
 503         case "com.sun.javafx.css.parser.DeriveSizeConverter" :
 504             styleConverter = DeriveSizeConverter.getInstance();
 505             break;
 506         case "javafx.css.converter.LadderConverter":
 507         case "com.sun.javafx.css.parser.LadderConverter" :
 508             styleConverter = LadderConverter.getInstance();
 509             break;
 510         case "javafx.css.converter.StopConverter":
 511         case "com.sun.javafx.css.parser.StopConverter" :
 512             styleConverter = StopConverter.getInstance();
 513             break;
 514 
 515             default :
 516             final PlatformLogger logger = Logging.getCSSLogger();
 517             if (logger.isLoggable(Level.SEVERE)) {
 518                 logger.severe("StyleConverter : converter Class is null for : "+converterClass);
 519             }
 520             break;
 521         }
 522 
 523         return styleConverter;
 524     }
 525 
 526 
 527     /**
 528      * The StringStore class
 529      * @since 9
 530      */
 531     public static class StringStore {
 532         private final Map<String,Integer> stringMap = new HashMap<String,Integer>();
 533         public final List<String> strings = new ArrayList<String>();
 534 
 535         public int addString(String s) {
 536             Integer index = stringMap.get(s);
 537             if (index == null) {
 538                 index = strings.size();
 539                 strings.add(s);
 540                 stringMap.put(s,index);
 541             }
 542             return index;
 543         }
 544 
 545         public void writeBinary(DataOutputStream os) throws IOException {
 546             os.writeShort(strings.size());
 547             if (stringMap.containsKey(null)) {
 548                 Integer index = stringMap.get(null);
 549                 os.writeShort(index);
 550             } else {
 551                 os.writeShort(-1);
 552             }
 553             for (int n=0; n<strings.size(); n++) {
 554                 String s = strings.get(n);
 555                 if (s == null) continue;
 556                 os.writeUTF(s);
 557             }
 558         }
 559 
 560         // TODO: this isn't parallel with writeBinary
 561         public static String[] readBinary(DataInputStream is) throws IOException {
 562             int nStrings = is.readShort();
 563             int nullIndex = is.readShort();
 564             String[] strings = new String[nStrings];
 565             java.util.Arrays.fill(strings, null);
 566             for (int n=0; n<nStrings; n++) {
 567                 if (n == nullIndex) continue;
 568                 strings[n] = is.readUTF();
 569             }
 570             return strings;
 571         }
 572     }
 573 }