1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.lib.meta;
20
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileReader;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.Reader;
30 import java.security.AccessController;
31 import java.security.PrivilegedActionException;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Map;
39
40 import org.apache.commons.lang.exception.NestableRuntimeException;
41 import org.apache.openjpa.lib.util.Files;
42 import org.apache.openjpa.lib.util.J2DoPrivHelper;
43 import org.apache.openjpa.lib.util.Localizer;
44 import serp.bytecode.lowlevel.ConstantPoolTable;
45 import serp.util.Strings;
46
47 /**
48 * Parser used to resolve arguments into java classes.
49 * Interprets command-line args as either class names, .class files or
50 * resources, .java files or resources, or metadata files or resources
51 * conforming to the common format defined by {@link CFMetaDataParser}.
52 * Transforms the information in these args into {@link Class} instances.
53 * Note that when parsing .java files, only the main class in the file
54 * is detected. Other classes defined in the file, such as inner classes,
55 * are not added to the returned classes list.
56 *
57 * @author Abe White
58 * @nojavadoc
59 */
60 public class ClassArgParser {
61
62 private static final int TOKEN_EOF = -1;
63 private static final int TOKEN_NONE = 0;
64 private static final int TOKEN_PACKAGE = 1;
65 private static final int TOKEN_CLASS = 2;
66 private static final int TOKEN_PACKAGE_NOATTR = 3;
67 private static final int TOKEN_CLASS_NOATTR = 4;
68
69 private static final Localizer _loc = Localizer.forPackage
70 (ClassArgParser.class);
71
72 private ClassLoader _loader = null;
73 private char[] _packageAttr = "name".toCharArray();
74 private char[] _classAttr = "name".toCharArray();
75 private char[][] _beginElements = { { 'p' }, { 'c' } };
76 private char[][] _endElements = { "ackage".toCharArray(),
77 "lass".toCharArray() };
78
79 /**
80 * The class loader with which to load parsed classes.
81 */
82 public ClassLoader getClassLoader() {
83 return _loader;
84 }
85
86 /**
87 * The class loader with which to load parsed classes.
88 */
89 public void setClassLoader(ClassLoader loader) {
90 _loader = loader;
91 }
92
93 /**
94 * Set the the relevant metadata file structure so that metadata files
95 * containing class names can be parsed. Null attribute names indicate
96 * that the text content of the element contains the data.
97 */
98 public void setMetaDataStructure(String packageElementName,
99 String packageAttributeName, String[] classElementNames,
100 String classAttributeName) {
101 // calculate how many chars deep we have to go to identify each element
102 // name as unique. this is extremely inefficient for large N, but
103 // should never be called for more than a few elements
104 char[] buf = new char[classElementNames.length + 1];
105 int charIdx = 0;
106 for (; true; charIdx++) {
107 for (int i = 0; i < buf.length; i++) {
108 if (i == 0) {
109 if (charIdx == packageElementName.length())
110 throw new UnsupportedOperationException(_loc.get
111 ("cant-diff-elems").getMessage());
112 buf[i] = packageElementName.charAt(charIdx);
113 } else {
114 if (charIdx == classElementNames[i - 1].length())
115 throw new UnsupportedOperationException(_loc.get
116 ("cant-diff-elems").getMessage());
117 buf[i] = classElementNames[i - 1].charAt(charIdx);
118 }
119 }
120 if (charsUnique(buf))
121 break;
122 }
123
124 _packageAttr = (packageAttributeName == null) ? null
125 : packageAttributeName.toCharArray();
126 _classAttr = (classAttributeName == null) ? null
127 : classAttributeName.toCharArray();
128 _beginElements = new char[classElementNames.length + 1][];
129 _endElements = new char[classElementNames.length + 1][];
130 _beginElements[0] = packageElementName.substring(0, charIdx + 1).
131 toCharArray();
132 _endElements[0] = packageElementName.substring(charIdx + 1).
133 toCharArray();
134 for (int i = 0; i < classElementNames.length; i++) {
135 _beginElements[i + 1] = classElementNames[i].
136 substring(0, charIdx + 1).toCharArray();
137 _endElements[i + 1] = classElementNames[i].
138 substring(charIdx + 1).toCharArray();
139 }
140 }
141
142 /**
143 * Return true if all characters in given buffer are unique.
144 */
145 private static boolean charsUnique(char[] buf) {
146 for (int i = buf.length - 1; i >= 0; i--)
147 for (int j = 0; j < i; j++)
148 if (buf[j] == buf[i])
149 return false;
150 return true;
151 }
152
153 /**
154 * Return the {@link Class} representation of the class(es) named in the
155 * given arg.
156 *
157 * @param arg a class name, .java file, .class file, or metadata
158 * file naming the type(s) to act on
159 */
160 public Class[] parseTypes(String arg) {
161 String[] names = parseTypeNames(arg);
162 Class[] objs = new Class[names.length];
163 for (int i = 0; i < names.length; i++)
164 objs[i] = Strings.toClass(names[i], _loader);
165 return objs;
166 }
167
168 /**
169 * Return the {@link Class} representation of the class(es) named in the
170 * given metadatas.
171 */
172 public Class[] parseTypes(MetaDataIterator itr) {
173 String[] names = parseTypeNames(itr);
174 Class[] objs = new Class[names.length];
175 for (int i = 0; i < names.length; i++)
176 objs[i] = Strings.toClass(names[i], _loader);
177 return objs;
178 }
179
180 /**
181 * Return a mapping of each metadata resource to an array of its
182 * contained classes.
183 */
184 public Map mapTypes(MetaDataIterator itr) {
185 Map map = mapTypeNames(itr);
186 Map.Entry entry;
187 String[] names;
188 Class[] objs;
189 for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
190 entry = (Map.Entry) i.next();
191 names = (String[]) entry.getValue();
192 objs = new Class[names.length];
193 for (int j = 0; j < names.length; j++)
194 objs[j] = Strings.toClass(names[j], _loader);
195 entry.setValue(objs);
196 }
197 return map;
198 }
199
200 /**
201 * Return the names of the class(es) from the given arg.
202 *
203 * @param arg a class name, .java file, .class file, or metadata
204 * file naming the type(s) to act on
205 * @throws IllegalArgumentException with appropriate message on error
206 */
207 public String[] parseTypeNames(String arg) {
208 if (arg == null)
209 return new String[0];
210
211 try {
212 File file = Files.getFile(arg, _loader);
213 if (arg.endsWith(".class"))
214 return new String[]{ getFromClassFile(file) };
215 if (arg.endsWith(".java"))
216 return new String[]{ getFromJavaFile(file) };
217 if (((Boolean) AccessController.doPrivileged(
218 J2DoPrivHelper.existsAction(file))).booleanValue()) {
219 Collection col = getFromMetaDataFile(file);
220 return (String[]) col.toArray(new String[col.size()]);
221 }
222 } catch (Exception e) {
223 throw new NestableRuntimeException(
224 _loc.get("class-arg", arg).getMessage(), e);
225 }
226
227 // must be a class name
228 return new String[]{ arg };
229 }
230
231 /**
232 * Return the names of the class(es) from the given metadatas.
233 */
234 public String[] parseTypeNames(MetaDataIterator itr) {
235 if (itr == null)
236 return new String[0];
237
238 List names = new ArrayList();
239 Object source = null;
240 try {
241 while (itr.hasNext()) {
242 source = itr.next();
243 appendTypeNames(source, itr.getInputStream(), names);
244 }
245 } catch (Exception e) {
246 throw new NestableRuntimeException(
247 _loc.get("class-arg", source).getMessage(), e);
248 }
249 return (String[]) names.toArray(new String[names.size()]);
250 }
251
252 /**
253 * Parse the names in the given metadata iterator stream, closing the
254 * stream on completion.
255 */
256 private void appendTypeNames(Object source, InputStream in, List names)
257 throws IOException {
258 try {
259 if (source.toString().endsWith(".class"))
260 names.add(getFromClass(in));
261 names.addAll(getFromMetaData(new InputStreamReader(in)));
262 } finally {
263 try {
264 in.close();
265 } catch (IOException ioe) {
266 }
267 }
268 }
269
270 /**
271 * Return a mapping of each metadata resource to an array of its contained
272 * class names.
273 */
274 public Map mapTypeNames(MetaDataIterator itr) {
275 if (itr == null)
276 return Collections.EMPTY_MAP;
277
278 Map map = new HashMap();
279 Object source = null;
280 List names = new ArrayList();
281 try {
282 while (itr.hasNext()) {
283 source = itr.next();
284 appendTypeNames(source, itr.getInputStream(), names);
285 if (!names.isEmpty())
286 map.put(source, (String[]) names.toArray
287 (new String[names.size()]));
288 names.clear();
289 }
290 } catch (Exception e) {
291 throw new NestableRuntimeException(
292 _loc.get("class-arg", source).getMessage(), e);
293 }
294 return map;
295 }
296
297 /**
298 * Returns the class named in the given .class file.
299 */
300 private String getFromClassFile(File file) throws IOException {
301 FileInputStream fin = null;
302 try {
303 fin = (FileInputStream) AccessController.doPrivileged(
304 J2DoPrivHelper.newFileInputStreamAction(file));
305 return getFromClass(fin);
306 } catch (PrivilegedActionException pae) {
307 throw (FileNotFoundException) pae.getException();
308 } finally {
309 if (fin != null)
310 try {
311 fin.close();
312 } catch (IOException ioe) {
313 }
314 }
315 }
316
317 /**
318 * Returns the class name in the given .class bytecode.
319 */
320 private String getFromClass(InputStream in) throws IOException {
321 ConstantPoolTable table = new ConstantPoolTable(in);
322 int idx = table.getEndIndex();
323 idx += 2; // access flags
324 int clsEntry = table.readUnsignedShort(idx);
325 int utfEntry = table.readUnsignedShort(table.get(clsEntry));
326 return table.readString(table.get(utfEntry)).replace('/', '.');
327 }
328
329 /**
330 * Returns the class named in the given .java file.
331 */
332 private String getFromJavaFile(File file) throws IOException {
333 BufferedReader in = null;
334 try {
335 // find the line with the package declaration
336 in = new BufferedReader(new FileReader(file));
337 String line;
338 StringBuffer pack = null;
339 while ((line = in.readLine()) != null) {
340 line = line.trim();
341 if (line.startsWith("package ")) {
342 line = line.substring(8).trim();
343
344 // strip off anything beyond the package declaration
345 pack = new StringBuffer();
346 for (int i = 0; i < line.length(); i++) {
347 if (Character.isJavaIdentifierPart(line.charAt(i))
348 || line.charAt(i) == '.')
349 pack.append(line.charAt(i));
350 else
351 break;
352 }
353 break;
354 }
355 }
356
357 // strip '.java'
358 String clsName = file.getName();
359 clsName = clsName.substring(0, clsName.length() - 5);
360
361 // prefix with package
362 if (pack != null && pack.length() > 0)
363 clsName = pack + "." + clsName;
364
365 return clsName;
366 } finally {
367 if (in != null)
368 try { in.close(); } catch (IOException ioe) {}
369 }
370 }
371
372 /**
373 * Returns the classes named in the given common format metadata file.
374 */
375 private Collection getFromMetaDataFile(File file) throws IOException {
376 FileReader in = null;
377 try {
378 in = new FileReader(file);
379 return getFromMetaData(in);
380 } finally {
381 if (in != null)
382 try {
383 in.close();
384 } catch (IOException ioe) {
385 }
386 }
387 }
388
389 /**
390 * Returns the classes named in the given common format metadata stream.
391 */
392 private Collection getFromMetaData(Reader xml) throws IOException {
393 Collection names = new ArrayList();
394 BufferedReader in = new BufferedReader(xml);
395
396 boolean comment = false;
397 int token = TOKEN_NONE;
398 String pkg = "";
399 String name;
400 read:
401 for (int ch = 0, last = 0, last2 = 0;
402 ch == '<' || (ch = in.read()) != -1; last2 = last, last = ch) {
403 // handle comments
404 if (comment && last2 == '-' && last == '-' && ch == '>') {
405 comment = false;
406 continue;
407 }
408 if (comment) {
409 if (ch == '<') {
410 ch = in.read();
411 if (ch == -1)
412 break read;
413 }
414 continue;
415 }
416 if (last2 == '<' && last == '!' && ch == '-') {
417 comment = true;
418 continue;
419 }
420
421 // if not an element start, skip it
422 if (ch != '<')
423 continue;
424 token = TOKEN_NONE; // reset token
425 last = ch; // update needed for comment detection
426 ch = readThroughWhitespace(in);
427 if (ch == '/' || ch == '!' || ch == '?')
428 continue;
429
430 // read element name; look for packages and classes
431 token = readElementToken(ch, in);
432 switch (token) {
433 case TOKEN_EOF:
434 break read;
435 case TOKEN_PACKAGE:
436 pkg = readAttribute(in, _packageAttr);
437 if (pkg == null)
438 break read;
439 break;
440 case TOKEN_PACKAGE_NOATTR:
441 pkg = readElementText(in);
442 if (pkg == null)
443 break read;
444 ch = '<'; // reading element text reads to next '<'
445 break;
446 case TOKEN_CLASS:
447 name = readAttribute(in, _classAttr);
448 if (name == null)
449 break read;
450 if (pkg.length() > 0 && name.indexOf('.') == -1)
451 names.add(pkg + "." + name);
452 else
453 names.add(name);
454 break;
455 case TOKEN_CLASS_NOATTR:
456 name = readElementText(in);
457 if (name == null)
458 break read;
459 ch = '<'; // reading element text reads to next '<'
460 if (pkg.length() > 0 && name.indexOf('.') == -1)
461 names.add(pkg + "." + name);
462 else
463 names.add(name);
464 break;
465 }
466 }
467 return names;
468 }
469
470 /**
471 * Read the name of the current XML element and return the matching token.
472 */
473 private int readElementToken(int ch, Reader in) throws IOException {
474 // look through the beginning element names to find what element this
475 // might be(if any)
476 int matchIdx = -1;
477 int matched = 0;
478 int dq = 0;
479 for (int beginIdx = 0; beginIdx < _beginElements[0].length; beginIdx++)
480 {
481 if (beginIdx != 0)
482 ch = in.read();
483 if (ch == -1)
484 return TOKEN_EOF;
485
486 matched = 0;
487 for (int i = 0; i < _beginElements.length; i++) {
488 if ((dq & (2 << i)) != 0)
489 continue;
490
491 if (ch == _beginElements[i][beginIdx]) {
492 matchIdx = i;
493 matched++;
494 } else
495 dq |= 2 << i;
496 }
497
498 if (matched == 0)
499 break;
500 }
501 if (matched != 1)
502 return TOKEN_NONE;
503
504 // make sure the rest of the element name matches
505 char[] match = _endElements[matchIdx];
506 for (int i = 0; i < match.length; i++) {
507 ch = in.read();
508 if (ch == -1)
509 return TOKEN_EOF;
510 if (ch != match[i])
511 return TOKEN_NONE;
512 }
513
514 // read the next char to make sure we finished the element name
515 ch = in.read();
516 if (ch == -1)
517 return TOKEN_EOF;
518 if (ch == '>') {
519 if (matchIdx == 0 && _packageAttr == null)
520 return TOKEN_PACKAGE_NOATTR;
521 if (matchIdx != 0 && _classAttr == null)
522 return TOKEN_CLASS_NOATTR;
523 } else if (Character.isWhitespace((char) ch)) {
524 if (matchIdx == 0 && _packageAttr != null)
525 return TOKEN_PACKAGE;
526 if (matchIdx != 0 && _classAttr != null)
527 return TOKEN_CLASS;
528 }
529 return TOKEN_NONE;
530 }
531
532 /**
533 * Read the attribute with the given name in chars of the current XML
534 * element.
535 */
536 private String readAttribute(Reader in, char[] name) throws IOException {
537 int expected = 0;
538 for (int ch, last = 0; true; last = ch) {
539 ch = in.read();
540 if (ch == -1)
541 return null;
542 if (ch == '>')
543 return "";
544
545 // if not expected char or still looking for 'n' and previous
546 // char is not whitespace, keep looking
547 if (ch != name[expected] || (expected == 0 && last != 0
548 && !Character.isWhitespace((char) last))) {
549 expected = 0;
550 continue;
551 }
552
553 // found expected char; have we found the whole "name"?
554 expected++;
555 if (expected == name.length) {
556 // make sure the next char is '='
557 ch = readThroughWhitespace(in);
558 if (ch == -1)
559 return null;
560 if (ch != '=') {
561 expected = 0;
562 continue;
563 }
564
565 // toss out any subsequent whitespace and the next char, which
566 // is the opening quote for the attr value, then read until the
567 // closing quote
568 readThroughWhitespace(in);
569 return readAttributeValue(in);
570 }
571 }
572 }
573
574 /**
575 * Read the current text value until the next element.
576 */
577 private String readElementText(Reader in) throws IOException {
578 StringBuffer buf = null;
579 int ch;
580 while (true) {
581 ch = in.read();
582 if (ch == -1)
583 return null;
584 if (ch == '<')
585 break;
586 if (Character.isWhitespace((char) ch))
587 continue;
588 if (buf == null)
589 buf = new StringBuffer();
590 buf.append((char) ch);
591 }
592 return (buf == null) ? "" : buf.toString();
593 }
594
595 /**
596 * Read until the next non-whitespace character.
597 */
598 private int readThroughWhitespace(Reader in) throws IOException {
599 int ch;
600 while (true) {
601 ch = in.read();
602 if (ch == -1 || !Character.isWhitespace((char) ch))
603 return ch;
604 }
605 }
606
607 /**
608 * Return the current attribute value.
609 */
610 private String readAttributeValue(Reader in) throws IOException {
611 StringBuffer buf = new StringBuffer();
612 int ch;
613 while (true) {
614 ch = in.read();
615 if (ch == -1)
616 return null;
617 if (ch == '\'' || ch == '"')
618 return buf.toString();
619 buf.append((char) ch);
620 }
621 }
622 }