1 /*
2  * LogicalPathname.java
3  *
4  * Copyright (C) 2004-2005 Peter Graves
5  * $Id$
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
20  *
21  * As a special exception, the copyright holders of this library give you
22  * permission to link this library with independent modules to produce an
23  * executable, regardless of the license terms of these independent
24  * modules, and to copy and distribute the resulting executable under
25  * terms of your choice, provided that you also meet, for each linked
26  * independent module, the terms and conditions of the license of that
27  * module.  An independent module is a module which is not derived from
28  * or based on this library.  If you modify this library, you may extend
29  * this exception to your version of the library, but you are not
30  * obligated to do so.  If you do not wish to do so, delete this
31  * exception statement from your version.
32  */
33 
34 package org.armedbear.lisp;
35 
36 import static org.armedbear.lisp.Lisp.*;
37 
38 import java.util.HashMap;
39 import java.util.StringTokenizer;
40 import java.text.MessageFormat;
41 
42 public final class LogicalPathname extends Pathname
43 {
44   public static final String LOGICAL_PATHNAME_CHARS
45     = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-;*.";
46   private static final HashMap map
47     = new HashMap();
48 
49   // A logical host is represented as the string that names it.
50   // (defvar *logical-pathname-translations* (make-hash-table :test 'equal))
51   public static HashTable TRANSLATIONS
52     = HashTable.newEqualHashTable(LOGICAL_PATHNAME_CHARS.length(), NIL, NIL);
53   private static final Symbol _TRANSLATIONS_
54     = exportSpecial("*LOGICAL-PATHNAME-TRANSLATIONS*", PACKAGE_SYS, TRANSLATIONS);
55 
isValidLogicalPathname(String namestring)56   static public boolean isValidLogicalPathname(String namestring) {
57     if (!isValidURL(namestring)) {
58       String host = getHostString(namestring);
59       if (host != null
60        && LogicalPathname.TRANSLATIONS.get(new SimpleString(host)) != null) {
61         return true;
62       }
63     }
64     return false;
65   }
66 
LogicalPathname()67   protected LogicalPathname() {
68   }
69 
70   // Used in Pathname._makePathname to indicate type for namestring
create()71   public static LogicalPathname create() {
72     return new LogicalPathname();
73   }
74 
create(LogicalPathname p)75   public static LogicalPathname create(LogicalPathname p) {
76     Pathname pathname = new Pathname();
77     pathname.copyFrom(p);
78     LogicalPathname result = new LogicalPathname();
79     Pathname.ncoerce(pathname, result);
80     return result;
81   }
82 
83 
84 
create(String namestring)85   public static LogicalPathname create(String namestring) {
86     // parse host out then call create(host, rest);
87     LogicalPathname result = null;
88     if (LogicalPathname.isValidLogicalPathname(namestring)) {
89       String h = LogicalPathname.getHostString(namestring);
90       result
91         = LogicalPathname.create(h, namestring.substring(namestring.indexOf(':') + 1));
92       return result;
93     }
94     error(new FileError("Failed to find a valid logical Pathname host in '" + namestring + "'",
95                         NIL));  // ??? return NIL as we don't have a
96                                 // PATHNAME.  Maybe signal a different
97                                 // condition?
98     return (LogicalPathname)UNREACHED;
99   }
100 
create(String host, String rest)101   public static LogicalPathname create(String host, String rest) {
102     // This may be "too late" in the creation chain to be meaningful?
103     SimpleString h = new SimpleString(host);
104     if (LogicalPathname.TRANSLATIONS.get(h) == null) {
105       // Logical pathnames are only valid when it's host exists
106       String message = MessageFormat.format("'{0}' is not a defined logical host", host);
107       error(new SimpleError(message));
108     }
109     LogicalPathname result = new LogicalPathname();
110     final int limit = rest.length();
111     for (int i = 0; i < limit; i++) {
112       char c = rest.charAt (i);
113       if (LOGICAL_PATHNAME_CHARS.indexOf(c) < 0) {
114         error(new ParseError("The character #\\" + c + " is not valid in a logical pathname."));
115 
116       }
117     }
118 
119     result.setHost(h);
120 
121     // "The device component of a logical pathname is always :UNSPECIFIC;
122     // no other component of a logical pathname can be :UNSPECIFIC."
123     result.setDevice(Keyword.UNSPECIFIC);
124 
125     int semi = rest.lastIndexOf(';');
126     if (semi >= 0) {
127       // Directory.
128       String d = rest.substring(0, semi + 1);
129       result.setDirectory(parseDirectory(d));
130       rest = rest.substring(semi + 1);
131     } else {
132       // "If a relative-directory-marker precedes the directories, the
133       // directory component parsed is as relative; otherwise, the
134             // directory component is parsed as absolute."
135           result.setDirectory(new Cons(Keyword.ABSOLUTE));
136     }
137 
138     int dot = rest.indexOf('.');
139     if (dot >= 0) {
140       String n = rest.substring(0, dot);
141       if (n.equals("*")) {
142         result.setName(Keyword.WILD);
143       } else {
144         result.setName(new SimpleString(n.toUpperCase()));
145       }
146       rest = rest.substring(dot + 1);
147       dot = rest.indexOf('.');
148       if (dot >= 0) {
149         String t = rest.substring(0, dot);
150         if (t.equals("*")) {
151           result.setType(Keyword.WILD);
152         } else {
153           result.setType(new SimpleString(t.toUpperCase()));
154         }
155         // What's left is the version.
156         String v = rest.substring(dot + 1);
157         if (v.equals("*")) {
158           result.setVersion(Keyword.WILD);
159         } else if (v.equals("NEWEST") || v.equals("newest")) {
160           result.setVersion(Keyword.NEWEST);
161         } else {
162           result.setVersion(PACKAGE_CL.intern("PARSE-INTEGER").execute(new SimpleString(v)));
163         }
164       } else {
165         String t = rest;
166         if (t.equals("*")) {
167           result.setType(Keyword.WILD);
168         } else {
169           result.setType(new SimpleString(t.toUpperCase()));
170         }
171       }
172     } else {
173       String n = rest;
174       if (n.equals("*")) {
175         result.setName(Keyword.WILD);
176       } else if (n.length() > 0) {
177         result.setName(new SimpleString(n.toUpperCase()));
178       }
179     }
180     return result;
181   }
182 
canonicalizeStringComponent(AbstractString s)183   public static final SimpleString canonicalizeStringComponent(AbstractString s) {
184     final int limit = s.length();
185     for (int i = 0; i < limit; i++) {
186       char c = s.charAt(i);
187       if (LOGICAL_PATHNAME_CHARS.indexOf(c) < 0) {
188         error(new ParseError("Invalid character #\\" + c +
189                              " in logical pathname component \"" + s +
190                              '"'));
191         // Not reached.
192         return null;
193       }
194     }
195     return new SimpleString(s.getStringValue().toUpperCase());
196   }
197 
translateLogicalPathname(LogicalPathname pathname)198   public static Pathname translateLogicalPathname(LogicalPathname pathname) {
199     return (Pathname) Symbol.TRANSLATE_LOGICAL_PATHNAME.execute(pathname);
200   }
201 
parseDirectory(String s)202   private static final LispObject parseDirectory(String s) {
203     LispObject result;
204     if (s.charAt(0) == ';') {
205       result = new Cons(Keyword.RELATIVE);
206       s = s.substring(1);
207     } else
208       result = new Cons(Keyword.ABSOLUTE);
209     StringTokenizer st = new StringTokenizer(s, ";");
210     while (st.hasMoreTokens()) {
211       String token = st.nextToken();
212       LispObject obj;
213       if (token.equals("*"))
214         obj = Keyword.WILD;
215       else if (token.equals("**"))
216         obj = Keyword.WILD_INFERIORS;
217       else if (token.equals("..")) {
218         if (result.car() instanceof AbstractString) {
219           result = result.cdr();
220           continue;
221         }
222         obj= Keyword.UP;
223       } else
224         obj = new SimpleString(token.toUpperCase());
225       result = new Cons(obj, result);
226     }
227     return result.nreverse();
228   }
229 
230   @Override
typeOf()231   public LispObject typeOf() {
232     return Symbol.LOGICAL_PATHNAME;
233   }
234 
235   @Override
classOf()236   public LispObject classOf() {
237     return BuiltInClass.LOGICAL_PATHNAME;
238   }
239 
240   @Override
typep(LispObject type)241   public LispObject typep(LispObject type) {
242     if (type == Symbol.LOGICAL_PATHNAME)
243       return T;
244     if (type == BuiltInClass.LOGICAL_PATHNAME)
245       return T;
246     return super.typep(type);
247   }
248 
249   @Override
getDirectoryNamestring()250   protected String getDirectoryNamestring() {
251     StringBuilder sb = new StringBuilder();
252     // "If a pathname is converted to a namestring, the symbols NIL and
253     // :UNSPECIFIC cause the field to be treated as if it were empty. That
254     // is, both NIL and :UNSPECIFIC cause the component not to appear in
255     // the namestring." 19.2.2.2.3.1
256     if (getDirectory() != NIL) {
257       LispObject temp = getDirectory();
258       LispObject part = temp.car();
259       if (part == Keyword.ABSOLUTE) {
260       } else if (part == Keyword.RELATIVE)
261         sb.append(';');
262       else
263         error(new FileError("Unsupported directory component " + part.princToString() + ".",
264                             this));
265       temp = temp.cdr();
266       while (temp != NIL) {
267         part = temp.car();
268         if (part instanceof AbstractString)
269           sb.append(part.getStringValue());
270         else if (part == Keyword.WILD)
271           sb.append('*');
272         else if (part == Keyword.WILD_INFERIORS)
273           sb.append("**");
274         else if (part == Keyword.UP)
275           sb.append("..");
276         else
277           error(new FileError("Unsupported directory component " + part.princToString() + ".",
278                               this));
279         sb.append(';');
280         temp = temp.cdr();
281       }
282     }
283     return sb.toString();
284   }
285 
286   @Override
printObject()287   public String printObject() {
288     final LispThread thread = LispThread.currentThread();
289     boolean printReadably = (Symbol.PRINT_READABLY.symbolValue(thread) != NIL);
290     boolean printEscape = (Symbol.PRINT_ESCAPE.symbolValue(thread) != NIL);
291     StringBuilder sb = new StringBuilder();
292     if (printReadably || printEscape)
293       sb.append("#P\"");
294     sb.append(getHost().getStringValue());
295     sb.append(':');
296     if (getDirectory() != NIL)
297       sb.append(getDirectoryNamestring());
298     if (getName() != NIL) {
299       if (getName() == Keyword.WILD)
300         sb.append('*');
301       else
302         sb.append(getName().getStringValue());
303     }
304     if (getType() != NIL) {
305       sb.append('.');
306       if (getType() == Keyword.WILD)
307         sb.append('*');
308       else
309         sb.append(getType().getStringValue());
310     }
311     if (getVersion().integerp()) {
312       sb.append('.');
313       int base = Fixnum.getValue(Symbol.PRINT_BASE.symbolValue(thread));
314       if (getVersion() instanceof Fixnum)
315         sb.append(Integer.toString(((Fixnum)getVersion()).value, base).toUpperCase());
316       else if (getVersion() instanceof Bignum)
317         sb.append(((Bignum)getVersion()).value.toString(base).toUpperCase());
318     } else if (getVersion() == Keyword.WILD) {
319       sb.append(".*");
320     } else if (getVersion() == Keyword.NEWEST) {
321       sb.append(".NEWEST");
322     }
323     if (printReadably || printEscape)
324       sb.append('"');
325     return sb.toString();
326   }
327 
328     // ### canonicalize-logical-host host => canonical-host
329     private static final Primitive CANONICALIZE_LOGICAL_HOST = new canonicalize_logical_host();
330     private static class canonicalize_logical_host extends Primitive {
canonicalize_logical_host()331         canonicalize_logical_host() {
332             super("canonicalize-logical-host", PACKAGE_SYS, true, "host");
333         }
334         @Override
execute(LispObject arg)335         public LispObject execute(LispObject arg)
336         {
337             AbstractString s = checkString(arg);
338             if (s.length() == 0) {
339                 // "The null string, "", is not a valid value for any
340                 // component of a logical pathname." 19.3.2.2
341                 return error(new LispError("Invalid logical host name: \"" +
342                                            s.getStringValue() + '"'));
343             }
344             return canonicalizeStringComponent(s);
345         }
346     }
347 
348     // ### %make-logical-pathname namestring => logical-pathname
349     private static final Primitive _MAKE_LOGICAL_PATHNAME = new _make_logical_pathname();
350     private static class _make_logical_pathname extends Primitive {
_make_logical_pathname()351         _make_logical_pathname() {
352             super("%make-logical-pathname", PACKAGE_SYS, true, "namestring");
353         }
354         @Override
execute(LispObject arg)355         public LispObject execute(LispObject arg)
356 
357         {
358             // Check for a logical pathname host.
359             String s = arg.getStringValue();
360             String h = getHostString(s);
361             if (h != null) {
362                 if (h.length() == 0) {
363                     // "The null string, "", is not a valid value for any
364                     // component of a logical pathname." 19.3.2.2
365                     return error(new LispError("Invalid logical host name: \"" +
366                                                 h + '"'));
367                 }
368                 if (LogicalPathname.TRANSLATIONS.get(new SimpleString(h)) != null) {
369                     // A defined logical pathname host.
370                     return LogicalPathname.create(h, s.substring(s.indexOf(':') + 1));
371                 }
372             }
373             return error(new TypeError("Logical namestring does not specify a host: \"" + s + '"'));
374         }
375     }
376 
377   // "one or more uppercase letters, digits, and hyphens"
getHostString(String s)378   protected static String getHostString(String s) {
379     int colon = s.indexOf(':');
380     if (colon >= 0) {
381       return s.substring(0, colon).toUpperCase();
382     } else {
383       return null;
384     }
385   }
386 
getLastModified()387     public long getLastModified() {
388         Pathname p = translateLogicalPathname(this);
389         return p.getLastModified();
390     }
391 }
392