001: /*
002: * ====================================================================
003: * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved.
004: *
005: * This software is licensed as described in the file COPYING, which
006: * you should have received as part of this distribution. The terms
007: * are also available at http://svnkit.com/license.html
008: * If newer versions of this license are posted there, you may use a
009: * newer version instead, at your option.
010: * ====================================================================
011: */
012:
013: package org.tmatesoft.svn.core.io;
014:
015: import java.io.BufferedInputStream;
016: import java.io.File;
017: import java.io.IOException;
018: import java.io.InputStream;
019: import java.io.OutputStream;
020: import java.util.Date;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.Map;
024: import java.util.jar.JarEntry;
025: import java.util.jar.JarFile;
026: import java.util.jar.JarInputStream;
027: import java.util.regex.Pattern;
028:
029: import org.tmatesoft.svn.core.SVNErrorCode;
030: import org.tmatesoft.svn.core.SVNErrorMessage;
031: import org.tmatesoft.svn.core.SVNException;
032: import org.tmatesoft.svn.core.SVNRevisionProperty;
033: import org.tmatesoft.svn.core.SVNURL;
034: import org.tmatesoft.svn.core.internal.util.SVNTimeUtil;
035: import org.tmatesoft.svn.core.internal.util.SVNUUIDGenerator;
036: import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
037: import org.tmatesoft.svn.core.internal.wc.SVNFileListUtil;
038: import org.tmatesoft.svn.core.internal.wc.SVNFileType;
039: import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
040: import org.tmatesoft.svn.core.internal.wc.SVNProperties;
041: import org.tmatesoft.svn.core.internal.wc.admin.SVNTranslator;
042:
043: /**
044: * <b>SVNRepositoryFactory</b> is an abstract factory that is responsible
045: * for creating an appropriate <b>SVNRepository</b> driver specific for the
046: * protocol to use.
047: *
048: * <p>
049: * Depending on what protocol a user exactly would like to use
050: * to access the repository he should first of all set up an
051: * appropriate extension of this factory. So, if the user is going to
052: * work with the repository via the custom <i>svn</i>-protocol (or
053: * <i>svn+xxx</i>) he initially calls:
054: * <pre class="javacode">
055: * ...
056: * <span class="javakeyword">import</span> org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
057: * ...
058: * <span class="javacomment">//do it once in your application prior to using the library</span>
059: * <span class="javacomment">//enables working with a repository via the svn-protocol (over svn and svn+ssh)</span>
060: * SVNRepositoryFactoryImpl.setup();
061: * ...</pre><br />
062: * From this point the <b>SVNRepositoryFactory</b> knows how to create
063: * <b>SVNRepository</b> instances specific for the <i>svn</i>-protocol.
064: * And further on the user can create an <b>SVNRepository</b> instance:
065: * <pre class="javacode">
066: * ...
067: * <span class="javacomment">//creating a new SVNRepository instance</span>
068: * String url = <span class="javastring">"svn://host/path"</span>;
069: * SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url));
070: * ...</pre><br />
071: *
072: * <table cellpadding="3" cellspacing="1" border="0" width="70%" bgcolor="#999933">
073: * <tr bgcolor="#ADB8D9" align="left">
074: * <td><b>Supported Protocols</b></td>
075: * <td><b>Factory to setup</b></td>
076: * </tr>
077: * <tr bgcolor="#EAEAEA" align="left">
078: * <td>svn://, svn+xxx://</td><td>SVNRepositoryFactoryImpl (<b>org.tmatesoft.svn.core.internal.io.svn</b>)</td>
079: * </tr>
080: * <tr bgcolor="#EAEAEA" align="left">
081: * <td>http://, https://</td><td>DAVRepositoryFactory (<b>org.tmatesoft.svn.core.internal.io.dav</b>)</td>
082: * </tr>
083: * <tr bgcolor="#EAEAEA" align="left">
084: * <td>file:/// (FSFS only)</td><td>FSRepositoryFactory (<b>org.tmatesoft.svn.core.internal.io.fs</b>)</td>
085: * </tr>
086: * </table>
087: *
088: * <p>
089: * Also <b>SVNRepositoryFactory</b> may be used to create local
090: * FSFS-type repositories.
091: *
092: * @version 1.1.1
093: * @author TMate Software Ltd.
094: * @see SVNRepository
095: * @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a>
096: */
097: public abstract class SVNRepositoryFactory {
098:
099: private static final Map myFactoriesMap = new HashMap();
100: private static final String REPOSITORY_TEMPLATE_PATH = "/org/tmatesoft/svn/core/io/repository/template.jar";
101:
102: protected static void registerRepositoryFactory(String protocol,
103: SVNRepositoryFactory factory) {
104: if (protocol != null && factory != null) {
105: myFactoriesMap.put(protocol, factory);
106: }
107: }
108:
109: protected static boolean hasRepositoryFactory(String protocol) {
110: if (protocol != null) {
111: return myFactoriesMap.get(protocol) != null;
112: }
113: return false;
114: }
115:
116: /**
117: * Creates an <b>SVNRepository</b> driver according to the protocol that is to be
118: * used to access a repository.
119: *
120: * <p>
121: * The protocol is defined as the beginning part of the URL schema. Currently
122: * SVNKit supports only <i>svn://</i> (<i>svn+ssh://</i>) and <i>http://</i> (<i>https://</i>)
123: * schemas.
124: *
125: * <p>
126: * The created <b>SVNRepository</b> driver can later be <i>"reused"</i> for another
127: * location - that is you can switch it to another repository url not to
128: * create yet one more <b>SVNRepository</b> object. Use the {@link SVNRepository#setLocation(SVNURL, boolean) SVNRepository.setLocation()}
129: * method for this purpose.
130: *
131: * <p>
132: * An <b>SVNRepository</b> driver created by this method uses a default
133: * session options driver ({@link ISVNSession#DEFAULT}) which does not
134: * allow to keep a single socket connection opened and commit log messages
135: * caching.
136: *
137: * @param url a repository location URL
138: * @return a protocol specific <b>SVNRepository</b> driver
139: * @throws SVNException if there's no implementation for the specified protocol
140: * (the user may have forgotten to register a specific
141: * factory that creates <b>SVNRepository</b>
142: * instances for that protocol or the SVNKit
143: * library does not support that protocol at all)
144: * @see #create(SVNURL, ISVNSession)
145: * @see SVNRepository
146: *
147: */
148: public static SVNRepository create(SVNURL url) throws SVNException {
149: return create(url, null);
150:
151: }
152:
153: /**
154: * Creates an <b>SVNRepository</b> driver according to the protocol that is to be
155: * used to access a repository.
156: *
157: * <p>
158: * The protocol is defined as the beginning part of the URL schema. Currently
159: * SVNKit supports only <i>svn://</i> (<i>svn+ssh://</i>) and <i>http://</i> (<i>https://</i>)
160: * schemas.
161: *
162: * <p>
163: * The created <b>SVNRepository</b> driver can later be <i>"reused"</i> for another
164: * location - that is you can switch it to another repository url not to
165: * create yet one more <b>SVNRepository</b> object. Use the {@link SVNRepository#setLocation(SVNURL, boolean) SVNRepository.setLocation()}
166: * method for this purpose.
167: *
168: * <p>
169: * This method allows to customize a session options driver for an <b>SVNRepository</b> driver.
170: * A session options driver must implement the <b>ISVNSession</b> interface. It manages socket
171: * connections - says whether an <b>SVNRepository</b> driver may use a single socket connection
172: * during the runtime, or it should open a new connection per each repository access operation.
173: * And also a session options driver may cache and provide commit log messages during the
174: * runtime.
175: *
176: * @param url a repository location URL
177: * @param options a session options driver
178: * @return a protocol specific <b>SVNRepository</b> driver
179: * @throws SVNException if there's no implementation for the specified protocol
180: * (the user may have forgotten to register a specific
181: * factory that creates <b>SVNRepository</b>
182: * instances for that protocol or the SVNKit
183: * library does not support that protocol at all)
184: * @see #create(SVNURL)
185: * @see SVNRepository
186: */
187: public static SVNRepository create(SVNURL url, ISVNSession options)
188: throws SVNException {
189: String urlString = url.toString();
190: for (Iterator keys = myFactoriesMap.keySet().iterator(); keys
191: .hasNext();) {
192: String key = (String) keys.next();
193: if (Pattern.matches(key, urlString)) {
194: return ((SVNRepositoryFactory) myFactoriesMap.get(key))
195: .createRepositoryImpl(url, options);
196: }
197: }
198: if ("file".equalsIgnoreCase(url.getProtocol())) {
199: SVNErrorMessage err = SVNErrorMessage.create(
200: SVNErrorCode.RA_LOCAL_REPOS_OPEN_FAILED,
201: "Unable to open an ra_local session to URL");
202: SVNErrorManager.error(err);
203: }
204: SVNErrorMessage err = SVNErrorMessage.create(
205: SVNErrorCode.BAD_URL,
206: "Unable to create SVNRepository object for ''{0}''",
207: url);
208: SVNErrorManager.error(err);
209: return null;
210: }
211:
212: /**
213: * Creates a local blank FSFS-type repository.
214: * A call to this routine is equivalent to
215: * <code>createLocalRepository(path, null, enableRevisionProperties, force)</code>.
216: *
217: * @param path a repository root location
218: * @param enableRevisionProperties enables or not revision property
219: * modifications
220: * @param force forces operation to run
221: * @return a local URL (file:///) of a newly
222: * created repository
223: * @throws SVNException
224: * @see #createLocalRepository(File, String, boolean, boolean)
225: * @since 1.1
226: */
227: public static SVNURL createLocalRepository(File path,
228: boolean enableRevisionProperties, boolean force)
229: throws SVNException {
230: return createLocalRepository(path, null,
231: enableRevisionProperties, force);
232: }
233:
234: /**
235: * Creates a local blank FSFS-type repository. This is just similar to
236: * the Subversion's command: <code>svnadmin create --fs-type=fsfs REPOS_PATH</code>.
237: * The resultant repository is absolutely format-compatible with Subversion.
238: *
239: * <p>
240: * If <code>uuid</code> is <span class="javakeyword">null</span> or not 36 chars
241: * wide, the method generates a new UUID for the repository. This UUID would have
242: * the same format as if it's generated by Subversion itself.
243: *
244: * <p>
245: * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>
246: * then the method creates a <code>pre-revprop-change</code> executable file inside
247: * the <code>"hooks"</code> subdir of the repository tree. This executable file
248: * simply returns 0 thus allowing revision property modifications, which are not
249: * permitted, unless one puts such a hook into that very directory.
250: *
251: * <p>
252: * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
253: * exists, deletes that path and creates a repository in its place.
254: *
255: * <p>
256: * A call to this routine is equivalent to
257: * <code>createLocalRepository(path, uuid, enableRevisionProperties, force, false)</code>.
258: *
259: * @param path a repository root location
260: * @param uuid a repository's uuid
261: * @param enableRevisionProperties enables or not revision property
262: * modifications
263: * @param force forces operation to run
264: * @return a local URL (file:///) of a newly
265: * created repository
266: * @throws SVNException
267: * @see #createLocalRepository(File, String, boolean, boolean, boolean)
268: * @since 1.1
269: */
270: public static SVNURL createLocalRepository(File path, String uuid,
271: boolean enableRevisionProperties, boolean force)
272: throws SVNException {
273: return createLocalRepository(path, uuid,
274: enableRevisionProperties, force, false);
275: }
276:
277: /**
278: * Creates a local blank FSFS-type repository. This is just similar to
279: * the Subversion's command: <code>svnadmin create --fs-type=fsfs REPOS_PATH</code>.
280: * The resultant repository is absolutely format-compatible with Subversion.
281: *
282: * <p>
283: * If <code>uuid</code> is <span class="javakeyword">null</span> or not 36 chars
284: * wide, the method generates a new UUID for the repository. This UUID would have
285: * the same format as if it's generated by Subversion itself.
286: *
287: * <p>
288: * If <code>enableRevisionProperties</code> is <span class="javakeyword">true</span>
289: * then the method creates a <code>pre-revprop-change</code> executable file inside
290: * the <code>"hooks"</code> subdir of the repository tree. This executable file
291: * simply returns 0 thus allowing revision property modifications, which are not
292: * permitted, unless one puts such a hook into that very directory.
293: *
294: * <p>
295: * If <code>force</code> is <span class="javakeyword">true</span> and <code>path</code> already
296: * exists, deletes that path and creates a repository in its place.
297: *
298: * <p>
299: * Set <code>pre14Compatible</code> to <span class="javakeyword">true</span> if you want a new repository
300: * to be compatible with pre-1.4 servers.
301: *
302: * @param path a repository root location
303: * @param uuid a repository's uuid
304: * @param enableRevisionProperties enables or not revision property
305: * modifications
306: * @param force forces operation to run
307: * @param pre14Compatible <span class="javakeyword">true</span> to
308: * create a repository with pre-1.4 format
309: * @return a local URL (file:///) of a newly
310: * created repository
311: * @throws SVNException
312: * @since 1.1.1
313: */
314: public static SVNURL createLocalRepository(File path, String uuid,
315: boolean enableRevisionProperties, boolean force,
316: boolean pre14Compatible) throws SVNException {
317: SVNFileType fType = SVNFileType.getType(path);
318: if (fType != SVNFileType.NONE) {
319: if (fType == SVNFileType.DIRECTORY) {
320: File[] children = SVNFileListUtil.listFiles(path);
321: if (children != null && children.length != 0) {
322: if (!force) {
323: SVNErrorMessage err = SVNErrorMessage
324: .create(
325: SVNErrorCode.IO_ERROR,
326: "''{0}'' already exists; use ''force'' to overwrite existing files",
327: path);
328: SVNErrorManager.error(err);
329: } else {
330: SVNFileUtil.deleteAll(path, true);
331: }
332: }
333: } else {
334: if (!force) {
335: SVNErrorMessage err = SVNErrorMessage
336: .create(
337: SVNErrorCode.IO_ERROR,
338: "''{0}'' already exists; use ''force'' to overwrite existing files",
339: path);
340: SVNErrorManager.error(err);
341: } else {
342: SVNFileUtil.deleteAll(path, true);
343: }
344: }
345: }
346: //SVNFileUtil.deleteAll(path, true);
347: if (!path.mkdirs() && !path.exists()) {
348: SVNErrorMessage err = SVNErrorMessage.create(
349: SVNErrorCode.IO_ERROR,
350: "Can not create directory ''{0}''", path);
351: SVNErrorManager.error(err);
352: }
353: InputStream is = SVNRepositoryFactory.class
354: .getResourceAsStream(REPOSITORY_TEMPLATE_PATH);
355: if (is == null) {
356: SVNErrorMessage err = SVNErrorMessage
357: .create(SVNErrorCode.IO_ERROR,
358: "No repository template found; should be part of SVNKit library jar");
359: SVNErrorManager.error(err);
360: }
361: File jarFile = SVNFileUtil.createUniqueFile(path, ".template",
362: ".jar");
363: OutputStream uuidOS = null;
364: OutputStream reposFormatOS = null;
365: OutputStream fsFormatOS = null;
366: try {
367: copyToFile(is, jarFile);
368: extract(jarFile, path);
369: // translate eols.
370: if (!SVNFileUtil.isWindows) {
371: translateFiles(path);
372: translateFiles(new File(path, "conf"));
373: translateFiles(new File(path, "hooks"));
374: translateFiles(new File(path, "locks"));
375: }
376: // create pre-rev-prop.
377: if (enableRevisionProperties) {
378: if (SVNFileUtil.isWindows) {
379: SVNFileUtil.createEmptyFile(new File(path,
380: "hooks/pre-revprop-change.bat"));
381: } else {
382: File hookFile = new File(path,
383: "hooks/pre-revprop-change");
384: OutputStream os = null;
385: try {
386: os = SVNFileUtil.openFileForWriting(hookFile);
387: os.write("#!/bin/sh\nexit 0"
388: .getBytes("US-ASCII"));
389: } catch (IOException e) {
390: SVNErrorMessage err = SVNErrorMessage
391: .create(
392: SVNErrorCode.IO_ERROR,
393: "Cannot create pre-rev-prop-change hook file at ''{0}'': {1}",
394: new Object[] { hookFile,
395: e.getLocalizedMessage() });
396: SVNErrorManager.error(err);
397: } finally {
398: SVNFileUtil.closeFile(os);
399: }
400: SVNFileUtil.setExecutable(hookFile, true);
401: }
402: }
403: // generate and write UUID.
404: File uuidFile = new File(path, "db/uuid");
405: if (uuid == null || uuid.length() != 36) {
406: byte[] uuidBytes = SVNUUIDGenerator.generateUUID();
407: uuid = SVNUUIDGenerator.formatUUID(uuidBytes);
408: }
409: uuid += '\n';
410: try {
411: uuidOS = SVNFileUtil.openFileForWriting(uuidFile);
412: uuidOS.write(uuid.getBytes("US-ASCII"));
413: } catch (IOException e) {
414: SVNErrorMessage err = SVNErrorMessage.create(
415: SVNErrorCode.IO_ERROR,
416: "Error writing repository UUID to ''{0}''",
417: uuidFile);
418: err
419: .setChildErrorMessage(SVNErrorMessage.create(
420: SVNErrorCode.IO_ERROR, e
421: .getLocalizedMessage()));
422: SVNErrorManager.error(err);
423: }
424:
425: if (pre14Compatible) {
426: File reposFormatFile = new File(path, "format");
427: try {
428: reposFormatOS = SVNFileUtil
429: .openFileForWriting(reposFormatFile);
430: reposFormatOS.write("3\n".getBytes("US-ASCII"));
431: } catch (IOException e) {
432: SVNErrorMessage err = SVNErrorMessage
433: .create(
434: SVNErrorCode.IO_ERROR,
435: "Error writing repository format to ''{0}''",
436: reposFormatFile);
437: err.setChildErrorMessage(SVNErrorMessage.create(
438: SVNErrorCode.IO_ERROR, e
439: .getLocalizedMessage()));
440: SVNErrorManager.error(err);
441: }
442:
443: File fsFormatFile = new File(path, "db/format");
444: try {
445: fsFormatOS = SVNFileUtil
446: .openFileForWriting(fsFormatFile);
447: fsFormatOS.write("1\n".getBytes("US-ASCII"));
448: } catch (IOException e) {
449: SVNErrorMessage err = SVNErrorMessage.create(
450: SVNErrorCode.IO_ERROR,
451: "Error writing fs format to ''{0}''",
452: fsFormatFile);
453: err.setChildErrorMessage(SVNErrorMessage.create(
454: SVNErrorCode.IO_ERROR, e
455: .getLocalizedMessage()));
456: SVNErrorManager.error(err);
457: }
458: }
459: // set creation date.
460: File rev0File = new File(path, "db/revprops/0");
461: SVNProperties props = new SVNProperties(rev0File, null);
462: String date = SVNTimeUtil.formatDate(new Date(System
463: .currentTimeMillis()), true);
464: props.setPropertyValue(SVNRevisionProperty.DATE, date);
465: } finally {
466: SVNFileUtil.closeFile(uuidOS);
467: SVNFileUtil.closeFile(reposFormatOS);
468: SVNFileUtil.closeFile(fsFormatOS);
469: SVNFileUtil.deleteFile(jarFile);
470: }
471: return SVNURL.fromFile(path);
472: }
473:
474: protected abstract SVNRepository createRepositoryImpl(SVNURL url,
475: ISVNSession session);
476:
477: private static void copyToFile(InputStream is, File dstFile)
478: throws SVNException {
479: OutputStream os = null;
480: byte[] buffer = new byte[16 * 1024];
481: try {
482: os = SVNFileUtil.openFileForWriting(dstFile);
483: while (true) {
484: int r = is.read(buffer);
485: if (r <= 0) {
486: break;
487: }
488: os.write(buffer, 0, r);
489: }
490: } catch (IOException e) {
491: SVNErrorMessage err = SVNErrorMessage.create(
492: SVNErrorCode.IO_ERROR,
493: "Can not copy repository template file to ''{0}''",
494: dstFile);
495: err.setChildErrorMessage(SVNErrorMessage.create(
496: SVNErrorCode.IO_ERROR, e.getLocalizedMessage()));
497: SVNErrorManager.error(err);
498: } finally {
499: SVNFileUtil.closeFile(os);
500: SVNFileUtil.closeFile(is);
501: }
502: }
503:
504: private static void extract(File srcFile, File dst)
505: throws SVNException {
506: JarInputStream jis = null;
507: InputStream is = SVNFileUtil.openFileForReading(srcFile);
508: byte[] buffer = new byte[16 * 1024];
509:
510: JarFile jarFile = null;
511: try {
512: jarFile = new JarFile(srcFile);
513: jis = new JarInputStream(is);
514: while (true) {
515: JarEntry entry = jis.getNextJarEntry();
516: if (entry == null) {
517: break;
518: }
519: String name = entry.getName();
520: File entryFile = new File(dst, name);
521: if (entry.isDirectory()) {
522: entryFile.mkdirs();
523: } else {
524: InputStream fis = null;
525: OutputStream fos = null;
526: try {
527: fis = new BufferedInputStream(jarFile
528: .getInputStream(entry));
529: fos = SVNFileUtil.openFileForWriting(entryFile);
530: while (true) {
531: int r = fis.read(buffer);
532: if (r <= 0) {
533: break;
534: }
535: fos.write(buffer, 0, r);
536: }
537: } finally {
538: SVNFileUtil.closeFile(fos);
539: SVNFileUtil.closeFile(fis);
540: }
541: }
542: jis.closeEntry();
543: }
544: } catch (IOException e) {
545: SVNErrorMessage err = SVNErrorMessage
546: .create(
547: SVNErrorCode.IO_ERROR,
548: "Can not extract repository files from ''{0}'' to ''{1}''",
549: new Object[] { srcFile, dst });
550: err.setChildErrorMessage(SVNErrorMessage.create(
551: SVNErrorCode.IO_ERROR, e.getLocalizedMessage()));
552: SVNErrorManager.error(err);
553: } finally {
554: SVNFileUtil.closeFile(jis);
555: SVNFileUtil.closeFile(is);
556: if (jarFile != null) {
557: try {
558: jarFile.close();
559: } catch (IOException e) {
560: }
561: }
562: }
563: }
564:
565: private static void translateFiles(File directory)
566: throws SVNException {
567: File[] children = SVNFileListUtil.listFiles(directory);
568: byte[] eol = new byte[] { '\n' };
569: for (int i = 0; children != null && i < children.length; i++) {
570: File child = children[i];
571: File tmpChild = null;
572: if (child.isFile()) {
573: try {
574: tmpChild = SVNFileUtil.createUniqueFile(directory,
575: ".repos", ".tmp");
576: SVNTranslator.translate(child, tmpChild, eol, null,
577: false, true);
578: SVNFileUtil.deleteFile(child);
579: SVNFileUtil.rename(tmpChild, child);
580: } finally {
581: SVNFileUtil.deleteFile(tmpChild);
582: }
583: }
584: }
585: }
586: }
|