001: /*
002: * BEGIN_HEADER - DO NOT EDIT
003: *
004: * The contents of this file are subject to the terms
005: * of the Common Development and Distribution License
006: * (the "License"). You may not use this file except
007: * in compliance with the License.
008: *
009: * You can obtain a copy of the license at
010: * https://open-esb.dev.java.net/public/CDDLv1.0.html.
011: * See the License for the specific language governing
012: * permissions and limitations under the License.
013: *
014: * When distributing Covered Code, include this CDDL
015: * HEADER in each file and include the License file at
016: * https://open-esb.dev.java.net/public/CDDLv1.0.html.
017: * If applicable add the following below this CDDL HEADER,
018: * with the fields enclosed by brackets "[]" replaced with
019: * your own identifying information: Portions Copyright
020: * [year] [name of copyright owner]
021: */
022:
023: /*
024: * @(#)Repository.java
025: * Copyright 2004-2007 Sun Microsystems, Inc. All Rights Reserved.
026: *
027: * END_HEADER - DO NOT EDIT
028: */
029: package com.sun.jbi.management.repository;
030:
031: import com.sun.jbi.StringTranslator;
032: import com.sun.jbi.management.LocalStringKeys;
033: import com.sun.jbi.management.system.ManagementContext;
034: import com.sun.jbi.management.internal.support.DirectoryUtil;
035: import com.sun.jbi.management.internal.support.JarFactory;
036:
037: import java.io.File;
038: import java.io.FileInputStream;
039: import java.io.FileOutputStream;
040: import java.io.InputStream;
041: import java.io.OutputStream;
042: import java.util.Calendar;
043: import java.util.Iterator;
044: import java.util.HashMap;
045: import java.util.List;
046: import java.util.regex.Pattern;
047: import java.util.zip.ZipEntry;
048: import java.util.zip.ZipFile;
049: import java.util.logging.Logger;
050:
051: /**
052: * A repository for ESB artifacts. The ESB Repository stores bindings, engines,
053: * shared libraries, service assemblies, service units, and temporary uploads
054: * from remote administration clients.
055: *
056: * The ArchiveType enumeration is used to declare the type used by an archive
057: * operation. In addition to the archive type, a name is given to identify
058: * an archive uniquely. The combination of name and type prevents naming
059: * conflicts between different classes of artifacts (e.g. bindings and shared
060: * libraries). When attempting to reference a service unit, the following
061: * naming rule is used: <br><br>
062: *
063: * <code>[sa-name]'/'[su-name]<code>
064: */
065: public class Repository {
066: /** Repository directory names. */
067: private static final String COMPONENT_STORE_NAME = "components";
068: private static final String SHARED_LIBRARY_STORE_NAME = "shared-libraries";
069: private static final String SERVICE_ASSEMBLY_STORE_NAME = "service-assemblies";
070: private static final String TEMP_STORE_NAME = "tmp"
071: + File.separator + "upload";
072:
073: /** Install Root */
074: private static final String INSTALL_ROOT = "install_root";
075:
076: /** AS Applications dir name */
077: private static final String APPLICATIONS_DIR = "applications";
078:
079: /** Name used to locate string translation file. */
080: private static final String STRING_TRANSLATOR_NAME = "com.sun.jbi.management";
081:
082: /** R/W buffer size for copying archive files. */
083: private static final int BUFFER_SIZE = 8 * 1024;
084:
085: /** Translate messages into exciting languages. */
086: private StringTranslator mStrings;
087:
088: private ManagementContext mContext;
089: private Logger mLog;
090:
091: /** The Directory suffix pattern. This is the suffix of a directory name
092: * when a directory by the actual name exists. It is ".(n)+"
093: * example SunFileBinding.12
094: */
095: protected static final String DIR_SUFFIX_PATTERN = "\\x2E\\d+";
096:
097: /** File references to repository stores. */
098: private File mComponentStore;
099: private File mSharedLibraryStore;
100: private File mServiceAssemblyStore;
101: private File mTempStore;
102: private long mStartupTime;
103:
104: /** Cache containing archives which have been added or retrieved. Archives
105: * removed from the repository must be removed from the cache. Since the
106: * individual operations which manipulate the cache are thread-safe, there
107: * is no need to independently synchronize access to the cache. The cache
108: * is keyed by archive's absolute path.
109: */
110: private HashMap<String, Archive> mArchiveCache;
111:
112: /** Create a new instance of the repository.
113: */
114: public Repository(ManagementContext context)
115: throws RepositoryException {
116: mContext = context;
117: mStrings = context.getEnvironmentContext().getStringTranslator(
118: STRING_TRANSLATOR_NAME);
119: mLog = context.getLogger();
120: mArchiveCache = new HashMap<String, Archive>();
121: mStartupTime = System.currentTimeMillis();
122: initStores();
123: }
124:
125: public void cleanRepository() {
126: // clean out the temp store
127: cleanDirectory(mTempStore, false);
128:
129: // Clean up archive directories which are marked for deletion
130: removeDeletedArchives();
131: }
132:
133: /** Add an archive to the repository. */
134: public Archive addArchive(ArchiveType type, String path)
135: throws RepositoryException {
136: Archive archive;
137:
138: try {
139: archive = new Archive(new File(path), true);
140: } catch (java.io.IOException ioEx) {
141: throw new RepositoryException(ioEx);
142: }
143:
144: // verify archive type
145: if (!archive.getType().equals(type)) {
146: throw new RepositoryException(mStrings.getString(
147: LocalStringKeys.JBI_ADMIN_ARCHIVE_TYPE_MISMATCH,
148: type.toString(), archive.getType().toString()));
149: }
150:
151: addArchive(archive);
152: return archive;
153: }
154:
155: /** Add a pre-parsed archive to the repository.
156: */
157: public synchronized void addArchive(Archive archive)
158: throws RepositoryException {
159: // check for duplicates
160: if (archiveExists(archive.getType(), archive.getJbiName())) {
161: throw new RepositoryException(mStrings
162: .getString(
163: LocalStringKeys.JBI_ADMIN_ARCHIVE_EXISTS,
164: archive.getType().toString(), archive
165: .getJbiName()));
166: }
167:
168: // add the archive to the repository
169: addToRepository(archive);
170: mArchiveCache.put(archive.getPath(), archive);
171: }
172:
173: /** Used to determine if a particular archive is stored in the repository. */
174: public boolean archiveExists(ArchiveType type, String name) {
175: return getArchiveDirectory(type, name) != null;
176: }
177:
178: /** Retrieve archive details for the specified id. */
179: public synchronized Archive getArchive(Object archiveId) {
180: File file;
181: Archive archive = null;
182: Calendar uploadTime;
183:
184: try {
185: file = new File((String) archiveId);
186:
187: if (file.exists()) {
188: // check to see if we have a copy in our cache first
189: if (mArchiveCache.containsKey(archiveId)) {
190: return mArchiveCache.get(archiveId);
191: }
192:
193: archive = new Archive(file, false);
194: uploadTime = Calendar.getInstance();
195: uploadTime.setTimeInMillis(file.lastModified());
196: archive.setUploadTimestamp(uploadTime);
197:
198: if (archive.getType().equals(ArchiveType.SERVICE_UNIT)) {
199: // jbi.xml does not provide SU name, so cheat with the dir name
200: archive.setJbiName(file.getParentFile().getName());
201: }
202:
203: // Update the cache
204: mArchiveCache.put(archive.getPath(), archive);
205: }
206: } catch (Exception ex) {
207: mLog.info(ex.getMessage());
208: }
209:
210: return archive;
211: }
212:
213: /** Retrieve archive details for the specified id. */
214: public synchronized Archive getArchive(ArchiveType type, String name) {
215: Archive archive = null;
216: String path;
217:
218: path = findArchive(type, name);
219: if (path != null) {
220: archive = getArchive(path);
221: }
222:
223: return archive;
224: }
225:
226: /** Returns the path to the directory where the archive is stored, or null
227: * if archive does not exist in the repository.
228: */
229: public String findArchiveDirectory(ArchiveType type, String name) {
230: String dirPath = null;
231: File archiveDir;
232:
233: archiveDir = getArchiveDirectory(type, name);
234: if (archiveDir != null && archiveDir.exists()) {
235: dirPath = archiveDir.getAbsolutePath();
236: }
237:
238: return dirPath;
239: }
240:
241: /** Returns the path to the specified archive, or null if it does not exist
242: * in the repository.
243: */
244: public synchronized String findArchive(ArchiveType type, String name) {
245: String path = null;
246: File archiveDir;
247:
248: archiveDir = getArchiveDirectory(type, name);
249:
250: if (archiveDir != null && archiveDir.exists()) {
251: // there should only be one file in the archive directory
252: File[] files = archiveDir.listFiles();
253: if (files.length >= 1) {
254: /**
255: * If more than one file, find the non-dir file, which should
256: * be the archive and return the path to that. This is the case
257: * when the the archive we are looking for is a Service Assembly.
258: */
259: int i;
260: for (i = 0; i < files.length && files[i].isDirectory(); i++)
261: ;
262: if (i != files.length) {
263: path = files[i].getAbsolutePath();
264: }
265: }
266: }
267:
268: return path;
269: }
270:
271: /** Remove the specified archive.
272: * @throws RepositoryException archive not found in repository
273: */
274: public synchronized void removeArchive(ArchiveType type, String name)
275: throws RepositoryException {
276: String path;
277: File file;
278: File dir = null;
279:
280: path = findArchive(type, name);
281:
282: if (path == null) {
283: // The archive does not exist, just clean the parent dir
284: mLog.warning(mStrings.getString(
285: LocalStringKeys.JBI_ADMIN_ARCHIVE_NOT_EXIST, type
286: .toString(), name));
287: String dirPath = findArchiveDirectory(type, name);
288:
289: if (dirPath != null) {
290: dir = new File(dirPath);
291: }
292: } else {
293: // remove the archive zip and parent directory
294: file = new File(path);
295: dir = file.getParentFile();
296: }
297:
298: if (dir != null) {
299: if (!DirectoryUtil.removeDir(dir.getAbsolutePath())) {
300: // Failed to completely remove the archive directory from the
301: // repository, but it is marked for deletion on restart by
302: // DirectoryUtil. At this point, we just need to log the failure.
303: mLog.fine(mStrings.getString(
304: LocalStringKeys.JBI_ADMIN_FILE_DELETE_FAILED,
305: dir.getAbsolutePath()));
306: }
307: }
308:
309: if (path != null) {
310: mArchiveCache.remove(path);
311: }
312: }
313:
314: /** Returns a file pointer to the repository temp store. */
315: public File getTempStore() {
316: return mTempStore;
317: }
318:
319: /** Purges the repository, removing all archives. */
320: public synchronized void purge() {
321: mArchiveCache.clear();
322: cleanDirectory(mComponentStore, true);
323: cleanDirectory(mSharedLibraryStore, true);
324: cleanDirectory(mServiceAssemblyStore, true);
325: cleanDirectory(mTempStore, true);
326: }
327:
328: /**
329: *
330: * @return an list of names of entities of type ArchiveType, which are
331: * stored in the repository and are not marked for deletion.
332: */
333: public List<String> getArchiveEntityNames(ArchiveType type) {
334: File dir = getStore(type);
335:
336: File[] subDirs = dir.listFiles();
337: List<String> entityNames = new java.util.ArrayList();
338: for (File subDir : subDirs) {
339: if (subDir.isDirectory()) {
340: // -- Check if it is marked for deletion
341: if (!DirectoryUtil.isMarked(subDir)) {
342: entityNames.add(stripSuffix(subDir.getName()));
343: }
344: }
345: }
346:
347: return entityNames;
348: }
349:
350: /**
351: * Clean up service assembly, shared library, and component directories
352: * which could not be deleted the last time JBI was up. This problem
353: * is pretty much a Windows-only thing that comes up when an open file
354: * stream has not been closed by a component or a classloader.
355: */
356: private void removeDeletedArchives() {
357: DirectoryUtil.removeMarkedDirs(mComponentStore
358: .getAbsolutePath());
359: DirectoryUtil.removeMarkedDirs(mSharedLibraryStore
360: .getAbsolutePath());
361: DirectoryUtil.removeMarkedDirs(mServiceAssemblyStore
362: .getAbsolutePath());
363: }
364:
365: /** Initialize archive store directories. The temp directory is cleaned
366: * every time the repository is initialized.
367: */
368: private void initStores() throws RepositoryException {
369: String jbiInstallRoot = mContext.getEnvironmentContext()
370: .getJbiInstanceRoot();
371: String jbiHome = mContext.getEnvironmentContext()
372: .getJbiInstallRoot();
373:
374: try {
375: mComponentStore = loadStore(jbiInstallRoot,
376: COMPONENT_STORE_NAME);
377: mSharedLibraryStore = loadStore(jbiInstallRoot,
378: SHARED_LIBRARY_STORE_NAME);
379: mTempStore = loadStore(jbiInstallRoot, TEMP_STORE_NAME);
380: mServiceAssemblyStore = loadStore(jbiInstallRoot,
381: SERVICE_ASSEMBLY_STORE_NAME);
382:
383: } catch (java.io.IOException ioEx) {
384: throw new RepositoryException(ioEx);
385: }
386: }
387:
388: /** Handles the transfer of archive bits into the repository. */
389: private void addToRepository(Archive archive)
390: throws RepositoryException {
391: File archiveDir;
392: File archiveZip;
393: File archiveFile;
394: Calendar calendar;
395: long timestamp;
396:
397: // create the archive directory and file
398: archiveDir = createArchiveDirectory(archive.getType(), archive
399: .getJbiName());
400: archiveZip = new File(archiveDir, archive.getFileName());
401: archiveFile = new File(archive.getPath());
402:
403: try {
404: // copy the file content
405: transfer(new FileInputStream(archiveFile),
406: new FileOutputStream(archiveZip));
407:
408: // Copy the modified timestamp.
409: archiveZip.setLastModified(timestamp = archiveFile
410: .lastModified());
411:
412: // set the new path for the archive
413: archive.setPath(archiveZip.getAbsolutePath());
414:
415: // perform service assembly post-processing, if necessary
416: if (archive.getType().equals(ArchiveType.SERVICE_ASSEMBLY)) {
417: extractServiceUnits(archive);
418: }
419: } catch (java.io.IOException ioEx) {
420: throw new RepositoryException(ioEx);
421: }
422:
423: // we're done, set the upload timestamp
424: calendar = Calendar.getInstance();
425: calendar.setTimeInMillis(timestamp);
426: archive.setUploadTimestamp(calendar);
427:
428: // extract the component / shared library archive in the archiveDir
429: if (!archive.getType().equals(ArchiveType.SERVICE_ASSEMBLY)) {
430: File archiveInstallRoot = new File(archiveDir, INSTALL_ROOT);
431:
432: extractArchive(archiveInstallRoot, archiveZip);
433: }
434: }
435:
436: /**
437: * Extract the archive contents.
438: *
439: * @param parentDir - the parent directory to extract tp
440: * @param archiveZip - the archive to extract
441: */
442: private void extractArchive(File parentDir, File archiveZip)
443: throws RepositoryException {
444: parentDir.mkdir();
445:
446: try {
447: JarFactory jarHelper = new JarFactory(parentDir
448: .getAbsolutePath());
449: jarHelper.unJar(archiveZip);
450: } catch (Exception ex) {
451: throw new RepositoryException(ex);
452: }
453: }
454:
455: /**
456: * Removes all files and child directories in the specified directory.
457: * @return false if unable to delete the file
458: */
459: private boolean cleanDirectory(File dir, boolean all) {
460: File[] tmps = dir.listFiles();
461: for (int i = 0; i < tmps.length; i++) {
462: if (tmps[i].isDirectory()) {
463: // -- Even if a single file / dir in a parent folder cannot be deleted
464: // -- break the recursion as there is no point in continuing the loop
465: if (!cleanDirectory(tmps[i], all)) {
466: return false;
467: }
468: }
469: if (all || tmps[i].lastModified() < mStartupTime) {
470: if (!tmps[i].delete()) {
471: mLog
472: .warning(mStrings
473: .getString(
474: LocalStringKeys.JBI_ADMIN_FILE_DELETE_FAILED,
475: tmps[i].getAbsolutePath()));
476: return false;
477: }
478: }
479: }
480:
481: return true;
482: }
483:
484: /**
485: * Reclaim memory, run object finalizers
486: */
487: private void finalizeDiscardedObjects() {
488: System.gc();
489: System.runFinalization();
490: }
491:
492: /** Loads an archive store, creating it if necessary. */
493: private File loadStore(String root, String name)
494: throws java.io.IOException {
495: File dir;
496:
497: dir = new File(root + File.separator + name);
498: if (!dir.exists()) {
499: dir.mkdirs();
500: }
501:
502: return dir;
503: }
504:
505: /** Retrieve an archive store based on the archive type.
506: */
507: private File getStore(ArchiveType type) {
508: File store = null;
509:
510: if (type.equals(ArchiveType.COMPONENT)) {
511: store = mComponentStore;
512: } else if (type.equals(ArchiveType.SHARED_LIBRARY)) {
513: store = mSharedLibraryStore;
514: } else if (type.equals(ArchiveType.SERVICE_ASSEMBLY)) {
515: store = mServiceAssemblyStore;
516: } else if (type.equals(ArchiveType.SERVICE_UNIT)) {
517: /* This may look weird, but service units are stored in
518: * sub-directories under the service assembly. The jbiName
519: * for an SU archive contains the relative path to the sub-directory
520: */
521: store = mServiceAssemblyStore;
522: }
523: return store;
524: }
525:
526: /** Utility method that writes all data read from the input stream to
527: * the output stream. All streams are closed at the end of this method.
528: */
529: private void transfer(InputStream input, OutputStream output)
530: throws java.io.IOException {
531: byte[] buf = new byte[BUFFER_SIZE];
532:
533: for (int count = 0; count > -1; count = input.read(buf)) {
534: output.write(buf, 0, count);
535: }
536:
537: output.flush();
538: input.close();
539: output.close();
540: }
541:
542: private void extractServiceUnits(Archive archive)
543: throws java.io.IOException, RepositoryException {
544: ZipFile zip;
545: Iterator suList;
546: ZipEntry entry;
547: File suDir;
548: File suFile;
549: String suName;
550: File suComponentDir;
551: String targetComponent = null;
552:
553: zip = new ZipFile(archive.getPath());
554: suList = archive.listChildren();
555:
556: // extract each SU into a sub-directory of the parent service assembly
557: while (suList.hasNext()) {
558: suName = (String) suList.next();
559: entry = zip.getEntry(archive.getChildPath(suName));
560: suDir = new File(
561: getArchiveDirectory(ArchiveType.SERVICE_ASSEMBLY,
562: archive.getJbiName()), suName);
563:
564: // create the su directory and file (using original file name)
565: suDir.mkdir();
566: suFile = new File(suDir, archive.getChildPath(suName));
567: transfer(zip.getInputStream(entry), new FileOutputStream(
568: suFile));
569:
570: // Now extract the SU archive to
571: // ${SA_REPOS_ROOT}/${SA_NAME}/${SU_NAME}/${COMPONENT_NAME}
572: java.util.List<com.sun.jbi.management.descriptor.ServiceUnit> sus = archive
573: .getJbiXml(false).getServiceAssembly()
574: .getServiceUnit();
575:
576: for (com.sun.jbi.management.descriptor.ServiceUnit su : sus) {
577: if (su.getIdentification().getName().equals(suName)) {
578: targetComponent = su.getTarget().getComponentName();
579: }
580: }
581:
582: suComponentDir = new File(suDir, targetComponent);
583: extractArchive(suComponentDir, suFile);
584: }
585:
586: zip.close();
587: }
588:
589: /** Creates a directory in the appropriate repository store for the
590: * specified archive. Code calling this method must ensure that the
591: * archive is not currently active in the repository. In this case,
592: * active means that an archive directory with this name does not exist,
593: * or if it does, it is marked for deletion. In the latter case, the
594: * archive name is made unique by using the original name as a prefix and
595: * adding a ".nn", where nn is a positive integer.
596: */
597: private File createArchiveDirectory(ArchiveType type, String name) {
598: File archiveDir;
599:
600: archiveDir = new File(getStore(type), name);
601:
602: if (archiveDir.exists()) {
603: // This should only happen if an attempt to remove the archive previously
604: // failed and the archive directory was marked for deletion.
605: for (int i = 1; archiveDir.exists(); i++) {
606: archiveDir = new File(getStore(type), name + "." + i);
607: }
608: }
609:
610: archiveDir.mkdir();
611: return archiveDir;
612: }
613:
614: /** Returns a reference to the directory where an archive is stored, or null
615: * if archive does not exist in the repository. This method will
616: * ignore archive directories which have been marked for deletion. All
617: * repository methods that search for an archive should use this method
618: * instead of directly creating a File reference to the archive directory
619: * using the component/SL/SA name.
620: */
621: private File getArchiveDirectory(ArchiveType type, String name) {
622: File archiveDir = null;
623: String dirName = null;
624:
625: // Finding the archive is a two-stage process:
626: // Step 1: See if the standard pattern match, archive directory =
627: // archive name, results in a hit. If that directory exists
628: // and is not marked for deletion, we're done.
629: // Step 2: This step is potentially expensive. We need to scan
630: // through the existing archive directories of the specified
631: // type to determine if the archive exists with an offset.
632: // We run into this situation if an archive is added to the
633: // repository after a previous removal failed to complete (but
634: // the archive was marked for deletion).
635:
636: // If we are getting a Service Unit archive then the directory is the
637: // Service Assembly Name.
638: if (type.equals(ArchiveType.SERVICE_UNIT)) {
639: name = name.replace('\\', '/');
640: dirName = extractServiceAssemblyName(name);
641: } else {
642: dirName = name;
643: }
644:
645: File tmpDir = new File(getStore(type), dirName);
646: if (tmpDir.exists() && !DirectoryUtil.isMarked(tmpDir)) {
647: archiveDir = tmpDir;
648: } else {
649: // Find all files which match the name pattern and then look
650: // for one that is not deleted.
651: // The name pattern is : name{[0-9]*}
652: Pattern namePattern = Pattern.compile(dirName
653: + DIR_SUFFIX_PATTERN);
654: File[] dirs = DirectoryUtil.listFiles(getStore(type),
655: namePattern);
656: if (dirs != null) {
657: for (File file : dirs) {
658: if (file.isDirectory()
659: && !DirectoryUtil.isMarked(file)) {
660: archiveDir = file;
661: break;
662: }
663: }
664: }
665: }
666:
667: if (type.equals(ArchiveType.SERVICE_UNIT)) {
668: // If it is a service unit then the su folder
669: // is relative to the service assembly folder we found
670: String suName = extractServiceUnitName(name);
671: archiveDir = new File(archiveDir, suName);
672: }
673:
674: return archiveDir;
675: }
676:
677: /**
678: * @param relativeName - relative name of a service unit
679: *
680: * @return the service unit name from the relative name. Ex. if the
681: * relative name is ServiceAssembly/ServiceUnit, this will return ServiceUnit.
682: * If the relativeName is not of the pattern <sa-name>/<su-name>, the
683: * original string is returned.
684: */
685: private String extractServiceUnitName(String relativeName) {
686: String suName = relativeName;
687: int fSlash = relativeName.indexOf('/');
688: if (fSlash != -1) {
689: suName = relativeName.substring(fSlash + 1, relativeName
690: .length());
691: }
692: return suName;
693: }
694:
695: /**
696: * @param relativeName - relative name of a service unit
697: *
698: * @return the service assembly name from the relative name. Ex. if the
699: * relative name is ServiceAssembly/ServiceUnit, this will return
700: * ServiceAssembly.
701: *
702: * If the relativeName is not of the pattern <sa-name>/<su-name>, the
703: * original string is returned.
704: */
705: private String extractServiceAssemblyName(String relativeName) {
706: String saName = relativeName;
707: int fSlash = relativeName.indexOf('/');
708: if (fSlash != -1) {
709: saName = relativeName.substring(0, fSlash);
710: }
711: return saName;
712: }
713:
714: /**
715: * If the name string passed in contains a suffix matching the DIR_SUFFIX_PATTERN
716: * the suffix is removed and result is returned.
717: *
718: * @param dirName - possibly suffixed directory name
719: * @return the name with the suffix ( if any ) removed.
720: */
721: private String stripSuffix(String dirName) {
722: int dotIndex = dirName.lastIndexOf('.');
723: if (dotIndex != -1) {
724: String name = dirName.substring(0, dotIndex);
725: String suffix = dirName.substring(dotIndex, dirName
726: .length());
727:
728: if (isSuffix(suffix)) {
729: return name;
730: }
731: }
732: return dirName;
733: }
734:
735: /**
736: * @return true if the suffix matches the DIR_SUFFIX_PATTERN
737: */
738: private boolean isSuffix(String suffix) {
739: Pattern suffixPattern = Pattern.compile(DIR_SUFFIX_PATTERN);
740: return suffixPattern.matcher(suffix).matches();
741: }
742:
743: }
|