Source for gnu.java.security.PolicyFile

   1: /* PolicyFile.java -- policy file reader
   2:    Copyright (C) 2004, 2005  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: package gnu.java.security;
  39: 
  40: import gnu.classpath.SystemProperties;
  41: import gnu.classpath.debug.Component;
  42: import gnu.classpath.debug.SystemLogger;
  43: 
  44: import java.io.File;
  45: import java.io.IOException;
  46: import java.io.InputStreamReader;
  47: import java.io.StreamTokenizer;
  48: import java.lang.reflect.Constructor;
  49: import java.net.MalformedURLException;
  50: import java.net.URL;
  51: import java.security.AccessController;
  52: import java.security.CodeSource;
  53: import java.security.KeyStore;
  54: import java.security.KeyStoreException;
  55: import java.security.Permission;
  56: import java.security.PermissionCollection;
  57: import java.security.Permissions;
  58: import java.security.Policy;
  59: import java.security.Principal;
  60: import java.security.PrivilegedActionException;
  61: import java.security.PrivilegedExceptionAction;
  62: import java.security.Security;
  63: import java.security.UnresolvedPermission;
  64: import java.security.cert.Certificate;
  65: import java.security.cert.X509Certificate;
  66: import java.util.Enumeration;
  67: import java.util.HashMap;
  68: import java.util.Iterator;
  69: import java.util.LinkedList;
  70: import java.util.List;
  71: import java.util.Map;
  72: import java.util.StringTokenizer;
  73: import java.util.logging.Logger;
  74: 
  75: /**
  76:  * An implementation of a {@link java.security.Policy} object whose
  77:  * permissions are specified by a <em>policy file</em>.
  78:  *
  79:  * <p>The approximate syntax of policy files is:</p>
  80:  *
  81:  * <pre>
  82:  * policyFile ::= keystoreOrGrantEntries ;
  83:  *
  84:  * keystoreOrGrantEntries ::= keystoreOrGrantEntry |
  85:  *                            keystoreOrGrantEntries keystoreOrGrantEntry |
  86:  *                            EMPTY ;
  87:  *
  88:  * keystoreOrGrantEntry ::= keystoreEntry | grantEntry ;
  89:  *
  90:  * keystoreEntry ::= "keystore" keystoreUrl ';' |
  91:  *                   "keystore" keystoreUrl ',' keystoreAlgorithm ';' ;
  92:  *
  93:  * keystoreUrl ::= URL ;
  94:  * keystoreAlgorithm ::= STRING ;
  95:  *
  96:  * grantEntry ::= "grant" domainParameters '{' permissions '}' ';'
  97:  *
  98:  * domainParameters ::= domainParameter |
  99:  *                      domainParameter ',' domainParameters ;
 100:  *
 101:  * domainParameter ::= "signedBy" signerNames |
 102:  *                     "codeBase" codeBaseUrl |
 103:  *                     "principal" principalClassName principalName |
 104:  *                     "principal" principalName ;
 105:  *
 106:  * signerNames ::= quotedString ;
 107:  * codeBaseUrl ::= URL ;
 108:  * principalClassName ::= STRING ;
 109:  * principalName ::= quotedString ;
 110:  *
 111:  * quotedString ::= quoteChar STRING quoteChar ;
 112:  * quoteChar ::= '"' | '\'';
 113:  *
 114:  * permissions ::= permission | permissions permission ;
 115:  *
 116:  * permission ::= "permission" permissionClassName permissionTarget permissionAction |
 117:  *                "permission" permissionClassName permissionTarget |
 118:  *                "permission" permissionClassName;
 119:  * </pre>
 120:  *
 121:  * <p>Comments are either form of Java comments. Keystore entries only
 122:  * affect subsequent grant entries, so if a grant entry preceeds a
 123:  * keystore entry, that grant entry is not affected by that keystore
 124:  * entry. Certian instances of <code>${property-name}</code> will be
 125:  * replaced with <code>System.getProperty("property-name")</code> in
 126:  * quoted strings.</p>
 127:  *
 128:  * <p>This class will load the following files when created or
 129:  * refreshed, in order:</p>
 130:  *
 131:  * <ol>
 132:  * <li>The file <code>${java.home}/lib/security/java.policy</code>.</li>
 133:  * <li>All URLs specified by security properties
 134:  * <code>"policy.file.<i>n</i>"</code>, for increasing <i>n</i>
 135:  * starting from 1. The sequence stops at the first undefined
 136:  * property, so you must set <code>"policy.file.1"</code> if you also
 137:  * set <code>"policy.file.2"</code>, and so on.</li>
 138:  * <li>The URL specified by the property
 139:  * <code>"java.security.policy"</code>.</li>
 140:  * </ol>
 141:  *
 142:  * @author Casey Marshall (csm@gnu.org)
 143:  * @see java.security.Policy
 144:  */
 145: public final class PolicyFile extends Policy
 146: {
 147: 
 148:   // Constants and fields.
 149:   // -------------------------------------------------------------------------
 150: 
 151:   private static final Logger logger = SystemLogger.SYSTEM;
 152: 
 153:   private static final String DEFAULT_POLICY =
 154:     SystemProperties.getProperty("java.home")
 155:     + SystemProperties.getProperty("file.separator") + "lib"
 156:     + SystemProperties.getProperty("file.separator") + "security"
 157:     + SystemProperties.getProperty("file.separator") + "java.policy";
 158:   private static final String DEFAULT_USER_POLICY =
 159:     SystemProperties.getProperty ("user.home") +
 160:     SystemProperties.getProperty ("file.separator") + ".java.policy";
 161: 
 162:   private final Map cs2pc;
 163: 
 164:   // Constructors.
 165:   // -------------------------------------------------------------------------
 166: 
 167:   public PolicyFile()
 168:   {
 169:     cs2pc = new HashMap();
 170:     refresh();
 171:   }
 172: 
 173:   // Instance methods.
 174:   // -------------------------------------------------------------------------
 175: 
 176:   public PermissionCollection getPermissions(CodeSource codeSource)
 177:   {
 178:     Permissions perms = new Permissions();
 179:     for (Iterator it = cs2pc.entrySet().iterator(); it.hasNext(); )
 180:       {
 181:         Map.Entry e = (Map.Entry) it.next();
 182:         CodeSource cs = (CodeSource) e.getKey();
 183:         if (cs.implies(codeSource))
 184:           {
 185:             logger.log (Component.POLICY, "{0} -> {1}", new Object[]
 186:               { cs, codeSource });
 187:             PermissionCollection pc = (PermissionCollection) e.getValue();
 188:             for (Enumeration ee = pc.elements(); ee.hasMoreElements(); )
 189:               {
 190:                 perms.add((Permission) ee.nextElement());
 191:               }
 192:           }
 193:         else
 194:           logger.log (Component.POLICY, "{0} !-> {1}", new Object[]
 195:             { cs, codeSource });
 196:       }
 197:     logger.log (Component.POLICY, "returning permissions {0} for {1}",
 198:                 new Object[] { perms, codeSource });
 199:     return perms;
 200:   }
 201: 
 202:   public void refresh()
 203:   {
 204:     cs2pc.clear();
 205:     final List policyFiles = new LinkedList();
 206:     try
 207:       {
 208:         policyFiles.add (new File (DEFAULT_POLICY).toURL());
 209:         policyFiles.add (new File (DEFAULT_USER_POLICY).toURL ());
 210: 
 211:         AccessController.doPrivileged(
 212:           new PrivilegedExceptionAction()
 213:           {
 214:             public Object run() throws Exception
 215:             {
 216:               String allow = Security.getProperty ("policy.allowSystemProperty");
 217:               if (allow == null || Boolean.getBoolean (allow))
 218:                 {
 219:                   String s = SystemProperties.getProperty ("java.security.policy");
 220:                   logger.log (Component.POLICY, "java.security.policy={0}", s);
 221:                   if (s != null)
 222:                     {
 223:                       boolean only = s.startsWith ("=");
 224:                       if (only)
 225:                         s = s.substring (1);
 226:                       policyFiles.clear ();
 227:                       policyFiles.add (new URL (s));
 228:                       if (only)
 229:                         return null;
 230:                     }
 231:                 }
 232:               for (int i = 1; ; i++)
 233:                 {
 234:                   String pname = "policy.url." + i;
 235:                   String s = Security.getProperty (pname);
 236:                   logger.log (Component.POLICY, "{0}={1}", new Object []
 237:                     { pname, s });
 238:                   if (s == null)
 239:                     break;
 240:                   policyFiles.add (new URL (s));
 241:                 }
 242:               return null;
 243:             }
 244:           });
 245:       }
 246:     catch (PrivilegedActionException pae)
 247:       {
 248:         logger.log (Component.POLICY, "reading policy properties", pae);
 249:       }
 250:     catch (MalformedURLException mue)
 251:       {
 252:         logger.log (Component.POLICY, "setting default policies", mue);
 253:       }
 254: 
 255:     logger.log (Component.POLICY, "building policy from URLs {0}",
 256:                 policyFiles);
 257:     for (Iterator it = policyFiles.iterator(); it.hasNext(); )
 258:       {
 259:         try
 260:           {
 261:             URL url = (URL) it.next();
 262:             parse(url);
 263:           }
 264:         catch (IOException ioe)
 265:           {
 266:             logger.log (Component.POLICY, "reading policy", ioe);
 267:           }
 268:       }
 269:   }
 270: 
 271:   public String toString()
 272:   {
 273:     return super.toString() + " [ " + cs2pc.toString() + " ]";
 274:   }
 275: 
 276:   // Own methods.
 277:   // -------------------------------------------------------------------------
 278: 
 279:   private static final int STATE_BEGIN = 0;
 280:   private static final int STATE_GRANT = 1;
 281:   private static final int STATE_PERMS = 2;
 282: 
 283:   /**
 284:    * Parse a policy file, incorporating the permission definitions
 285:    * described therein.
 286:    *
 287:    * @param url The URL of the policy file to read.
 288:    * @throws IOException if an I/O error occurs, or if the policy file
 289:    * cannot be parsed.
 290:    */
 291:   private void parse(final URL url) throws IOException
 292:   {
 293:     logger.log (Component.POLICY, "reading policy file from {0}", url);
 294:     final StreamTokenizer in = new StreamTokenizer(new InputStreamReader(url.openStream()));
 295:     in.resetSyntax();
 296:     in.slashSlashComments(true);
 297:     in.slashStarComments(true);
 298:     in.wordChars('A', 'Z');
 299:     in.wordChars('a', 'z');
 300:     in.wordChars('0', '9');
 301:     in.wordChars('.', '.');
 302:     in.wordChars('_', '_');
 303:     in.wordChars('$', '$');
 304:     in.whitespaceChars(' ', ' ');
 305:     in.whitespaceChars('\t', '\t');
 306:     in.whitespaceChars('\f', '\f');
 307:     in.whitespaceChars('\n', '\n');
 308:     in.whitespaceChars('\r', '\r');
 309:     in.quoteChar('\'');
 310:     in.quoteChar('"');
 311: 
 312:     int tok;
 313:     int state = STATE_BEGIN;
 314:     List keystores = new LinkedList();
 315:     URL currentBase = null;
 316:     List currentCerts = new LinkedList();
 317:     Permissions currentPerms = new Permissions();
 318:     while ((tok = in.nextToken()) != StreamTokenizer.TT_EOF)
 319:       {
 320:         switch (tok)
 321:           {
 322:           case '{':
 323:             if (state != STATE_GRANT)
 324:               error(url, in, "spurious '{'");
 325:             state = STATE_PERMS;
 326:             tok = in.nextToken();
 327:             break;
 328:           case '}':
 329:             if (state != STATE_PERMS)
 330:               error(url, in, "spurious '}'");
 331:             state = STATE_BEGIN;
 332:             currentPerms.setReadOnly();
 333:             Certificate[] c = null;
 334:             if (!currentCerts.isEmpty())
 335:               c = (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()]);
 336:             cs2pc.put(new CodeSource(currentBase, c), currentPerms);
 337:             currentCerts.clear();
 338:             currentPerms = new Permissions();
 339:             currentBase = null;
 340:             tok = in.nextToken();
 341:             if (tok != ';')
 342:               in.pushBack();
 343:             continue;
 344:           }
 345:         if (tok != StreamTokenizer.TT_WORD)
 346:           {
 347:             error(url, in, "expecting word token");
 348:           }
 349: 
 350:         // keystore "<keystore-path>" [',' "<keystore-type>"] ';'
 351:         if (in.sval.equalsIgnoreCase("keystore"))
 352:           {
 353:             String alg = KeyStore.getDefaultType();
 354:             tok = in.nextToken();
 355:             if (tok != '"' && tok != '\'')
 356:               error(url, in, "expecting key store URL");
 357:             String store = in.sval;
 358:             tok = in.nextToken();
 359:             if (tok == ',')
 360:               {
 361:                 tok = in.nextToken();
 362:                 if (tok != '"' && tok != '\'')
 363:                   error(url, in, "expecting key store type");
 364:                 alg = in.sval;
 365:                 tok = in.nextToken();
 366:               }
 367:             if (tok != ';')
 368:               error(url, in, "expecting semicolon");
 369:             try
 370:               {
 371:                 KeyStore keystore = KeyStore.getInstance(alg);
 372:                 keystore.load(new URL(url, store).openStream(), null);
 373:                 keystores.add(keystore);
 374:               }
 375:             catch (Exception x)
 376:               {
 377:                 error(url, in, x.toString());
 378:               }
 379:           }
 380:         else if (in.sval.equalsIgnoreCase("grant"))
 381:           {
 382:             if (state != STATE_BEGIN)
 383:               error(url, in, "extraneous grant keyword");
 384:             state = STATE_GRANT;
 385:           }
 386:         else if (in.sval.equalsIgnoreCase("signedBy"))
 387:           {
 388:             if (state != STATE_GRANT && state != STATE_PERMS)
 389:               error(url, in, "spurious 'signedBy'");
 390:             if (keystores.isEmpty())
 391:               error(url, in, "'signedBy' with no keystores");
 392:             tok = in.nextToken();
 393:             if (tok != '"' && tok != '\'')
 394:               error(url, in, "expecting signedBy name");
 395:             StringTokenizer st = new StringTokenizer(in.sval, ",");
 396:             while (st.hasMoreTokens())
 397:               {
 398:                 String alias = st.nextToken();
 399:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 400:                   {
 401:                     KeyStore keystore = (KeyStore) it.next();
 402:                     try
 403:                       {
 404:                         if (keystore.isCertificateEntry(alias))
 405:                           currentCerts.add(keystore.getCertificate(alias));
 406:                       }
 407:                     catch (KeyStoreException kse)
 408:                       {
 409:                         error(url, in, kse.toString());
 410:                       }
 411:                   }
 412:               }
 413:             tok = in.nextToken();
 414:             if (tok != ',')
 415:               {
 416:                 if (state != STATE_GRANT)
 417:                   error(url, in, "spurious ','");
 418:                 in.pushBack();
 419:               }
 420:           }
 421:         else if (in.sval.equalsIgnoreCase("codeBase"))
 422:           {
 423:             if (state != STATE_GRANT)
 424:               error(url, in, "spurious 'codeBase'");
 425:             tok = in.nextToken();
 426:             if (tok != '"' && tok != '\'')
 427:               error(url, in, "expecting code base URL");
 428:             String base = expand(in.sval);
 429:             if (File.separatorChar != '/')
 430:               base = base.replace(File.separatorChar, '/');
 431:             try
 432:               {
 433:                 currentBase = new URL(base);
 434:               }
 435:             catch (MalformedURLException mue)
 436:               {
 437:                 error(url, in, mue.toString());
 438:               }
 439:             tok = in.nextToken();
 440:             if (tok != ',')
 441:               in.pushBack();
 442:           }
 443:         else if (in.sval.equalsIgnoreCase("principal"))
 444:           {
 445:             if (state != STATE_GRANT)
 446:               error(url, in, "spurious 'principal'");
 447:             tok = in.nextToken();
 448:             if (tok == StreamTokenizer.TT_WORD)
 449:               {
 450:                 tok = in.nextToken();
 451:                 if (tok != '"' && tok != '\'')
 452:                   error(url, in, "expecting principal name");
 453:                 String name = in.sval;
 454:                 Principal p = null;
 455:                 try
 456:                   {
 457:                     Class pclass = Class.forName(in.sval);
 458:                     Constructor c =
 459:                       pclass.getConstructor(new Class[] { String.class });
 460:                     p = (Principal) c.newInstance(new Object[] { name });
 461:                   }
 462:                 catch (Exception x)
 463:                   {
 464:                     error(url, in, x.toString());
 465:                   }
 466:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 467:                   {
 468:                     KeyStore ks = (KeyStore) it.next();
 469:                     try
 470:                       {
 471:                         for (Enumeration e = ks.aliases(); e.hasMoreElements(); )
 472:                           {
 473:                             String alias = (String) e.nextElement();
 474:                             if (ks.isCertificateEntry(alias))
 475:                               {
 476:                                 Certificate cert = ks.getCertificate(alias);
 477:                                 if (!(cert instanceof X509Certificate))
 478:                                   continue;
 479:                                 if (p.equals(((X509Certificate) cert).getSubjectDN()) ||
 480:                                     p.equals(((X509Certificate) cert).getSubjectX500Principal()))
 481:                                   currentCerts.add(cert);
 482:                               }
 483:                           }
 484:                       }
 485:                     catch (KeyStoreException kse)
 486:                       {
 487:                         error(url, in, kse.toString());
 488:                       }
 489:                   }
 490:               }
 491:             else if (tok == '"' || tok == '\'')
 492:               {
 493:                 String alias = in.sval;
 494:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 495:                   {
 496:                     KeyStore ks = (KeyStore) it.next();
 497:                     try
 498:                       {
 499:                         if (ks.isCertificateEntry(alias))
 500:                           currentCerts.add(ks.getCertificate(alias));
 501:                       }
 502:                     catch (KeyStoreException kse)
 503:                       {
 504:                         error(url, in, kse.toString());
 505:                       }
 506:                   }
 507:               }
 508:             else
 509:               error(url, in, "expecting principal");
 510:             tok = in.nextToken();
 511:             if (tok != ',')
 512:               in.pushBack();
 513:           }
 514:         else if (in.sval.equalsIgnoreCase("permission"))
 515:           {
 516:             if (state != STATE_PERMS)
 517:               error(url, in, "spurious 'permission'");
 518:             tok = in.nextToken();
 519:             if (tok != StreamTokenizer.TT_WORD)
 520:               error(url, in, "expecting permission class name");
 521:             String className = in.sval;
 522:             Class clazz = null;
 523:             try
 524:               {
 525:                 clazz = Class.forName(className);
 526:               }
 527:             catch (ClassNotFoundException cnfe)
 528:               {
 529:               }
 530:             tok = in.nextToken();
 531:             if (tok == ';')
 532:               {
 533:                 if (clazz == null)
 534:                   {
 535:                     currentPerms.add(new UnresolvedPermission(className,
 536:               null, null, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 537:                     continue;
 538:                   }
 539:                 try
 540:                   {
 541:                     currentPerms.add((Permission) clazz.newInstance());
 542:                   }
 543:                 catch (Exception x)
 544:                   {
 545:                     error(url, in, x.toString());
 546:                   }
 547:                 continue;
 548:               }
 549:             if (tok != '"' && tok != '\'')
 550:               error(url, in, "expecting permission target");
 551:             String target = expand(in.sval);
 552:             tok = in.nextToken();
 553:             if (tok == ';')
 554:               {
 555:                 if (clazz == null)
 556:                   {
 557:                     currentPerms.add(new UnresolvedPermission(className,
 558:               target, null, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 559:                     continue;
 560:                   }
 561:                 try
 562:                   {
 563:                     Constructor c =
 564:                       clazz.getConstructor(new Class[] { String.class });
 565:                     currentPerms.add((Permission) c.newInstance(
 566:                       new Object[] { target }));
 567:                   }
 568:                 catch (Exception x)
 569:                   {
 570:                     error(url, in, x.toString());
 571:                   }
 572:                 continue;
 573:               }
 574:             if (tok != ',')
 575:               error(url, in, "expecting ','");
 576:             tok = in.nextToken();
 577:             if (tok == StreamTokenizer.TT_WORD)
 578:               {
 579:                 if (!in.sval.equalsIgnoreCase("signedBy"))
 580:                   error(url, in, "expecting 'signedBy'");
 581:                 try
 582:                   {
 583:                     Constructor c =
 584:                       clazz.getConstructor(new Class[] { String.class });
 585:                     currentPerms.add((Permission) c.newInstance(
 586:                       new Object[] { target }));
 587:                   }
 588:                 catch (Exception x)
 589:                   {
 590:                     error(url, in, x.toString());
 591:                   }
 592:                 in.pushBack();
 593:                 continue;
 594:               }
 595:             if (tok != '"' && tok != '\'')
 596:               error(url, in, "expecting permission action");
 597:             String action = in.sval;
 598:             if (clazz == null)
 599:               {
 600:                 currentPerms.add(new UnresolvedPermission(className,
 601:           target, action, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 602:                 continue;
 603:               }
 604:             else
 605:               {
 606:                 try
 607:                   {
 608:                     Constructor c = clazz.getConstructor(
 609:                       new Class[] { String.class, String.class });
 610:                     currentPerms.add((Permission) c.newInstance(
 611:                       new Object[] { target, action }));
 612:                   }
 613:                 catch (Exception x)
 614:                   {
 615:                     error(url, in, x.toString());
 616:                   }
 617:               }
 618:             tok = in.nextToken();
 619:             if (tok != ';' && tok != ',')
 620:               error(url, in, "expecting ';' or ','");
 621:           }
 622:       }
 623:   }
 624: 
 625:   /**
 626:    * Expand all instances of <code>"${property-name}"</code> into
 627:    * <code>System.getProperty("property-name")</code>.
 628:    */
 629:   private static String expand(final String s)
 630:   {
 631:     final StringBuffer result = new StringBuffer();
 632:     final StringBuffer prop = new StringBuffer();
 633:     int state = 0;
 634:     for (int i = 0; i < s.length(); i++)
 635:       {
 636:         switch (state)
 637:           {
 638:           case 0:
 639:             if (s.charAt(i) == '$')
 640:               state = 1;
 641:             else
 642:               result.append(s.charAt(i));
 643:             break;
 644:           case 1:
 645:             if (s.charAt(i) == '{')
 646:               state = 2;
 647:             else
 648:               {
 649:                 state = 0;
 650:                 result.append('$').append(s.charAt(i));
 651:               }
 652:             break;
 653:           case 2:
 654:             if (s.charAt(i) == '}')
 655:               {
 656:                 String p = prop.toString();
 657:                 if (p.equals("/"))
 658:                   p = "file.separator";
 659:                 p = System.getProperty(p);
 660:                 if (p == null)
 661:                   p = "";
 662:                 result.append(p);
 663:                 prop.setLength(0);
 664:                 state = 0;
 665:               }
 666:             else
 667:               prop.append(s.charAt(i));
 668:             break;
 669:           }
 670:       }
 671:     if (state != 0)
 672:       result.append('$').append('{').append(prop);
 673:     return result.toString();
 674:   }
 675: 
 676:   /**
 677:    * I miss macros.
 678:    */
 679:   private static void error(URL base, StreamTokenizer in, String msg)
 680:     throws IOException
 681:   {
 682:     throw new IOException(base+":"+in.lineno()+": "+msg);
 683:   }
 684: }