/*
* 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.core.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.sling.ide.eclipse.core.ProjectUtil;
import org.apache.sling.ide.eclipse.core.ResourceUtil;
import org.apache.sling.ide.filter.Filter;
import org.apache.sling.ide.filter.FilterResult;
import org.apache.sling.ide.log.Logger;
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.CommandContext;
import org.apache.sling.ide.transport.FileInfo;
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.util.PathUtil;
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.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.ui.statushandlers.StatusManager;
/**
* The <tt>ResourceChangeCommandFactory</tt> creates new {@link #Command commands} correspoding to resource addition,
* change, or removal
*
*/
public class ResourceChangeCommandFactory {
private final Set<String> ignoredFileNames;
private final SerializationManager serializationManager;
public ResourceChangeCommandFactory(SerializationManager serializationManager, Set<String> ignoredFileNames) {
this.serializationManager = serializationManager;
this.ignoredFileNames = ignoredFileNames;
}
public Command<?> newCommandForAddedOrUpdated(Repository repository, IResource addedOrUpdated) throws CoreException {
try {
return addFileCommand(repository, addedOrUpdated);
} catch (IOException e) {
throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed updating " + addedOrUpdated,
e));
}
}
private Command<?> addFileCommand(Repository repository, IResource resource) throws CoreException, IOException {
ResourceAndInfo rai = buildResourceAndInfo(resource, repository);
if (rai == null) {
return null;
}
CommandContext context = new CommandContext(ProjectUtil.loadFilter(resource.getProject()));
if (rai.isOnlyWhenMissing()) {
return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource(),
Repository.CommandExecutionFlag.CREATE_ONLY_WHEN_MISSING);
}
return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource());
}
/**
* Convenience method which builds a <tt>ResourceAndInfo</tt> info for a specific <tt>IResource</tt>
*
* @param resource the resource to process
* @param repository the repository, used to extract serialization information for different resource types
* @return the build object, or null if one could not be built
* @throws CoreException
* @throws SerializationException
* @throws IOException
*/
public ResourceAndInfo buildResourceAndInfo(IResource resource, Repository repository) throws CoreException,
IOException {
if (ignoredFileNames.contains(resource.getName())) {
return null;
}
Long modificationTimestamp = (Long) resource.getSessionProperty(ResourceUtil.QN_IMPORT_MODIFICATION_TIMESTAMP);
if (modificationTimestamp != null && modificationTimestamp >= resource.getModificationStamp()) {
Activator.getDefault().getPluginLogger()
.trace("Change for resource {0} ignored as the import timestamp {1} >= modification timestamp {2}",
resource, modificationTimestamp, resource.getModificationStamp());
return null;
}
if (resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS)) {
Activator.getDefault().getPluginLogger().trace("Skipping team-private resource {0}", resource);
return null;
}
FileInfo info = createFileInfo(resource);
Activator.getDefault().getPluginLogger().trace("For {0} built fileInfo {1}", resource, info);
File syncDirectoryAsFile = ProjectUtil.getSyncDirectoryFullPath(resource.getProject()).toFile();
IFolder syncDirectory = ProjectUtil.getSyncDirectory(resource.getProject());
Filter filter = ProjectUtil.loadFilter(resource.getProject());
ResourceProxy resourceProxy = null;
if (serializationManager.isSerializationFile(resource.getLocation().toOSString())) {
IFile file = (IFile) resource;
try (InputStream contents = file.getContents()) {
String resourceLocation = file.getFullPath().makeRelativeTo(syncDirectory.getFullPath())
.toPortableString();
resourceProxy = serializationManager.readSerializationData(resourceLocation, contents);
normaliseResourceChildren(file, resourceProxy, syncDirectory, repository);
// TODO - not sure if this 100% correct, but we definitely should not refer to the FileInfo as the
// .serialization file, since for nt:file/nt:resource nodes this will overwrite the file contents
String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
if (Repository.NT_FILE.equals(primaryType)) {
// TODO move logic to serializationManager
File locationFile = new File(info.getLocation());
String locationFileParent = locationFile.getParent();
int endIndex = locationFileParent.length() - ".dir".length();
File actualFile = new File(locationFileParent.substring(0, endIndex));
String newLocation = actualFile.getAbsolutePath();
String newName = actualFile.getName();
String newRelativeLocation = actualFile.getAbsolutePath().substring(
syncDirectoryAsFile.getAbsolutePath().length());
info = new FileInfo(newLocation, newRelativeLocation, newName);
Activator.getDefault().getPluginLogger()
.trace("Adjusted original location from {0} to {1}", resourceLocation, newLocation);
}
} catch (IOException e) {
Status s = new Status(Status.WARNING, Activator.PLUGIN_ID, "Failed reading file at "
+ resource.getFullPath(), e);
StatusManager.getManager().handle(s, StatusManager.LOG | StatusManager.SHOW);
return null;
}
} else {
// TODO - move logic to serializationManager
// possible .dir serialization holder
if (resource.getType() == IResource.FOLDER && resource.getName().endsWith(".dir")) {
IFolder folder = (IFolder) resource;
IResource contentXml = folder.findMember(".content.xml");
// .dir serialization holder ; nothing to process here, the .content.xml will trigger the actual work
if (contentXml != null && contentXml.exists()
&& serializationManager.isSerializationFile(contentXml.getLocation().toOSString())) {
return null;
}
}
resourceProxy = buildResourceProxyForPlainFileOrFolder(resource, syncDirectory, repository);
}
FilterResult filterResult = getFilterResult(resource, resourceProxy, filter);
switch (filterResult) {
case ALLOW:
return new ResourceAndInfo(resourceProxy, info);
case PREREQUISITE:
// never try to 'create' the root node, we assume it exists
if (!resourceProxy.getPath().equals("/")) {
// we don't explicitly set the primary type, which will allow the the repository to choose the best
// suited one ( typically nt:unstructured )
return new ResourceAndInfo(new ResourceProxy(resourceProxy.getPath()), null, true);
}
case DENY: // falls through
default:
return null;
}
}
private FileInfo createFileInfo(IResource resource) throws CoreException {
if (resource.getType() != IResource.FILE) {
return null;
}
IProject project = resource.getProject();
IFolder syncFolder = project.getFolder(ProjectUtil.getSyncDirectoryValue(project));
IPath relativePath = resource.getFullPath().makeRelativeTo(syncFolder.getFullPath());
FileInfo info = new FileInfo(resource.getLocation().toOSString(), relativePath.toOSString(), resource.getName());
Activator.getDefault().getPluginLogger().trace("For {0} built fileInfo {1}", resource, info);
return info;
}
/**
* Gets the filter result for a resource/resource proxy combination
*
* <p>
* The resourceProxy may be null, typically when a resource is already deleted.
*
* <p>
* In case the filter is {@code null} no resource should be added, i.e. {@link FilterResult#DENY} is returned
*
* @param resource the resource to filter for, must not be <code>null</code>
* @param resourceProxy the resource proxy to filter for, possibly <code>null</code>
* @param filter the filter to use, possibly <tt>null</tt>
* @return the filtering result, never <code>null</code>
*/
private FilterResult getFilterResult(IResource resource, ResourceProxy resourceProxy, Filter filter) {
if (filter == null) {
return FilterResult.DENY;
}
File contentSyncRoot = ProjectUtil.getSyncDirectoryFile(resource.getProject());
String repositoryPath = resourceProxy != null ? resourceProxy.getPath() : getRepositoryPathForDeletedResource(
resource, contentSyncRoot);
FilterResult filterResult = filter.filter(repositoryPath);
Activator.getDefault().getPluginLogger().trace("Filter result for {0} for {1}", repositoryPath, filterResult);
return filterResult;
}
private String getRepositoryPathForDeletedResource(IResource resource, File contentSyncRoot) {
IFolder syncFolder = ProjectUtil.getSyncDirectory(resource.getProject());
IPath relativePath = resource.getFullPath().makeRelativeTo(syncFolder.getFullPath());
String absFilePath = new File(contentSyncRoot, relativePath.toOSString()).getAbsolutePath();
String filePath = serializationManager.getBaseResourcePath(absFilePath);
IPath osPath = Path.fromOSString(filePath);
String repositoryPath = serializationManager.getRepositoryPath(osPath.makeRelativeTo(syncFolder.getLocation())
.makeAbsolute().toPortableString());
Activator.getDefault().getPluginLogger()
.trace("Repository path for deleted resource {0} is {1}", resource, repositoryPath);
return repositoryPath;
}
private ResourceProxy buildResourceProxyForPlainFileOrFolder(IResource changedResource, IFolder syncDirectory,
Repository repository)
throws CoreException, IOException {
SerializationKind serializationKind;
String fallbackNodeType;
if (changedResource.getType() == IResource.FILE) {
serializationKind = SerializationKind.FILE;
fallbackNodeType = Repository.NT_FILE;
} else { // i.e. IResource.FOLDER
serializationKind = SerializationKind.FOLDER;
fallbackNodeType = Repository.NT_FOLDER;
}
String resourceLocation = '/' + changedResource.getFullPath().makeRelativeTo(syncDirectory.getFullPath())
.toPortableString();
IPath serializationFilePath = Path.fromOSString(serializationManager.getSerializationFilePath(
resourceLocation, serializationKind));
IResource serializationResource = syncDirectory.findMember(serializationFilePath);
if (serializationResource == null && changedResource.getType() == IResource.FOLDER) {
ResourceProxy dataFromCoveringParent = findSerializationDataFromCoveringParent(changedResource,
syncDirectory, resourceLocation, serializationFilePath);
if (dataFromCoveringParent != null) {
return dataFromCoveringParent;
}
}
return buildResourceProxy(resourceLocation, serializationResource, syncDirectory, fallbackNodeType, repository);
}
/**
* Tries to find serialization data from a resource in a covering parent
*
* <p>
* If the serialization resource is null, it's valid to look for a serialization resource higher in the filesystem,
* given that the found serialization resource covers this resource
*
* @param changedResource the resource which has changed
* @param syncDirectory the content sync directory for the resource's project
* @param resourceLocation the resource location relative to the sync directory
* @param serializationFilePath the location
* @return a <tt>ResourceProxy</tt> if there is a covering parent, or null is there is not
* @throws CoreException
* @throws IOException
*/
private ResourceProxy findSerializationDataFromCoveringParent(IResource changedResource, IFolder syncDirectory,
String resourceLocation, IPath serializationFilePath) throws CoreException, IOException {
// TODO - this too should be abstracted in the service layer, rather than in the Eclipse-specific code
Logger logger = Activator.getDefault().getPluginLogger();
logger.trace("Found plain nt:folder candidate at {0}, trying to find a covering resource for it",
changedResource.getProjectRelativePath());
// don't use isRoot() to prevent infinite loop when the final path is '//'
while (serializationFilePath.segmentCount() != 0) {
serializationFilePath = serializationFilePath.removeLastSegments(1);
IFolder folderWithPossibleSerializationFile = syncDirectory.getFolder(serializationFilePath);
if (folderWithPossibleSerializationFile == null) {
logger.trace("No folder found at {0}, moving up to the next level", serializationFilePath);
continue;
}
// it's safe to use a specific SerializationKind since this scenario is only valid for METADATA_PARTIAL
// coverage
String possibleSerializationFilePath = serializationManager.getSerializationFilePath(
((IFolder) folderWithPossibleSerializationFile).getLocation().toOSString(),
SerializationKind.METADATA_PARTIAL);
logger.trace("Looking for serialization data in {0}", possibleSerializationFilePath);
if (serializationManager.isSerializationFile(possibleSerializationFilePath)) {
IPath parentSerializationFilePath = Path.fromOSString(possibleSerializationFilePath).makeRelativeTo(
syncDirectory.getLocation());
IFile possibleSerializationFile = syncDirectory.getFile(parentSerializationFilePath);
if (!possibleSerializationFile.exists()) {
logger.trace("Potential serialization data file {0} does not exist, moving up to the next level",
possibleSerializationFile.getFullPath());
continue;
}
ResourceProxy serializationData;
try (InputStream contents = possibleSerializationFile.getContents()) {
serializationData = serializationManager.readSerializationData(
parentSerializationFilePath.toPortableString(), contents);
}
String repositoryPath = serializationManager.getRepositoryPath(resourceLocation);
String potentialPath = serializationData.getPath();
boolean covered = serializationData.covers(repositoryPath);
logger.trace(
"Found possible serialization data at {0}. Resource :{1} ; our resource: {2}. Covered: {3}",
parentSerializationFilePath, potentialPath, repositoryPath, covered);
// note what we don't need to normalize the children here since this resource's data is covered by
// another resource
if (covered) {
return serializationData.getChild(repositoryPath);
}
break;
}
}
return null;
}
private ResourceProxy buildResourceProxy(String resourceLocation, IResource serializationResource,
IFolder syncDirectory, String fallbackPrimaryType, Repository repository) throws CoreException, IOException {
if (serializationResource instanceof IFile) {
IFile serializationFile = (IFile) serializationResource;
try (InputStream contents = serializationFile.getContents() ) {
String serializationFilePath = serializationResource.getFullPath()
.makeRelativeTo(syncDirectory.getFullPath()).toPortableString();
ResourceProxy resourceProxy = serializationManager.readSerializationData(serializationFilePath, contents);
normaliseResourceChildren(serializationFile, resourceProxy, syncDirectory, repository);
return resourceProxy;
}
}
return new ResourceProxy(serializationManager.getRepositoryPath(resourceLocation), Collections.singletonMap(
Repository.JCR_PRIMARY_TYPE, (Object) fallbackPrimaryType));
}
/**
* Normalises the of the specified <tt>resourceProxy</tt> by comparing the serialization data and the filesystem
* data
*
* @param serializationFile the file which contains the serialization data
* @param resourceProxy the resource proxy
* @param syncDirectory the sync directory
* @param repository TODO
* @throws CoreException
*/
private void normaliseResourceChildren(IFile serializationFile, ResourceProxy resourceProxy, IFolder syncDirectory,
Repository repository) throws CoreException {
// TODO - this logic should be moved to the serializationManager
try {
SerializationKindManager skm = new SerializationKindManager();
skm.init(repository);
String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
List<String> mixinTypesList = getMixinTypes(resourceProxy);
SerializationKind serializationKind = skm.getSerializationKind(primaryType, mixinTypesList);
if (serializationKind == SerializationKind.METADATA_FULL) {
return;
}
} catch (RepositoryException e) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed creating a "
+ SerializationDataBuilder.class.getName(), e));
}
IPath serializationDirectoryPath = serializationFile.getFullPath().removeLastSegments(1);
Iterator<ResourceProxy> childIterator = resourceProxy.getChildren().iterator();
Map<String, IResource> extraChildResources = new HashMap<>();
for (IResource member : serializationFile.getParent().members()) {
if (member.equals(serializationFile)) {
continue;
}
extraChildResources.put(member.getName(), member);
}
while (childIterator.hasNext()) {
ResourceProxy child = childIterator.next();
String childName = PathUtil.getName(child.getPath());
String osPath = serializationManager.getOsPath(childName);
// covered children might have a FS representation, depending on their child nodes, so
// accept a directory which maps to their name
extraChildResources.remove(osPath);
// covered children do not need a filesystem representation
if (resourceProxy.covers(child.getPath())) {
continue;
}
IPath childPath = serializationDirectoryPath.append(osPath);
IResource childResource = ResourcesPlugin.getWorkspace().getRoot().findMember(childPath);
if (childResource == null) {
Activator.getDefault().getPluginLogger()
.trace("For resource at with serialization data {0} the serialized child resource at {1} does not exist in the filesystem and will be ignored",
serializationFile, childPath);
childIterator.remove();
}
}
for ( IResource extraChildResource : extraChildResources.values()) {
IPath extraChildResourcePath = extraChildResource.getFullPath()
.makeRelativeTo(syncDirectory.getFullPath()).makeAbsolute();
resourceProxy.addChild(new ResourceProxy(serializationManager
.getRepositoryPath(extraChildResourcePath.toPortableString())));
Activator.getDefault().getPluginLogger()
.trace("For resource at with serialization data {0} the found a child resource at {1} which is not listed in the serialized child resources and will be added",
serializationFile, extraChildResource);
}
}
private List<String> getMixinTypes(ResourceProxy resourceProxy) {
Object mixinTypesProp = resourceProxy.getProperties().get(Repository.JCR_MIXIN_TYPES);
if (mixinTypesProp == null) {
return Collections.emptyList();
}
if (mixinTypesProp instanceof String) {
return Collections.singletonList((String) mixinTypesProp);
}
return Arrays.asList((String[]) mixinTypesProp);
}
public Command<?> newCommandForRemovedResources(Repository repository, IResource removed) throws CoreException {
try {
return removeFileCommand(repository, removed);
} catch (IOException e) {
throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed removing" + removed, e));
}
}
private Command<?> removeFileCommand(Repository repository, IResource resource) throws CoreException, IOException {
if (resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS)) {
Activator.getDefault().getPluginLogger().trace("Skipping team-private resource {0}", resource);
return null;
}
if (ignoredFileNames.contains(resource.getName())) {
return null;
}
IFolder syncDirectory = ProjectUtil.getSyncDirectory(resource.getProject());
Filter filter = ProjectUtil.loadFilter(syncDirectory.getProject());
FilterResult filterResult = getFilterResult(resource, null, filter);
if (filterResult == FilterResult.DENY || filterResult == FilterResult.PREREQUISITE) {
return null;
}
String resourceLocation = getRepositoryPathForDeletedResource(resource,
ProjectUtil.getSyncDirectoryFile(resource.getProject()));
// verify whether a resource being deleted does not signal that the content structure
// was rearranged under a covering parent aggregate
IPath serializationFilePath = Path.fromOSString(serializationManager.getSerializationFilePath(resourceLocation,
SerializationKind.FOLDER));
ResourceProxy coveringParentData = findSerializationDataFromCoveringParent(resource, syncDirectory,
resourceLocation, serializationFilePath);
if (coveringParentData != null) {
Activator
.getDefault()
.getPluginLogger()
.trace("Found covering resource data ( repository path = {0} ) for resource at {1}, skipping deletion and performing an update instead",
coveringParentData.getPath(), resource.getFullPath());
FileInfo info = createFileInfo(resource);
return repository.newAddOrUpdateNodeCommand(new CommandContext(filter), info, coveringParentData);
}
return repository.newDeleteNodeCommand(serializationManager.getRepositoryPath(resourceLocation));
}
public Command<Void> newReorderChildNodesCommand(Repository repository, IResource res) throws CoreException {
try {
ResourceAndInfo rai = buildResourceAndInfo(res, repository);
if (rai == null || rai.isOnlyWhenMissing()) {
return null;
}
return repository.newReorderChildNodesCommand(rai.getResource());
} catch (IOException e) {
throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed reordering child nodes for "
+ res, e));
}
}
}