001: /*
002: * Enhydra Java Application Server Project
003: *
004: * The contents of this file are subject to the Enhydra Public License
005: * Version 1.1 (the "License"); you may not use this file except in
006: * compliance with the License. You may obtain a copy of the License on
007: * the Enhydra web site ( http://www.enhydra.org/ ).
008: *
009: * Software distributed under the License is distributed on an "AS IS"
010: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
011: * the License for the specific terms governing rights and limitations
012: * under the License.
013: *
014: * The Initial Developer of the Enhydra Application Server is Lutris
015: * Technologies, Inc. The Enhydra Application Server and portions created
016: * by Lutris Technologies, Inc. are Copyright Lutris Technologies, Inc.
017: * All Rights Reserved.
018: *
019: * Contributor(s):
020: *
021: */
022: package org.enhydra.kelp.common.node;
023:
024: // Toolbox imports
025: import org.enhydra.tool.common.PathHandle;
026: import org.enhydra.tool.ToolBoxInfo;
027:
028: // Enhydra imports
029: import org.enhydra.xml.xmlc.compiler.Parse;
030: import org.enhydra.xml.xmlc.XMLCException;
031:
032: // Kelp imports
033: import org.enhydra.kelp.common.Constants;
034: import org.enhydra.kelp.common.PropUtil;
035: import org.enhydra.kelp.common.bridge.XMLCConnectionFactory;
036: import org.enhydra.kelp.common.bridge.MetaDataHandler;
037: import org.enhydra.kelp.common.map.Mapper;
038: import org.enhydra.kelp.common.node.OtterProject;
039:
040: // Standard imports
041: import java.io.File;
042: import java.io.IOException;
043: import java.io.PrintWriter;
044: import java.lang.reflect.Constructor;
045: import java.lang.reflect.Method;
046: import java.lang.reflect.InvocationTargetException;
047: import java.util.ArrayList;
048: import java.util.Arrays;
049: import java.util.Vector;
050: import java.util.StringTokenizer;
051: import java.util.ResourceBundle;
052:
053: /**
054: * Class declaration
055: *
056: *
057: * @author Paul Mahar
058: */
059: public class OtterXMLCNode implements OtterDocumentNode {
060:
061: // strings not to be resourced
062: static ResourceBundle res = ResourceBundle
063: .getBundle("org.enhydra.kelp.common.Res"); // nores
064: private final String POUND = "#"; // nores
065: private final String DOMFACTORY = "-domfactory"; // nores
066: private final String IMPL = "Impl"; // nores
067:
068: //
069: public final static int CLASS_NAME_DEFAULT = 1;
070: public final static int CLASS_NAME_CUSTOM = 2;
071: public final static int CLASS_NAME_MAPPED = 3;
072:
073: //
074: private MetaDataHandler metaDataHandler;
075: private OtterDocumentNode docNode;
076:
077: /**
078: * Create a node to use when calling XMLC.
079: *
080: */
081: public OtterXMLCNode(OtterDocumentNode d) {
082: docNode = d;
083: initMetaDataHandler();
084: }
085:
086: /**
087: * Method declaration
088: *
089: *
090: * @return
091: */
092: public String getXMLCOptionFilePath() {
093: return docNode.getXMLCOptionFilePath();
094: }
095:
096: /**
097: * Method declaration
098: *
099: *
100: * @param n
101: */
102: public void setXMLCOptionFilePath(String n) {
103: docNode.setXMLCOptionFilePath(n);
104: }
105:
106: /**
107: * Method declaration
108: *
109: *
110: * @return
111: */
112: public String getXMLCParameters() {
113: String params = new String();
114: StringBuffer buf = new StringBuffer();
115: int index = -1;
116:
117: params = docNode.getXMLCParameters();
118: params = PropUtil.removeQuotes(params);
119: return params;
120: }
121:
122: /**
123: * Method declaration
124: *
125: *
126: * @param p
127: */
128: public void setXMLCParameters(String p) {
129: docNode.setXMLCParameters(p);
130: }
131:
132: /**
133: * Method declaration
134: *
135: *
136: * @return
137: */
138: public boolean isSelected() {
139: return docNode.isSelected();
140: }
141:
142: /**
143: * Method declaration
144: *
145: *
146: * @param b
147: */
148: public void setSelected(boolean b) {
149: docNode.setSelected(b);
150: if (b) {
151: setStatic(false);
152: }
153: }
154:
155: public boolean isStatic() {
156: return docNode.isStatic();
157: }
158:
159: public void setStatic(boolean b) {
160: docNode.setStatic(b);
161: if (b) {
162: setSelected(false);
163: }
164: }
165:
166: /**
167: * Method declaration
168: *
169: *
170: * @return
171: */
172: public String getFilePath() {
173: return docNode.getFilePath();
174: }
175:
176: /**
177: * Method declaration
178: *
179: *
180: * @return
181: */
182: public String getCustomClassName() {
183: String custom = getProperty(PropertyKeys.NAME_XMLC_CUSTOM);
184:
185: if (custom == null) {
186: custom = new String();
187: }
188: return custom;
189: }
190:
191: /**
192: * Method declaration
193: *
194: *
195: * @param n
196: */
197: public void setCustomClassName(String n) {
198: setProperty(PropertyKeys.NAME_XMLC_CUSTOM, n);
199: }
200:
201: /**
202: * Method declaration
203: *
204: *
205: * @return
206: */
207: public Object getNativeNode() {
208: return docNode.getNativeNode();
209: }
210:
211: /**
212: * Method declaration
213: *
214: *
215: * @param o
216: */
217: public void setNativeNode(Object o) {
218: docNode.setNativeNode(o);
219: }
220:
221: /**
222: * Method declaration
223: *
224: *
225: * @return
226: */
227: public OtterProject getProject() {
228: return docNode.getProject();
229: }
230:
231: /**
232: * Method declaration
233: *
234: *
235: * @param n
236: *
237: * @return
238: */
239: public String getProperty(String n) {
240: return docNode.getProperty(n);
241: }
242:
243: /**
244: * Method declaration
245: *
246: *
247: * @param n
248: * @param v
249: */
250: public void setProperty(String n, String v) {
251: docNode.setProperty(n, v);
252: }
253:
254: /**
255: * Method declaration
256: *
257: *
258: * @param n
259: * @param v
260: */
261: public void setProperty(String n, int v) {
262: docNode.setProperty(n, v);
263: }
264:
265: /**
266: * Method declaration
267: *
268: *
269: * @return
270: */
271: public int getClassNameType() {
272:
273: // When getting the project scope overrides
274: // local type.
275: int type;
276: int projectScope;
277: String stringType = getProperty(PropertyKeys.NAME_XMLC_TYPE);
278:
279: projectScope = getProject().getMapScope();
280: type = PropUtil.stringToInt(stringType, CLASS_NAME_MAPPED);
281: switch (type) {
282: case CLASS_NAME_DEFAULT:
283: case CLASS_NAME_CUSTOM:
284: if (projectScope == OtterProject.MAP_SCOPE_ALL) {
285: type = CLASS_NAME_MAPPED;
286: setProperty(PropertyKeys.NAME_XMLC_TYPE, type);
287: }
288: break;
289: case CLASS_NAME_MAPPED:
290: if (projectScope == OtterProject.MAP_SCOPE_NONE) {
291: type = CLASS_NAME_DEFAULT;
292: setProperty(PropertyKeys.NAME_XMLC_TYPE, type);
293: }
294: break;
295: }
296: return type;
297: }
298:
299: /**
300: * Method declaration
301: *
302: *
303: * @param newType
304: */
305: public void setClassNameType(int newType) {
306:
307: // If conflict exists with project scope,
308: // change project scope.
309: int oldType = getClassNameType();
310: int projectScope = getProject().getMapScope();
311:
312: setProperty(PropertyKeys.NAME_XMLC_TYPE, newType);
313: syncProjectToType(newType);
314: if ((newType != oldType)
315: || (newType == OtterXMLCNode.CLASS_NAME_MAPPED)) {
316: initClassName();
317: }
318: }
319:
320: /**
321: * Method declaration
322: *
323: *
324: * @param forPrint
325: *
326: * @return
327: */
328: private String[] getAllParameters(boolean forPrint) {
329: StringBuffer buf = new StringBuffer();
330: String[] parameters = new String[0];
331: StringTokenizer tokenizer = null;
332: int count = 0;
333: int index = 0;
334:
335: // File - Node
336: if (getXMLCParameters() != null) {
337: buf.append(getXMLCParameters().trim());
338: }
339: if (forPrint) {
340: buf.append('#');
341: } else {
342: buf.append(' ');
343: }
344:
345: // Package - Folder
346: OtterNode packNode = null;
347: String packParam = new String();
348:
349: packNode = docNode.getParent();
350: if (packNode == null || packNode instanceof OtterProject) {
351:
352: // skip it.
353: } else {
354: packParam = packNode.getXMLCParameters();
355: }
356: if ((packParam != null) && (packParam.trim().length() > 0)) {
357: buf.append(packParam);
358: if (forPrint) {
359: buf.append('#');
360: } else {
361: buf.append(' ');
362: }
363: }
364:
365: // Project
366: if (docNode.getProject().getXMLCParameters() != null) {
367: buf.append(docNode.getProject().getXMLCParameters().trim());
368: }
369: if (forPrint) {
370: tokenizer = new StringTokenizer(buf.toString().trim(),
371: POUND);
372: } else {
373: tokenizer = new StringTokenizer(buf.toString().trim());
374: }
375: count = tokenizer.countTokens();
376: parameters = new String[tokenizer.countTokens()];
377: while (tokenizer.hasMoreTokens()) {
378: parameters[index] = tokenizer.nextToken();
379: index++;
380: }
381:
382: // Add domFactory if required
383: parameters = addDomFactory(parameters);
384: return parameters;
385: }
386:
387: private String[] addDomFactory(String[] in) {
388: PathHandle path = null;
389: String[] out = in;
390: ArrayList list = null;
391: boolean found = false;
392:
393: path = PathHandle.createPathHandle(docNode.getFilePath());
394: for (int i = 0; i < in.length; i++) {
395: if (in[i].trim().equalsIgnoreCase(DOMFACTORY)) {
396: found = true;
397: break;
398: }
399: }
400: if (found) {
401:
402: // don't overwride
403: } else if (path.hasExtension(Constants.TYPE_HTML)
404: || path.hasExtension(Constants.TYPE_HTM)) {
405:
406: // use default
407: } else if (path.hasExtension(Constants.TYPE_WML)
408: && ToolBoxInfo
409: .isClassAvailable(ToolBoxInfo.WML_FACTORY)) {
410: list = new ArrayList(Arrays.asList(in));
411: list.add(DOMFACTORY);
412: list.add(ToolBoxInfo.WML_FACTORY);
413: out = (String[]) list.toArray(out);
414: } else if (path.hasExtension(Constants.TYPE_CHTML)
415: && ToolBoxInfo
416: .isClassAvailable(ToolBoxInfo.CHTML_FACTORY)) {
417: list = new ArrayList(Arrays.asList(in));
418: list.add(DOMFACTORY);
419: list.add(ToolBoxInfo.CHTML_FACTORY);
420: out = (String[]) list.toArray(out);
421: } else if (path.hasExtension(Constants.TYPE_XHTML)
422: && ToolBoxInfo
423: .isClassAvailable(ToolBoxInfo.XHTML_FACTORY)) {
424: list = new ArrayList(Arrays.asList(in));
425: list.add(DOMFACTORY);
426: list.add(ToolBoxInfo.XHTML_FACTORY);
427: out = (String[]) list.toArray(out);
428: }
429: return out;
430: }
431:
432: /**
433: * Method declaration
434: *
435: *
436: * @return
437: */
438: private String[] getAllOptionFilenames() {
439: StringBuffer buf = new StringBuffer();
440: File file = null;
441: PathHandle handle = null;
442: String filename = new String();
443: String[] filenames = new String[0];
444: StringTokenizer tokenizer = null;
445: int count = 0;
446: int index = 0;
447:
448: // Project
449: handle = PathHandle.createPathHandle(docNode.getProject()
450: .getXMLCOptionFilePath());
451: if (handle.isFile() && handle.hasExtension(Constants.TYPE_XMLC)) {
452: buf.append(handle.getPath());
453: }
454:
455: // Package
456: OtterNode parent = docNode.getParent();
457:
458: if (parent == null || parent instanceof OtterProject) {
459:
460: // skip it
461: } else {
462: handle = PathHandle.createPathHandle(parent
463: .getXMLCOptionFilePath());
464: if (handle.isFile()
465: && handle.hasExtension(Constants.TYPE_XMLC)) {
466: buf.append('#');
467: buf.append(handle.getPath());
468: }
469: }
470: buf.append('#');
471:
472: // Node
473: handle = PathHandle.createPathHandle(getXMLCOptionFilePath());
474: if (handle.isFile() && handle.hasExtension(Constants.TYPE_XMLC)) {
475: buf.append(handle.getPath());
476: }
477: tokenizer = new StringTokenizer(buf.toString().trim(), POUND);
478: count = tokenizer.countTokens();
479: filenames = new String[tokenizer.countTokens()];
480: while (tokenizer.hasMoreTokens()) {
481: filenames[index] = tokenizer.nextToken();
482: index++;
483: }
484: return filenames;
485: }
486:
487: /**
488: * Method declaration
489: *
490: *
491: * @param type
492: */
493: private void syncProjectToType(int type) {
494: int projectScope = getProject().getMapScope();
495:
496: switch (type) {
497: case CLASS_NAME_CUSTOM:
498: case CLASS_NAME_DEFAULT:
499: if (projectScope == OtterProject.MAP_SCOPE_ALL) {
500: getProject().setMapScope(
501: OtterProject.MAP_SCOPE_SELECTED);
502: }
503: break;
504: case CLASS_NAME_MAPPED:
505: if (projectScope == OtterProject.MAP_SCOPE_NONE) {
506: getProject().setMapScope(
507: OtterProject.MAP_SCOPE_SELECTED);
508: }
509: break;
510: }
511: }
512:
513: /**
514: * Save Exception information for reporting. This lets exceptions from
515: * multiple nodes to be reported in a batch.
516: *
517: * @param e
518: * An exception that occured while invoking XMLC.
519: */
520: public void setException(Throwable e) {
521: if (e == null) {
522: docNode.setException(null);
523: } else {
524: docNode.setException(new LocalException(e));
525: }
526: }
527:
528: /**
529: * Get an exception that occured when invoking XMLC. Null if the node
530: * has not been compiled or was compiled without error.
531: */
532: public Throwable getException() {
533: return docNode.getException();
534: }
535:
536: public OtterNode getParent() {
537: return docNode.getParent();
538: }
539:
540: public void save() {
541: docNode.save();
542: }
543:
544: /**
545: * Get the XMLC options selected this HTML/WML file.
546: */
547: public MetaDataHandler getMetaDataHandler() {
548: return metaDataHandler;
549: }
550:
551: /**
552: * Add the generated Java source file to the IDE project. Do not call
553: * this when using Win32 JBuilder as it can't handle the width of source
554: * lines generated by XMLC.
555: */
556: public void replaceGeneratedSource(boolean forRecomp) {
557: if ((metaDataHandler.getJavaClassSource().exists())
558: && (getProject().isOpenBuild())) {
559: int count = 1;
560:
561: if (forRecomp) {
562: count = 2;
563: }
564: String[] source = new String[count];
565: OtterNodeFactory factory = null;
566:
567: source[0] = metaDataHandler.getJavaClassSource()
568: .getAbsolutePath();
569: factory = getProject().getNodeFactory();
570: if (forRecomp) {
571: source[1] = metaDataHandler.getJavaInterfaceSource()
572: .getAbsolutePath();
573: }
574: factory.replaceGeneratedSource(getProject(), docNode,
575: source);
576: }
577: }
578:
579: /**
580: * Method declaration
581: *
582: */
583: public void initProjectOptions() {
584: metaDataHandler.setKeepGeneratedSource(true); // always keep Java source
585: metaDataHandler.setCompileSource(false); // never compile Java source
586: setXMLCOptionFilePath(docNode.getProject().getSourcePath()
587: + getResourceRelativePath() + "/resources/options.xmlc");
588: // loging options
589: metaDataHandler.setPrintAccessorInfo(getProject()
590: .isPrintAccessorInfo());
591: metaDataHandler.setPrintDocumentInfo(getProject()
592: .isPrintDocumentInfo());
593: metaDataHandler.setPrintDOM(getProject().isPrintDOM());
594: metaDataHandler.setPrintParseInfo(getProject()
595: .isPrintParseInfo());
596: metaDataHandler.setVerbose(getProject().isVerbose());
597:
598: // Call XMLC with the do not call javac option.
599: }
600:
601: public String getGenerateToRoot() {
602: String projectGenTo = getProject().getGenerateToDirectory();
603: StringBuffer buf = new StringBuffer();
604:
605: if (projectGenTo == null) {
606: buf.append(docNode.getProject().getSourcePathOf(docNode));
607: } else {
608: buf.append(projectGenTo);
609: }
610: return buf.toString();
611: }
612:
613: /**
614: * Initialize the XMLC options.
615: *
616: * @throws XMLCException
617: * Thrown when options are invalid.
618: *
619: */
620: private void initMetaDataHandler() {
621: metaDataHandler = XMLCConnectionFactory.createMetaDataHandler();
622: initClassName();
623: initProjectOptions();
624: metaDataHandler.setInputDocument(getFilePath());
625: initJavaSourceFile();
626: }
627:
628: /**
629: * Method declaration
630: *
631: *
632: * @exception IOException
633: * @exception XMLCException
634: */
635: private void parseOptions() throws XMLCException, IOException {
636: metaDataHandler.parse(getAllOptionFilenames(),
637: getAllParameters(false), (PrintWriter) null, this );
638: }
639:
640: /**
641: * Method declaration
642: *
643: */
644: private void initJavaSourceFile() {
645: StringBuffer filename = new StringBuffer();
646: String className = new String();
647: String packageName = new String();
648: File source = null;
649: int index = -1;
650:
651: filename.append(getGenToDirectory().toString());
652: filename.append(File.separator);
653: className = metaDataHandler.getClassName();
654: packageName = metaDataHandler.getPackageName();
655: if ((packageName == null) || (packageName.trim().length() == 0)) {
656: index = -1;
657: } else {
658: index = className.indexOf(packageName);
659: }
660: if (index > -1) {
661: className = className.substring(metaDataHandler
662: .getPackageName().length() + 1);
663: }
664: index = className.lastIndexOf('.');
665: if (index > -1) {
666: className = className.substring(index + 1);
667: }
668: filename.append(className);
669: filename.append('.');
670: filename.append(Constants.TYPE_JAVA);
671: source = new File(filename.toString());
672: metaDataHandler.setJavaClassSource(source, this );
673: }
674:
675: /**
676: * Get the generate to directory and create the directory if needed.
677: * This is where the Java source files are created.
678: */
679: private File getGenToDirectory() {
680: String genTo = getGenToPath();
681: File genDir = new File(genTo);
682:
683: if (genDir.exists()) {
684: if (!genDir.isDirectory()) {
685: System.err.println(res.getString("Unable_to_write")
686: + genTo);
687: }
688: } else {
689: genDir.mkdirs();
690: }
691: return genDir;
692: }
693:
694: /**
695: * Get the generate to directory as a string. This is where XMLC will create
696: * Java source files.
697: */
698: private String getGenToPath() {
699: String packagePath = getPackagePath();
700: StringBuffer buf = new StringBuffer();
701:
702: buf.append(getGenerateToRoot());
703: if (packagePath.length() > 0) {
704: buf.append(File.separator);
705: buf.append(packagePath);
706: }
707: return buf.toString();
708: }
709:
710: /**
711: * Method declaration
712: *
713: *
714: * @return
715: */
716: private String getPackagePath() {
717: String packPath = new String();
718:
719: if (metaDataHandler.getPackageName() != null) {
720: packPath = metaDataHandler.getPackageName();
721: } else if (metaDataHandler.getClassName() != null) {
722: int index = metaDataHandler.getClassName().lastIndexOf('.');
723:
724: if (index > -1) {
725: packPath = metaDataHandler.getClassName().substring(0,
726: index);
727: }
728: }
729: packPath = packPath.replace('.', File.separatorChar);
730: return packPath;
731: }
732:
733: /**
734: * Create a class name for the given HTML/WML file and map the
735: * package name as needed. Mapping allows users to specify
736: * a package name to use for all the HTML/WML files in a directory.
737: */
738: private void initClassName() {
739: Mapper mapper = new Mapper(getProject());
740: String className = mapper.getClassName(this );
741:
742: metaDataHandler.setClassName(className);
743: }
744:
745: private String getResourceRelativePath() {
746: StringBuffer buf = new StringBuffer();
747: String path = new String();
748:
749: buf.append(File.separator);
750: path = metaDataHandler.getPackageName();
751: int index = path.lastIndexOf("presentation");
752: if (index != -1) {
753: path = path.substring(0, index - 1);
754: }
755: path = path.replace('.', File.separatorChar);
756: buf.append(path);
757: return buf.toString();
758: }
759:
760: private String baseOut() {
761: StringBuffer buf = new StringBuffer();
762: String path = new String();
763:
764: buf.append(getProject().getClassOutputPath());
765: buf.append(File.separator);
766: path = metaDataHandler.getClassName();
767: path = path.replace('.', File.separatorChar);
768: buf.append(path);
769: return buf.toString();
770: }
771:
772: /**
773: * Get the class file that we expect after XMLC has generated
774: * a java file and called javac to compile it.
775: */
776: public File getClassFile() {
777: File file = null;
778: StringBuffer buf = new StringBuffer();
779:
780: buf.append(baseOut());
781: buf.append('.');
782: buf.append(Constants.TYPE_CLASS);
783: file = new File(buf.toString());
784: return file;
785: }
786:
787: public File getOptionFileForRecomp() {
788: File file = null;
789: StringBuffer buf = new StringBuffer();
790:
791: buf.append(baseOut());
792: buf.append('.');
793: buf.append(Constants.TYPE_XMLC);
794: file = new File(buf.toString());
795: return file;
796: }
797:
798: /**
799: * True if we can find the Java source file that we expect XMLC to create.
800: * This is used to in reporting compilation results.
801: */
802: public boolean isNewJavaFound() {
803: boolean newFound = false;
804:
805: if (metaDataHandler.getKeepGeneratedSource()) {
806: newFound = metaDataHandler.getJavaClassSource().exists();
807: }
808: return newFound;
809: }
810:
811: /**
812: * Method declaration
813: *
814: *
815: * @exception IOException
816: * @exception XMLCException
817: */
818: public void preCompile() throws XMLCException, IOException {
819: parseOptions();
820:
821: // clear out any existing impl or standard classes.
822: File implSource = null;
823: StringBuffer implName = new StringBuffer();
824: int index = -1;
825:
826: implName.append(metaDataHandler.getJavaClassSource());
827: index = implName.toString().lastIndexOf('.');
828: implName.insert(index, IMPL);
829: implSource = new File(implName.toString());
830: if (implSource.exists()) {
831: implSource.delete();
832: }
833: if (metaDataHandler.getJavaClassSource().exists()) {
834: metaDataHandler.getJavaClassSource().delete();
835: }
836:
837: // reset names if needed
838: if (metaDataHandler.getRecompilation()) {
839: metaDataHandler.setJavaInterfaceSource(metaDataHandler
840: .getJavaClassSource(), this );
841: metaDataHandler.setJavaClassSource(implSource, this );
842: }
843: docNode.preCompile();
844: }
845:
846: /**
847: * Class declaration
848: *
849: * @author Paul Mahar
850: */
851: class LocalException extends Exception {
852: private Throwable baseException = null;
853:
854: /**
855: * Constructor declaration
856: *
857: *
858: * @param e
859: */
860: public LocalException(Throwable e) {
861: baseException = e;
862: }
863:
864: /**
865: * Method declaration
866: *
867: *
868: * @return
869: */
870: public String getMessage() {
871: StringBuffer buf = new StringBuffer();
872: String[] parameters = getAllParameters(true);
873: String[] filenames = getAllOptionFilenames();
874:
875: buf.append(baseException.getMessage());
876: buf.append('\n');
877: if (parameters.length >= 1) {
878: buf.append(Constants.TAB2);
879: buf.append(res.getString("Command_line"));
880: buf.append('\n');
881: for (int i = 0; i < parameters.length; i++) {
882: buf.append(Constants.TAB4);
883: buf.append(parameters[i]);
884: buf.append('\n');
885: }
886: }
887: if (filenames.length >= 1) {
888: buf.append(Constants.TAB2);
889: if (filenames.length >= 2) {
890: buf.append(res.getString("Option_files"));
891: } else {
892: buf.append(res.getString("Option_file"));
893: }
894: buf.append('\n');
895: for (int i = 0; i < filenames.length; i++) {
896: buf.append(Constants.TAB4);
897: buf.append(filenames[i]);
898: }
899: }
900: return buf.toString().trim();
901: }
902:
903: }
904: }
|