001package co.codewizards.cloudstore.rest.client.transport;
002
003import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*;
004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
005import static java.util.Objects.*;
006
007import java.io.IOException;
008import java.net.MalformedURLException;
009import java.net.URL;
010import java.security.GeneralSecurityException;
011import java.util.Arrays;
012import java.util.Collections;
013import java.util.Date;
014import java.util.HashMap;
015import java.util.List;
016import java.util.Map;
017import java.util.UUID;
018
019import javax.ws.rs.client.ClientBuilder;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import co.codewizards.cloudstore.core.auth.AuthConstants;
025import co.codewizards.cloudstore.core.auth.AuthToken;
026import co.codewizards.cloudstore.core.auth.AuthTokenIO;
027import co.codewizards.cloudstore.core.auth.AuthTokenVerifier;
028import co.codewizards.cloudstore.core.auth.EncryptedSignedAuthToken;
029import co.codewizards.cloudstore.core.auth.SignedAuthToken;
030import co.codewizards.cloudstore.core.auth.SignedAuthTokenDecrypter;
031import co.codewizards.cloudstore.core.auth.SignedAuthTokenIO;
032import co.codewizards.cloudstore.core.concurrent.DeferredCompletionException;
033import co.codewizards.cloudstore.core.config.ConfigImpl;
034import co.codewizards.cloudstore.core.dto.ChangeSetDto;
035import co.codewizards.cloudstore.core.dto.ConfigPropSetDto;
036import co.codewizards.cloudstore.core.dto.DateTime;
037import co.codewizards.cloudstore.core.dto.RepoFileDto;
038import co.codewizards.cloudstore.core.dto.RepositoryDto;
039import co.codewizards.cloudstore.core.dto.VersionInfoDto;
040import co.codewizards.cloudstore.core.dto.jaxb.ChangeSetDtoIo;
041import co.codewizards.cloudstore.core.io.TimeoutException;
042import co.codewizards.cloudstore.core.oio.File;
043import co.codewizards.cloudstore.core.oio.FileFilter;
044import co.codewizards.cloudstore.core.repo.local.ContextWithLocalRepoManager;
045import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
046import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory;
047import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl;
048import co.codewizards.cloudstore.core.repo.transport.AbstractRepoTransport;
049import co.codewizards.cloudstore.rest.client.ClientBuilderDefaultValuesDecorator;
050import co.codewizards.cloudstore.rest.client.CloudStoreRestClient;
051import co.codewizards.cloudstore.rest.client.CredentialsProvider;
052import co.codewizards.cloudstore.rest.client.request.BeginPutFile;
053import co.codewizards.cloudstore.rest.client.request.Copy;
054import co.codewizards.cloudstore.rest.client.request.Delete;
055import co.codewizards.cloudstore.rest.client.request.EndPutFile;
056import co.codewizards.cloudstore.rest.client.request.EndSyncFromRepository;
057import co.codewizards.cloudstore.rest.client.request.EndSyncToRepository;
058import co.codewizards.cloudstore.rest.client.request.GetChangeSetDto;
059import co.codewizards.cloudstore.rest.client.request.GetClientRepositoryDto;
060import co.codewizards.cloudstore.rest.client.request.GetEncryptedSignedAuthToken;
061import co.codewizards.cloudstore.rest.client.request.GetFileData;
062import co.codewizards.cloudstore.rest.client.request.GetRepoFileDto;
063import co.codewizards.cloudstore.rest.client.request.GetRepositoryDto;
064import co.codewizards.cloudstore.rest.client.request.GetVersionInfoDto;
065import co.codewizards.cloudstore.rest.client.request.MakeDirectory;
066import co.codewizards.cloudstore.rest.client.request.MakeSymlink;
067import co.codewizards.cloudstore.rest.client.request.Move;
068import co.codewizards.cloudstore.rest.client.request.PutFileData;
069import co.codewizards.cloudstore.rest.client.request.PutParentConfigPropSetDto;
070import co.codewizards.cloudstore.rest.client.request.RequestRepoConnection;
071import co.codewizards.cloudstore.rest.client.ssl.DynamicX509TrustManagerCallback;
072import co.codewizards.cloudstore.rest.client.ssl.SSLContextBuilder;
073
074public class RestRepoTransport extends AbstractRepoTransport implements CredentialsProvider, ContextWithLocalRepoManager {
075        private static final Logger logger = LoggerFactory.getLogger(RestRepoTransport.class);
076
077        public static final String CONFIG_KEY_GET_CHANGE_SET_DTO_TIMEOUT = "getChangeSetDtoTimeout";
078        public static final long CONFIG_DEFAULT_GET_CHANGE_SET_DTO_TIMEOUT = 60L * 60L * 1000L;
079
080        public static final String CONFIG_KEY_GET_REPO_FILE_DTO_WITH_FILE_CHUNK_DTOS_TIMEOUT = "getRepoFileDtoWithFileChunkDtosTimeout";
081        public static final long CONFIG_DEFAULT_GET_REPO_FILE_DTO_WITH_FILE_CHUNK_DTOS_TIMEOUT = 60L * 60L * 1000L;
082
083        private final long changeSetTimeout = ConfigImpl.getInstance().getPropertyAsPositiveOrZeroLong(
084                        CONFIG_KEY_GET_CHANGE_SET_DTO_TIMEOUT, CONFIG_DEFAULT_GET_CHANGE_SET_DTO_TIMEOUT);
085
086        private final long fileChunkSetTimeout = ConfigImpl.getInstance().getPropertyAsPositiveOrZeroLong(
087                        CONFIG_KEY_GET_REPO_FILE_DTO_WITH_FILE_CHUNK_DTOS_TIMEOUT, CONFIG_DEFAULT_GET_REPO_FILE_DTO_WITH_FILE_CHUNK_DTOS_TIMEOUT);
088
089//      public static final String CHANGE_SET_DTO_CACHE_FILE_NAME_TEMPLATE = "ChangeSetDto.${serverRepositoryId}.${lastRevisionSynced}.xml.gz";
090        public static final String CHANGE_SET_DTO_CACHE_FILE_NAME_PREFIX = "ChangeSetDto.";
091        public static final String CHANGE_SET_DTO_CACHE_FILE_NAME_SUFFIX = ".xml.gz";
092        public static final String TMP_FILE_NAME_SUFFIX = ".tmp";
093
094        private UUID repositoryId; // server-repository
095        private byte[] publicKey;
096        private String repositoryName; // server-repository
097        private CloudStoreRestClient client;
098        private final Map<UUID, AuthToken> clientRepositoryId2AuthToken = new HashMap<UUID, AuthToken>(1); // should never be more ;-)
099        private LocalRepoManager localRepoManager;
100        private File localRepoTmpDir;
101
102        protected DynamicX509TrustManagerCallback getDynamicX509TrustManagerCallback() {
103                final RestRepoTransportFactory repoTransportFactory = (RestRepoTransportFactory) getRepoTransportFactory();
104                final Class<? extends DynamicX509TrustManagerCallback> klass = repoTransportFactory.getDynamicX509TrustManagerCallbackClass();
105                if (klass == null)
106                        throw new IllegalStateException("dynamicX509TrustManagerCallbackClass is not set!");
107
108                try {
109                        final DynamicX509TrustManagerCallback instance = klass.newInstance();
110                        return instance;
111                } catch (final Exception e) {
112                        throw new RuntimeException(String.format("Could not instantiate class %s: %s", klass.getName(), e.toString()), e);
113                }
114        }
115
116        public RestRepoTransport() { }
117
118        @Override
119        public UUID getRepositoryId() {
120                if (repositoryId == null) {
121                        final RepositoryDto repositoryDto = getRepositoryDto();
122                        repositoryId = repositoryDto.getRepositoryId();
123                        publicKey = repositoryDto.getPublicKey();
124                }
125                return repositoryId;
126        }
127
128        @Override
129        public byte[] getPublicKey() {
130                getRepositoryId(); // ensure, the public key is loaded
131                return requireNonNull(publicKey, "publicKey");
132        }
133
134        @Override
135        public RepositoryDto getRepositoryDto() {
136                return getClient().execute(new GetRepositoryDto(getRepositoryName()));
137        }
138
139        @Override
140        public RepositoryDto getClientRepositoryDto() {
141                getClientRepositoryIdOrFail();
142                return getClient().execute(new GetClientRepositoryDto(getRepositoryName()));
143        }
144
145        @Override
146        public void requestRepoConnection(final byte[] publicKey) {
147                final RepositoryDto repositoryDto = new RepositoryDto();
148                repositoryDto.setRepositoryId(getClientRepositoryIdOrFail());
149                repositoryDto.setPublicKey(publicKey);
150                getClient().execute(new RequestRepoConnection(getRepositoryName(), getPathPrefix(), repositoryDto));
151        }
152
153        @Override
154        public void close() {
155                client = null;
156                super.close();
157        }
158
159        @Override
160        public ChangeSetDto getChangeSetDto(final boolean localSync, final Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) {
161                File changeSetDtoCacheFile = null;
162                ChangeSetDto result = null;
163
164                try {
165                        changeSetDtoCacheFile = getChangeSetDtoCacheFile(lastSyncToRemoteRepoLocalRepositoryRevisionSynced);
166                        if (changeSetDtoCacheFile.isFile() && changeSetDtoCacheFile.length() > 0) {
167                                ChangeSetDtoIo changeSetDtoIo = createObject(ChangeSetDtoIo.class);
168                                result = changeSetDtoIo.deserializeWithGz(changeSetDtoCacheFile);
169                                logger.info("getChangeSetDto: Read ChangeSetDto-cache-file: {}", changeSetDtoCacheFile.getAbsolutePath());
170                                return result;
171                        } else {
172                                logger.info("getChangeSetDto: ChangeSetDto-cache-file NOT found: {}", changeSetDtoCacheFile.getAbsolutePath());
173                        }
174                } catch (Exception x) {
175                        result = null;
176                        logger.error("getChangeSetDto: Reading ChangeSetDto-cache-file failed: " + x, x);
177                }
178
179                final long beginTimestamp = System.currentTimeMillis();
180                while (true) {
181                        try {
182                                result = getClient().execute(new GetChangeSetDto(getRepositoryId().toString(), localSync, lastSyncToRemoteRepoLocalRepositoryRevisionSynced));
183                        } catch (final DeferredCompletionException x) {
184                                if (System.currentTimeMillis() > beginTimestamp + changeSetTimeout)
185                                        throw new TimeoutException(String.format("Could not get change-set within %s milliseconds!", changeSetTimeout), x);
186
187                                logger.info("getChangeSetDto: Got DeferredCompletionException; will retry.");
188                        }
189
190                        if (result != null) {
191                                if (changeSetDtoCacheFile != null) {
192                                        File tmpFile = changeSetDtoCacheFile.getParentFile().createFile(changeSetDtoCacheFile.getName() + TMP_FILE_NAME_SUFFIX);
193                                        ChangeSetDtoIo changeSetDtoIo = createObject(ChangeSetDtoIo.class);
194                                        changeSetDtoIo.serializeWithGz(result, tmpFile);
195                                        if (! tmpFile.renameTo(changeSetDtoCacheFile)) {
196                                                logger.error("getChangeSetDto: Could not rename temporary file to active ChangeSetDto-cache-file: {}", changeSetDtoCacheFile.getAbsolutePath());
197                                        } else {
198                                                logger.info("getChangeSetDto: Wrote ChangeSetDto-cache-file: {}", changeSetDtoCacheFile.getAbsolutePath());
199                                        }
200                                }
201                                return result;
202                        }
203                }
204        }
205
206        @Override
207        public void prepareForChangeSetDto(ChangeSetDto changeSetDto) {
208                // nothing to do here.
209        }
210
211        @Override
212        public void makeDirectory(String path, final Date lastModified) {
213                path = prefixPath(path);
214                getClient().execute(new MakeDirectory(getRepositoryId().toString(), path, lastModified));
215        }
216
217        @Override
218        public void makeSymlink(String path, final String target, final Date lastModified) {
219                path = prefixPath(path);
220                getClient().execute(new MakeSymlink(getRepositoryId().toString(), path, target, lastModified));
221        }
222
223        @Override
224        public void copy(String fromPath, String toPath) {
225                fromPath = prefixPath(fromPath);
226                toPath = prefixPath(toPath);
227                getClient().execute(new Copy(getRepositoryId().toString(), fromPath, toPath));
228        }
229
230        @Override
231        public void move(String fromPath, String toPath) {
232                fromPath = prefixPath(fromPath);
233                toPath = prefixPath(toPath);
234                getClient().execute(new Move(getRepositoryId().toString(), fromPath, toPath));
235        }
236
237        @Override
238        public void delete(String path) {
239                path = prefixPath(path);
240                getClient().execute(new Delete(getRepositoryId().toString(), path));
241        }
242
243        @Override
244        public RepoFileDto getRepoFileDto(String path) {
245                path = prefixPath(path);
246                final long beginTimestamp = System.currentTimeMillis();
247                while (true) {
248                        try {
249                                return getClient().execute(new GetRepoFileDto(getRepositoryId().toString(), path));
250                        } catch (final DeferredCompletionException x) {
251                                if (System.currentTimeMillis() > beginTimestamp + fileChunkSetTimeout)
252                                        throw new TimeoutException(String.format("Could not get file-chunk-set within %s milliseconds!", fileChunkSetTimeout), x);
253
254                                logger.info("getFileChunkSet: Got DeferredCompletionException; will retry.");
255                        }
256                }
257        }
258
259        @Override
260        public byte[] getFileData(String path, final long offset, final int length) {
261                path = prefixPath(path);
262                return getClient().execute(new GetFileData(getRepositoryId().toString(), path, offset, length));
263        }
264
265        @Override
266        public void beginPutFile(String path) {
267                path = prefixPath(path);
268                getClient().execute(new BeginPutFile(getRepositoryId().toString(), path));
269        }
270
271        @Override
272        public void putFileData(String path, final long offset, final byte[] fileData) {
273                path = prefixPath(path);
274                getClient().execute(new PutFileData(getRepositoryId().toString(), path, offset, fileData));
275        }
276
277        @Override
278        public void endPutFile(String path, final Date lastModified, final long length, final String sha1) {
279                path = prefixPath(path);
280                getClient().execute(new EndPutFile(getRepositoryId().toString(), path, new DateTime(lastModified), length, sha1));
281        }
282
283        @Override
284        public void endSyncFromRepository() {
285                for (final File file : getChangeSetDtoCacheFiles(true)) {
286                        file.delete();
287                }
288                File tmpDir = this.localRepoTmpDir;
289                if (tmpDir != null) {
290                        tmpDir.delete(); // deletes only, if empty.
291                        this.localRepoTmpDir = null; // null this in order to ensure that it is re-created by getLocalRepoTmpDir().
292                }
293                getClient().execute(new EndSyncFromRepository(getRepositoryId().toString()));
294        }
295
296        @Override
297        public void endSyncToRepository(final long fromLocalRevision) {
298                getClient().execute(new EndSyncToRepository(getRepositoryId().toString(), fromLocalRevision));
299        }
300
301        @Override
302        public void putParentConfigPropSetDto(ConfigPropSetDto parentConfigPropSetDto) {
303                getClient().execute(new PutParentConfigPropSetDto(getRepositoryId().toString(), parentConfigPropSetDto));
304        }
305
306        @Override
307        public String getUserName() {
308                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
309                return AuthConstants.USER_NAME_REPOSITORY_ID_PREFIX + clientRepositoryId;
310        }
311
312        @Override
313        public String getPassword() {
314                final AuthToken authToken = getAuthToken();
315                return authToken.getPassword();
316        }
317
318        private AuthToken getAuthToken() {
319                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
320                AuthToken authToken = clientRepositoryId2AuthToken.get(clientRepositoryId);
321                if (authToken != null && isAfterRenewalDate(authToken)) {
322                        logger.debug("getAuthToken: old AuthToken passed renewal-date: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
323                                        clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
324
325                        authToken = null;
326                }
327
328                if (authToken == null) {
329                        logger.debug("getAuthToken: getting new AuthToken: clientRepositoryId={} serverRepositoryId={}",
330                                        clientRepositoryId, getRepositoryId());
331
332                        final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRoot(clientRepositoryId);
333                        final LocalRepoManager localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot);
334                        try {
335                                final EncryptedSignedAuthToken encryptedSignedAuthToken = getClient().execute(new GetEncryptedSignedAuthToken(getRepositoryName(), localRepoManager.getRepositoryId()));
336
337                                final byte[] signedAuthTokenData = new SignedAuthTokenDecrypter(localRepoManager.getPrivateKey()).decrypt(encryptedSignedAuthToken);
338
339                                final SignedAuthToken signedAuthToken = new SignedAuthTokenIO().deserialise(signedAuthTokenData);
340
341                                final AuthTokenVerifier verifier = new AuthTokenVerifier(localRepoManager.getRemoteRepositoryPublicKeyOrFail(getRepositoryId()));
342                                verifier.verify(signedAuthToken);
343
344                                authToken = new AuthTokenIO().deserialise(signedAuthToken.getAuthTokenData());
345                                final Date expiryDate = requireNonNull(authToken.getExpiryDateTime(), "authToken.expiryDateTime").toDate();
346                                final Date renewalDate = requireNonNull(authToken.getRenewalDateTime(), "authToken.renewalDateTime").toDate();
347                                if (!renewalDate.before(expiryDate))
348                                        throw new IllegalArgumentException(
349                                                        String.format("Invalid AuthToken: renewalDateTime >= expiryDateTime :: renewalDateTime=%s expiryDateTime=%s",
350                                                                        authToken.getRenewalDateTime(), authToken.getExpiryDateTime()));
351
352                                clientRepositoryId2AuthToken.put(clientRepositoryId, authToken);
353                        } finally {
354                                localRepoManager.close();
355                        }
356
357                        logger.info("getAuthToken: got new AuthToken: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
358                                        clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
359                }
360                else
361                        logger.trace("getAuthToken: old AuthToken still valid: clientRepositoryId={} serverRepositoryId={} renewalDateTime={} expiryDateTime={}",
362                                        clientRepositoryId, getRepositoryId(), authToken.getRenewalDateTime(), authToken.getExpiryDateTime());
363
364                return authToken;
365        }
366
367        private boolean isAfterRenewalDate(final AuthToken authToken) {
368                requireNonNull(authToken, "authToken");
369                return System.currentTimeMillis() > authToken.getRenewalDateTime().getMillis();
370        }
371
372        protected CloudStoreRestClient getClient() {
373                if (client == null) {
374                        ClientBuilder clientBuilder = createClientBuilder();
375                        final CloudStoreRestClient c = new CloudStoreRestClient(getRemoteRoot(), clientBuilder);
376                        c.setCredentialsProvider(this);
377                        client = c;
378                }
379                return client;
380        }
381
382        @Override
383        protected URL determineRemoteRootWithoutPathPrefix() {
384                final String repositoryName = getRepositoryName();
385                final String baseURL = getClient().getBaseUrl();
386                if (!baseURL.endsWith("/"))
387                        throw new IllegalStateException(String.format("baseURL does not end with a '/'! baseURL='%s'", baseURL));
388
389                try {
390                        return new URL(baseURL + repositoryName);
391                } catch (final MalformedURLException e) {
392                        throw new RuntimeException(e);
393                }
394        }
395
396        public String getRepositoryName() {
397                if (repositoryName == null) {
398                        final String pathAfterBaseURL = getPathAfterBaseURL();
399                        final int indexOfFirstSlash = pathAfterBaseURL.indexOf('/');
400                        if (indexOfFirstSlash < 0) {
401                                repositoryName = pathAfterBaseURL;
402                        }
403                        else {
404                                repositoryName = pathAfterBaseURL.substring(0, indexOfFirstSlash);
405                        }
406                        if (repositoryName.isEmpty())
407                                throw new IllegalStateException("repositoryName is empty!");
408                }
409                return repositoryName;
410        }
411
412        private String pathAfterBaseURL;
413
414        protected String getPathAfterBaseURL() {
415                String pathAfterBaseURL = this.pathAfterBaseURL;
416                if (pathAfterBaseURL == null) {
417                        final URL remoteRoot = getRemoteRoot();
418                        if (remoteRoot == null)
419                                throw new IllegalStateException("remoteRoot not yet assigned!");
420
421                        final String baseURL = getClient().getBaseUrl();
422                        if (!baseURL.endsWith("/"))
423                                throw new IllegalStateException(String.format("baseURL does not end with a '/'! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL));
424
425                        final String remoteRootString = remoteRoot.toExternalForm();
426                        if (!remoteRootString.startsWith(baseURL))
427                                throw new IllegalStateException(String.format("remoteRoot does not start with baseURL! remoteRoot='%s' baseURL='%s'", remoteRoot, baseURL));
428
429                        this.pathAfterBaseURL = pathAfterBaseURL = remoteRootString.substring(baseURL.length());
430                }
431                return pathAfterBaseURL;
432        }
433
434        private ClientBuilder createClientBuilder(){
435                final ClientBuilder builder = new ClientBuilderDefaultValuesDecorator();
436                try {
437                        builder.sslContext(SSLContextBuilder.create()
438                                        .remoteURL(getRemoteRoot())
439                                        .callback(getDynamicX509TrustManagerCallback()).build());
440                } catch (final GeneralSecurityException e) {
441                        throw new RuntimeException(e);
442                }
443                return builder;
444        }
445
446        @Override
447        public VersionInfoDto getVersionInfoDto() {
448                final VersionInfoDto versionInfoDto = getClient().execute(new GetVersionInfoDto());
449                return versionInfoDto;
450        }
451
452        protected File getChangeSetDtoCacheFile(final Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) {
453                String fileName = CHANGE_SET_DTO_CACHE_FILE_NAME_PREFIX
454                                + getRepositoryId() + "."
455                                + lastSyncToRemoteRepoLocalRepositoryRevisionSynced
456                                + CHANGE_SET_DTO_CACHE_FILE_NAME_SUFFIX;
457                return getLocalRepoTmpDir().createFile(fileName);
458        }
459
460        protected List<File> getChangeSetDtoCacheFiles(final boolean includeTmpFiles) {
461                File[] fileArray = getLocalRepoTmpDir().listFiles(new FileFilter() {
462                        @Override
463                        public boolean accept(File file) {
464                                if (! file.getName().startsWith(CHANGE_SET_DTO_CACHE_FILE_NAME_PREFIX))
465                                        return false;
466
467                                if (file.getName().endsWith(CHANGE_SET_DTO_CACHE_FILE_NAME_SUFFIX))
468                                        return true;
469
470                                if (includeTmpFiles && file.getName().endsWith(CHANGE_SET_DTO_CACHE_FILE_NAME_SUFFIX + TMP_FILE_NAME_SUFFIX))
471                                        return true;
472                                else
473                                        return false;
474                        }
475                });
476                return fileArray == null ? Collections.<File>emptyList() : Arrays.asList(fileArray);
477        }
478
479        protected File getLocalRepoTmpDir() {
480                if (localRepoTmpDir == null) {
481                        try {
482                                final File metaDir = getLocalRepoMetaDir();
483                                if (! metaDir.isDirectory()) {
484                                        if (metaDir.isFile())
485                                                throw new IOException(String.format("Path '%s' already exists as ordinary file! It should be a directory!", metaDir.getAbsolutePath()));
486                                        else
487                                                throw new IOException(String.format("Directory '%s' does not exist!", metaDir.getAbsolutePath()));
488                                }
489
490                                final File tmpDir = metaDir.createFile(LocalRepoManager.REPO_TEMP_DIR_NAME);
491                                if (! tmpDir.isDirectory()) {
492                                        tmpDir.mkdir();
493
494                                        if (! tmpDir.isDirectory()) {
495                                                if (tmpDir.isFile())
496                                                        throw new IOException(String.format("Cannot create directory '%s', because this path already exists as an ordinary file!", tmpDir.getAbsolutePath()));
497                                                else
498                                                        throw new IOException(String.format("Creating directory '%s' failed for an unknown reason (permissions? disk full?)!", tmpDir.getAbsolutePath()));
499                                        }
500                                }
501                                this.localRepoTmpDir = tmpDir;
502                        } catch (RuntimeException x) {
503                                throw x;
504                        } catch (Exception x) {
505                                throw new RuntimeException(x);
506                        }
507                }
508                return localRepoTmpDir;
509        }
510
511        protected File getLocalRepoMetaDir() {
512                final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRootOrFail(getClientRepositoryIdOrFail());
513                return createFile(localRoot, LocalRepoManager.META_DIR_NAME);
514        }
515
516        @Override
517        public LocalRepoManager getLocalRepoManager() {
518                if (localRepoManager == null) {
519                        logger.debug("getLocalRepoManager: Creating a new LocalRepoManager.");
520                        final File localRoot = LocalRepoRegistryImpl.getInstance().getLocalRoot(getClientRepositoryIdOrFail());
521                        localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRoot);
522                }
523                return localRepoManager;
524        }
525}