/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.fabric.netty.repository; import com.liferay.portal.fabric.netty.fileserver.FileHelperUtil; import com.liferay.portal.fabric.netty.fileserver.FileRequest; import com.liferay.portal.fabric.netty.fileserver.FileResponse; import com.liferay.portal.fabric.netty.util.NettyUtil; import com.liferay.portal.fabric.repository.Repository; import com.liferay.portal.fabric.repository.RepositoryHelperUtil; import com.liferay.portal.kernel.concurrent.AsyncBroker; import com.liferay.portal.kernel.concurrent.BaseFutureListener; import com.liferay.portal.kernel.concurrent.DefaultNoticeableFuture; import com.liferay.portal.kernel.concurrent.NoticeableFuture; import com.liferay.portal.kernel.concurrent.NoticeableFutureConverter; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; /** * @author Shuyang Zhou */ public class NettyRepository implements Repository<Channel> { public NettyRepository(Path repositoryPath, long getFileTimeout) { if (repositoryPath == null) { throw new NullPointerException("Repository path is null"); } if (!Files.isDirectory(repositoryPath)) { throw new IllegalArgumentException( repositoryPath + " is not a directory"); } this.repositoryPath = repositoryPath; this.getFileTimeout = getFileTimeout; } @Override public void dispose(boolean delete) { Set<Map.Entry<Path, Path>> entrySet = pathMap.entrySet(); Iterator<Map.Entry<Path, Path>> iterator = entrySet.iterator(); while (iterator.hasNext()) { Map.Entry<Path, Path> entry = iterator.next(); iterator.remove(); FileHelperUtil.delete(true, entry.getValue()); } if (delete) { FileHelperUtil.delete(true, repositoryPath); } } @Override public AsyncBroker<Path, FileResponse> getAsyncBroker() { return asyncBroker; } @Override public NoticeableFuture<Path> getFile( Channel channel, Path remoteFilePath, Path localFilePath, boolean deleteAfterFetch) { if (localFilePath == null) { return getFile( channel, remoteFilePath, RepositoryHelperUtil.getRepositoryFilePath( repositoryPath, remoteFilePath), deleteAfterFetch, true); } return getFile( channel, remoteFilePath, localFilePath, deleteAfterFetch, false); } @Override public NoticeableFuture<Map<Path, Path>> getFiles( Channel channel, Map<Path, Path> pathMap, boolean deleteAfterFetch) { final DefaultNoticeableFuture<Map<Path, Path>> defaultNoticeableFuture = new DefaultNoticeableFuture<>(); if (pathMap.isEmpty()) { defaultNoticeableFuture.set(pathMap); return defaultNoticeableFuture; } final Map<Path, Path> resultPathMap = new ConcurrentHashMap<>(); final AtomicInteger counter = new AtomicInteger(pathMap.size()); for (Map.Entry<Path, Path> entry : pathMap.entrySet()) { final Path remoteFilePath = entry.getKey(); NoticeableFuture<Path> noticeableFuture = getFile( channel, remoteFilePath, entry.getValue(), deleteAfterFetch); noticeableFuture.addFutureListener( new BaseFutureListener<Path>() { @Override public void completeWithCancel(Future<Path> future) { defaultNoticeableFuture.cancel(true); } @Override public void completeWithException( Future<Path> future, Throwable throwable) { defaultNoticeableFuture.setException(throwable); } @Override public void completeWithResult( Future<Path> future, Path localFilePath) { if (localFilePath != null) { resultPathMap.put(remoteFilePath, localFilePath); } if (counter.decrementAndGet() <= 0) { defaultNoticeableFuture.set(resultPathMap); } } }); } return defaultNoticeableFuture; } @Override public Path getRepositoryPath() { return repositoryPath; } protected static long getLastModifiedTime(Path path) { if (path == null) { return Long.MIN_VALUE; } try { FileTime fileTime = Files.getLastModifiedTime(path); return fileTime.toMillis(); } catch (IOException ioe) { return Long.MIN_VALUE; } } protected NoticeableFuture<Path> getFile( Channel channel, final Path remoteFilePath, final Path localFilePath, boolean deleteAfterFetch, final boolean populateCache) { if (_log.isDebugEnabled()) { _log.debug("Fetching remote file " + remoteFilePath); } final Path cachedLocalFilePath = pathMap.get(remoteFilePath); final DefaultNoticeableFuture<FileResponse> defaultNoticeableFuture = new DefaultNoticeableFuture<>(); NoticeableFuture<FileResponse> noticeableFuture = asyncBroker.post( remoteFilePath, defaultNoticeableFuture); if (noticeableFuture == null) { noticeableFuture = defaultNoticeableFuture; NettyUtil.scheduleCancellation( channel, defaultNoticeableFuture, getFileTimeout); ChannelFuture channelFuture = channel.writeAndFlush( new FileRequest( remoteFilePath, getLastModifiedTime(cachedLocalFilePath), deleteAfterFetch)); channelFuture.addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) { if (channelFuture.isSuccess()) { return; } if (channelFuture.isCancelled()) { defaultNoticeableFuture.cancel(true); return; } Throwable throwable = new IOException( "Unable to fetch remote file " + remoteFilePath, channelFuture.cause()); if (!asyncBroker.takeWithException( remoteFilePath, throwable)) { _log.error( "Unable to place exception because no future " + "exists with ID " + remoteFilePath, throwable); } } }); } return new NoticeableFutureConverter<Path, FileResponse>( noticeableFuture) { @Override protected Path convert(FileResponse fileResponse) throws IOException { if (fileResponse.isFileNotFound()) { if (_log.isWarnEnabled()) { _log.warn( "Remote file " + remoteFilePath + " is not found"); } return null; } if (fileResponse.isFileNotModified()) { if (_log.isDebugEnabled()) { _log.debug( "Remote file " + remoteFilePath + " is not modified, use cached local file " + cachedLocalFilePath); } return cachedLocalFilePath; } Path targetLocalFilePath = localFilePath; synchronized (fileResponse) { Path recheckCacheLocalFilePath = pathMap.get( remoteFilePath); if (recheckCacheLocalFilePath != null) { targetLocalFilePath = recheckCacheLocalFilePath; } Path tempLocalFilePath = fileResponse.getLocalFile(); if (tempLocalFilePath.startsWith(repositoryPath)) { Files.copy( fileResponse.getLocalFile(), targetLocalFilePath, StandardCopyOption.REPLACE_EXISTING); } else { Files.move( fileResponse.getLocalFile(), targetLocalFilePath, StandardCopyOption.REPLACE_EXISTING); } if (populateCache) { pathMap.put(remoteFilePath, targetLocalFilePath); } fileResponse.setLocalFile(targetLocalFilePath); } if (_log.isDebugEnabled()) { _log.debug( "Fetched remote file " + remoteFilePath + " to " + targetLocalFilePath); } return targetLocalFilePath; } }; } protected final AsyncBroker<Path, FileResponse> asyncBroker = new AsyncBroker<>(); protected final long getFileTimeout; protected final Map<Path, Path> pathMap = new ConcurrentHashMap<>(); protected final Path repositoryPath; private static final Log _log = LogFactoryUtil.getLog( NettyRepository.class); }