/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.sling.ide.eclipse.ui.internal; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.util.Text; import org.apache.sling.ide.eclipse.core.ProjectUtil; import org.apache.sling.ide.eclipse.core.ResourceUtil; import org.apache.sling.ide.eclipse.core.ServerUtil; import org.apache.sling.ide.eclipse.core.internal.ResourceAndInfo; import org.apache.sling.ide.eclipse.core.internal.ResourceChangeCommandFactory; import org.apache.sling.ide.eclipse.core.progress.ProgressUtils; import org.apache.sling.ide.filter.Filter; import org.apache.sling.ide.filter.FilterResult; import org.apache.sling.ide.filter.IgnoredResources; import org.apache.sling.ide.log.Logger; import org.apache.sling.ide.serialization.SerializationData; import org.apache.sling.ide.serialization.SerializationDataBuilder; import org.apache.sling.ide.serialization.SerializationException; import org.apache.sling.ide.serialization.SerializationKind; import org.apache.sling.ide.serialization.SerializationKindManager; import org.apache.sling.ide.serialization.SerializationManager; import org.apache.sling.ide.transport.Command; import org.apache.sling.ide.transport.Repository; import org.apache.sling.ide.transport.RepositoryException; import org.apache.sling.ide.transport.ResourceProxy; import org.apache.sling.ide.transport.Result; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceVisitor; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.wst.server.core.IServer; // intentionally does not implement IRunnableWithProgress to cut dependency on JFace public class ImportRepositoryContentAction { private final IServer server; private final IPath projectRelativePath; private final IProject project; private final Logger logger; private SerializationManager serializationManager; private SerializationDataBuilder builder; private IgnoredResources ignoredResources; private IProgressMonitor monitor; private Repository repository; private Filter filter; private File contentSyncRoot; private IFolder contentSyncRootDir; private Set<IResource> currentResources; private IPath repositoryImportRoot; /** * @param server * @param projectRelativePath * @param project * @throws SerializationException */ public ImportRepositoryContentAction(IServer server, IPath projectRelativePath, IProject project, SerializationManager serializationManager) throws SerializationException { this.logger = Activator.getDefault().getPluginLogger(); this.server = server; this.projectRelativePath = projectRelativePath; this.project = project; this.serializationManager = serializationManager; this.ignoredResources = new IgnoredResources(); this.currentResources = new HashSet<>(); } public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException, SerializationException, CoreException { // TODO: We should try to make this give 'nice' progress feedback (aka here's what I'm processing) monitor.beginTask("Repository import", IProgressMonitor.UNKNOWN); this.monitor = monitor; repository = ServerUtil.getConnectedRepository(server, monitor); this.builder = serializationManager.newBuilder( repository, ProjectUtil.getSyncDirectoryFile(project)); SerializationKindManager skm; try { skm = new SerializationKindManager(); skm.init(repository); } catch (RepositoryException e1) { throw new InvocationTargetException(e1); } filter = ProjectUtil.loadFilter(project); ProgressUtils.advance(monitor, 1); try { contentSyncRootDir = ProjectUtil.getSyncDirectory(project); repositoryImportRoot = projectRelativePath .makeRelativeTo(contentSyncRootDir.getProjectRelativePath()) .makeAbsolute(); contentSyncRoot = ProjectUtil.getSyncDirectoryFullPath(project).toFile(); readVltIgnoresNotUnderImportRoot(contentSyncRootDir, repositoryImportRoot); ProgressUtils.advance(monitor, 1); Activator .getDefault() .getPluginLogger() .trace("Starting import; repository start point is {0}, workspace start point is {1}", repositoryImportRoot, projectRelativePath); recordNotIgnoredResources(); ProgressUtils.advance(monitor, 1); crawlChildrenAndImport(repositoryImportRoot.toPortableString()); removeNotIgnoredAndNotUpdatedResources(new NullProgressMonitor()); ProgressUtils.advance(monitor, 1); } catch (OperationCanceledException e) { throw e; } catch (Exception e) { throw new InvocationTargetException(e); } finally { if (builder!=null) { builder.destroy(); builder = null; } monitor.done(); } } private void readVltIgnoresNotUnderImportRoot(IFolder syncDir, IPath repositoryImportRoot) throws IOException, CoreException { IFolder current = syncDir; for (int i = 0; i < repositoryImportRoot.segmentCount(); i++) { IPath repoPath = current.getProjectRelativePath().makeRelativeTo(syncDir.getProjectRelativePath()) .makeAbsolute(); parseIgnoreFiles(current, repoPath.toPortableString()); current = (IFolder) current.findMember(repositoryImportRoot.segment(i)); } } private void recordNotIgnoredResources() throws CoreException { final ResourceChangeCommandFactory rccf = new ResourceChangeCommandFactory(serializationManager, Activator.getDefault().getPreferences().getIgnoredFileNamesForSync()); IResource importStartingPoint = contentSyncRootDir.findMember(repositoryImportRoot); if (importStartingPoint == null) { return; } importStartingPoint.accept(new IResourceVisitor() { @Override public boolean visit(IResource resource) throws CoreException { try { ResourceAndInfo rai = rccf.buildResourceAndInfo(resource, repository); if (rai == null) { // can be a prerequisite return true; } String repositoryPath = rai.getResource().getPath(); FilterResult filterResult = filter.filter(repositoryPath); if (ignoredResources.isIgnored(repositoryPath)) { return false; } if (filterResult == FilterResult.ALLOW) { currentResources.add(resource); return true; } return false; } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed reading current project's resources", e)); } } }); logger.trace("Found {0} not ignored local resources", currentResources.size()); } private void removeNotIgnoredAndNotUpdatedResources(IProgressMonitor monitor) throws CoreException { logger.trace("Found {0} resources to clean up", currentResources.size()); for (IResource resource : currentResources) { if (resource.exists()) { logger.trace("Deleting {0}", resource); resource.delete(true, monitor); } } } /** * Crawls the repository and recursively imports founds resources * @param path the current path to import from * @param tracer * @throws JSONException * @throws RepositoryException * @throws CoreException * @throws IOException */ // TODO: This probably should be pushed into the service layer private void crawlChildrenAndImport(String path) throws RepositoryException, CoreException, IOException, SerializationException { logger.trace("crawlChildrenAndImport({0}, {1}, {2}, {3}", repository, path, project, projectRelativePath); ResourceProxy resource = executeCommand(repository.newListChildrenNodeCommand(path)); SerializationData serializationData = builder.buildSerializationData(contentSyncRoot, resource); logger.trace("For resource at path {0} got serialization data {1}", resource.getPath(), serializationData); final List<ResourceProxy> resourceChildren = new LinkedList<>(resource.getChildren()); if (serializationData != null) { IPath serializationFolderPath = contentSyncRootDir.getProjectRelativePath().append( serializationData.getFolderPath()); switch (serializationData.getSerializationKind()) { case FILE: { byte[] contents = executeCommand(repository.newGetNodeCommand(path)); createFile(project, getPathForPlainFileNode(resource, serializationFolderPath), contents); if (serializationData.hasContents()) { createFolder(project, serializationFolderPath); createFile(project, serializationFolderPath.append(serializationData.getFileName()), serializationData.getContents()); // special processing for nt:resource nodes for (Iterator<ResourceProxy> it = resourceChildren.iterator(); it.hasNext();) { ResourceProxy child = it.next(); if (Repository.NT_RESOURCE.equals(child.getProperties().get(Repository.JCR_PRIMARY_TYPE))) { ResourceProxy reloadedChildResource = executeCommand(repository .newListChildrenNodeCommand(child.getPath())); logger.trace( "Skipping direct handling of {0} node at {1} ; will additionally handle {2} direct children", Repository.NT_RESOURCE, child.getPath(), reloadedChildResource.getChildren() .size()); if (reloadedChildResource.getChildren().size() != 0) { String pathName = Text.getName(reloadedChildResource.getPath()); pathName = serializationManager.getOsPath(pathName); createFolder(project, serializationFolderPath.append(pathName)); // 2. recursively handle all resources for (ResourceProxy grandChild : reloadedChildResource.getChildren()) { crawlChildrenAndImport(grandChild.getPath()); } } it.remove(); break; } } } break; } case FOLDER: case METADATA_PARTIAL: { IFolder folder = createFolder(project, serializationFolderPath); parseIgnoreFiles(folder, path); if (serializationData.hasContents()) { createFile(project, serializationFolderPath.append(serializationData.getFileName()), serializationData.getContents()); } break; } case METADATA_FULL: { if (serializationData.hasContents()) { createFile(project, serializationFolderPath.append(serializationData.getFileName()), serializationData.getContents()); } break; } } logger.trace("Resource at {0} has children: {1}", resource.getPath(), resourceChildren); if (serializationData.getSerializationKind() == SerializationKind.METADATA_FULL) { return; } } else { logger.trace("No serialization data found for {0}", resource.getPath()); } ProgressUtils.advance(monitor, 1); for (ResourceProxy child : resourceChildren) { if (ignoredResources.isIgnored(child.getPath())) { continue; } if (filter != null) { FilterResult filterResult = filter.filter(child.getPath()); if (filterResult == FilterResult.DENY) { continue; } } crawlChildrenAndImport(child.getPath()); } } /** * Returns the path for serializing the nt:resource data of a nt:file node * * <p> * The path will be one level above the <tt>serializationFolderPath</tt>, and the name will be the last path segment * of the resource. * </p> * * @param resource The resource * @param serializationFolderPath the folder where the serialization data should be stored * @return the path for the plain file node */ private IPath getPathForPlainFileNode(ResourceProxy resource, IPath serializationFolderPath) { // TODO - can we just use the serializationFolderPath ? String name = serializationManager.getOsPath(Text.getName(resource.getPath())); return serializationFolderPath.removeLastSegments(1).append(name); } private void parseIgnoreFiles(IFolder folder, String path) throws IOException, CoreException { // TODO - the parsing should be extracted IResource vltIgnore = folder.findMember(".vltignore"); if (vltIgnore != null && vltIgnore instanceof IFile) { logger.trace("Found ignore file at {0}", vltIgnore.getFullPath()); try (InputStream contents = ((IFile) vltIgnore).getContents()) { List<String> ignoreLines = IOUtils.readLines(contents); for (String ignoreLine : ignoreLines) { logger.trace("Registering ignore rule {0}:{1}", path, ignoreLine); ignoredResources.registerRegExpIgnoreRule(path, ignoreLine); } } } } private <T> T executeCommand(Command<T> command) throws RepositoryException { Result<T> result = command.execute(); return result.get(); } private IFolder createFolder(IProject project, IPath destinationPath) throws CoreException { IFolder destinationFolder = project.getFolder(destinationPath); if (!destinationFolder.exists()) { logger.trace("Creating folder {0}", destinationFolder.getFullPath()); createParents(destinationFolder.getParent()); destinationFolder.create(true, true, null /* TODO progress monitor */); } destinationFolder.setSessionProperty(ResourceUtil.QN_IMPORT_MODIFICATION_TIMESTAMP, destinationFolder.getModificationStamp()); removeTouchedResource(destinationFolder); return destinationFolder; } private void createParents(IContainer container) throws CoreException { if (container.exists() || container.getType() != IResource.FOLDER) { return; } createParents(container.getParent()); createFolder(container.getProject(), container.getProjectRelativePath()); } private void removeTouchedResource(IResource resource) { IResource current = resource; do { currentResources.remove(current); } while ((current = current.getParent()) != null); } private void createFile(IProject project, IPath path, byte[] node) throws CoreException { if (node==null) { throw new IllegalArgumentException("node must not be null"); } IFile destinationFile = project.getFile(path); logger.trace("Writing content file at {0}", path); if (destinationFile.exists()) { /* TODO progress monitor */ destinationFile.setContents(new ByteArrayInputStream(node), IResource.KEEP_HISTORY, null); } else { /* TODO progress monitor */ if (!destinationFile.getParent().exists()) { createParents(destinationFile.getParent()); } destinationFile.create(new ByteArrayInputStream(node), true, null); } removeTouchedResource(destinationFile); destinationFile.setSessionProperty(ResourceUtil.QN_IMPORT_MODIFICATION_TIMESTAMP, destinationFile.getModificationStamp()); } }