/*******************************************************************************
* Copyright (c) 2009 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Zend Technologies
*******************************************************************************/
package org.eclipse.php.internal.debug.core.sourcelookup;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.internal.filesystem.local.LocalFile;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.sourcelookup.AbstractSourceLookupParticipant;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.debug.internal.core.sourcelookup.SourceLookupMessages;
import org.eclipse.dltk.core.IArchiveEntry;
import org.eclipse.dltk.core.IModelStatusConstants;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.environment.EnvironmentPathUtils;
import org.eclipse.dltk.core.internal.environment.LocalEnvironment;
import org.eclipse.dltk.core.search.IDLTKSearchScope;
import org.eclipse.dltk.core.search.SearchEngine;
import org.eclipse.dltk.internal.core.Openable;
import org.eclipse.dltk.internal.core.util.HandleFactory;
import org.eclipse.php.debug.core.debugger.parameters.IDebugParametersKeys;
import org.eclipse.php.internal.core.PHPLanguageToolkit;
import org.eclipse.php.internal.core.PHPSymbolicLinksCache;
import org.eclipse.php.internal.core.phar.PharArchiveFile;
import org.eclipse.php.internal.core.phar.PharPath;
import org.eclipse.php.internal.core.util.FileUtils;
import org.eclipse.php.internal.core.util.SyncObject;
import org.eclipse.php.internal.debug.core.IPHPDebugConstants;
import org.eclipse.php.internal.debug.core.PHPDebugPlugin;
import org.eclipse.php.internal.debug.core.launching.PHPLaunchUtilities;
import org.eclipse.php.internal.debug.core.xdebug.dbgp.model.DBGpStackFrame;
import org.eclipse.php.internal.debug.core.zend.communication.IRemoteFileContentRequestor;
import org.eclipse.php.internal.debug.core.zend.communication.RemoteFileStorage;
import org.eclipse.php.internal.debug.core.zend.debugger.RemoteDebugger;
import org.eclipse.php.internal.debug.core.zend.model.PHPDebugTarget;
import org.eclipse.php.internal.debug.core.zend.model.PHPStackFrame;
/**
* The PHP source lookup participant knows how to translate a PHP stack frame
* into a source file name
*/
public class PHPSourceLookupParticipant extends AbstractSourceLookupParticipant {
private LinkSubjectFileFinder linkSubjectFileFinder = new LinkSubjectFileFinder();
private Map<String, RemoteFileStorage> remoteStorageCache;
public PHPSourceLookupParticipant() {
remoteStorageCache = new HashMap<String, RemoteFileStorage>();
}
/**
* Helper class for finding workspace files that might point to provided
* source location by means of being a linked/symbolic link files or be a
* part of the chained structure that might consist of linked/symbolic link
* directories/files.
*/
private final class LinkSubjectFileFinder {
public Object find(final String sourceLocation, Object element) {
final LinkedList<IResource> matches = new LinkedList<IResource>();
final String sourceFileName = (new Path(sourceLocation)).lastSegment();
IProject project = null;
if (element instanceof IStackFrame) {
IDebugTarget target = ((IStackFrame) element).getDebugTarget();
project = PHPLaunchUtilities.getProject(target);
}
final IResource scope = project != null ? project : ResourcesPlugin.getWorkspace().getRoot();
try {
scope.accept(new IResourceVisitor() {
@Override
public boolean visit(IResource resource) throws CoreException {
try {
// Retreat if we already have a match
if (!matches.isEmpty()) {
return false;
}
// We are looking for files only
if (resource.getType() != IResource.FILE) {
return true;
}
/*
* The goal of this pre-check condition is to reduce
* the amount of files to be checked by NIO
* (comparing with NIO can be time consuming ).
*/
if (resource.getName().equals(sourceFileName) || resource.isLinked()
|| PHPSymbolicLinksCache.INSTANCE.isSymbolicLink(resource)) {
if (resource.getLocation() != null
&& FileUtils.isSameFile(sourceLocation, resource.getLocation().toOSString())) {
matches.add(resource);
}
}
} catch (IOException e) {
PHPDebugPlugin.log(e);
}
return true;
}
});
} catch (CoreException e) {
PHPDebugPlugin.log(e);
}
return !matches.isEmpty() ? matches.getFirst() : null;
}
}
private static final class ExternalEntryFile extends PlatformObject implements IStorage {
private String fileName;
private IArchiveEntry entry;
private PharArchiveFile archiveFile;
public ExternalEntryFile(String fileName, PharArchiveFile archiveFile, IArchiveEntry entry) {
this.fileName = fileName;
this.entry = entry;
this.archiveFile = archiveFile;
}
public InputStream getContents() throws CoreException {
try {
return this.archiveFile.getInputStream(entry);
} catch (IOException e) {
throw new ModelException(e, IModelStatusConstants.IO_EXCEPTION);
}
}
/**
* @see IStorage#getFullPath
*/
public IPath getFullPath() {
return new Path(this.fileName);
}
/**
* @see IStorage#getName
*/
public String getName() {
return this.entry.getName();
}
/**
* @see IStorage#isReadOnly()
*/
public boolean isReadOnly() {
return true;
}
/**
* @see IStorage#isReadOnly()
*/
public String toString() {
return "ExternalEntryFile[" + this.fileName + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ExternalEntryFile))
return false;
ExternalEntryFile other = (ExternalEntryFile) obj;
if (!fileName.toLowerCase().equals(other.fileName.toLowerCase()))
return false;
return true;
}
@Override
public int hashCode() {
return fileName.toLowerCase().hashCode();
}
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.debug.internal.core.sourcelookup.ISourceLookupParticipant
* #getSourceName(java.lang.Object)
*/
public String getSourceName(Object object) throws CoreException {
if (object instanceof PHPStackFrame) {
return ((PHPStackFrame) object).getSourceName();
}
if (object instanceof DBGpStackFrame) {
String sourceName = ((DBGpStackFrame) object).getSourceName();
if (sourceName == null) {
sourceName = ((DBGpStackFrame) object).getQualifiedFile();
IPath path = new Path(sourceName);
sourceName = path.lastSegment();
}
return sourceName;
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.sourcelookup.AbstractSourceLookupParticipant#
* findSourceElements(java.lang.Object)
*/
public Object[] findSourceElements(Object object) throws CoreException {
try {
// Check if the source should be retrieved from the server.
// If so, get it.
Object[] sourceElements = getRemoteSourceElements(object, false);
if (sourceElements != EMPTY) {
return sourceElements;
}
} catch (CoreException ce) {
}
// In case that the source should be obtained from the local client, or,
// in case that the source was
// not found on the client, perform the code below.
try {
// Try to get it locally.
Object[] result = getLocalSourceElements(object);
if (result != EMPTY) {
return result;
}
} catch (CoreException ce) {
}
List<Object> results = new ArrayList<Object>();
CoreException single = null;
MultiStatus multiStatus = null;
String name = getSourceName(object);
if (name != null) {
ISourceContainer[] containers = getSourceContainers();
for (int i = 0; i < containers.length; i++) {
try {
ISourceContainer container = getDelegateContainer(containers[i]);
if (container != null) {
Object[] objects = container.findSourceElements(name);
if (objects.length > 0) {
if (isFindDuplicates()) {
for (int j = 0; j < objects.length; j++) {
results.add(objects[j]);
}
} else {
if (objects.length == 1) {
return objects;
}
return new Object[] { objects[0] };
}
} else {
// Try to get the file remotely from the server
return getRemoteSourceElements(object, true);
}
}
} catch (CoreException e) {
if (single == null) {
single = e;
} else if (multiStatus == null) {
multiStatus = new MultiStatus(PHPDebugPlugin.ID, PHPDebugPlugin.INTERNAL_ERROR,
new IStatus[] { single.getStatus() }, SourceLookupMessages.Source_Lookup_Error, null);
multiStatus.add(e.getStatus());
} else {
multiStatus.add(e.getStatus());
}
}
}
}
if (results.isEmpty()) {
if (multiStatus != null) {
throw new CoreException(multiStatus);
} else if (single != null) {
throw single;
}
return EMPTY;
}
return results.toArray();
}
@Override
public void dispose() {
super.dispose();
remoteStorageCache.clear();
remoteStorageCache = null;
}
private Object[] getLocalSourceElements(Object object) throws CoreException {
Object[] sourceElements = EMPTY;
try {
sourceElements = super.findSourceElements(object);
} catch (CoreException e) {
// Check if the lookup failed because the source is outside the
// workspace.
}
if (sourceElements == EMPTY) {
// If the lookup returned an empty elements array, check if the
// source is outside the workspace.
String sourceFilePath = null;
if (object instanceof PHPStackFrame) {
sourceFilePath = ((PHPStackFrame) object).getSourceName();
} else if (object instanceof DBGpStackFrame) {
sourceFilePath = ((DBGpStackFrame) object).getQualifiedFile();
}
if (sourceFilePath != null) {
// Check if we have it in DLTK model
HandleFactory handleFactory = new HandleFactory();
IDLTKSearchScope scope = SearchEngine.createWorkspaceScope(PHPLanguageToolkit.getDefault());
IPath localPath = EnvironmentPathUtils.getFile(LocalEnvironment.getInstance(), new Path(sourceFilePath))
.getFullPath();
Openable openable = handleFactory.createOpenable(localPath.toString(), scope);
if (openable instanceof IStorage) {
return new Object[] { openable };
}
// Check if it is not a file from PHAR
final PharPath pharPath = PharPath.getPharPath(new Path(sourceFilePath));
if (pharPath != null && !pharPath.getFile().isEmpty()) {
try {
final PharArchiveFile archiveFile = new PharArchiveFile(pharPath.getPharName());
final IArchiveEntry entry = archiveFile.getArchiveEntry((pharPath.getFolder().length() == 0 ? "" //$NON-NLS-1$
: pharPath.getFolder() + "/") //$NON-NLS-1$
+ pharPath.getFile());
return new Object[] { new ExternalEntryFile(sourceFilePath, archiveFile, entry) };
} catch (Exception e) {
PHPDebugPlugin.log(e);
}
}
// Check if file exists before moving on to the next checks
File file = new File(sourceFilePath);
if (!file.exists()) {
return EMPTY;
}
// Check if we have corresponding "linked chain subject" file
Object linkedFile = linkSubjectFileFinder.find(sourceFilePath, object);
if (linkedFile != null) {
return new Object[] { linkedFile };
}
// None from above succeeded - just open external file
return new Object[] { new LocalFile(file) };
}
}
return sourceElements;
}
private Object[] getRemoteSourceElements(Object stackFrameObj, boolean forceRetrieval) throws CoreException {
if (stackFrameObj instanceof PHPStackFrame) {
PHPStackFrame stackFrame = (PHPStackFrame) stackFrameObj;
ILaunchConfiguration launchConfiguration = stackFrame.getLaunch().getLaunchConfiguration();
if (launchConfiguration != null) {
if (forceRetrieval
|| launchConfiguration.getAttribute(IPHPDebugConstants.DEBUGGING_USE_SERVER_FILES, false)) {
// Return a Remote
PHPDebugTarget debugTarget = (PHPDebugTarget) stackFrame.getDebugTarget();
String fileName = stackFrame.getAbsoluteFileName();
RemoteFileStorage fileStorage = (RemoteFileStorage) remoteStorageCache.get(fileName);
if (fileStorage == null) {
RemoteDebugger remoteDebugger = (RemoteDebugger) debugTarget.getRemoteDebugger();
String decodedFileName;
try {
decodedFileName = URLDecoder.decode(fileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
decodedFileName = fileName;
}
String originalURL = debugTarget.getLaunch().getAttribute(IDebugParametersKeys.ORIGINAL_URL);
fileStorage = getRemoteFileStorage(remoteDebugger, decodedFileName, originalURL);
remoteStorageCache.put(fileName, fileStorage);
}
return new Object[] { fileStorage };
}
}
}
return EMPTY;
}
private RemoteFileStorage getRemoteFileStorage(RemoteDebugger remoteDebugger, final String fileName,
final String originalURL) {
final SyncObject<RemoteFileStorage> syncObject = new SyncObject<>();
final CountDownLatch waitForContentLatch = new CountDownLatch(1);
if (remoteDebugger == null || !remoteDebugger.isActive()) {
return null;
}
RemoteDebugger.requestRemoteFile(new IRemoteFileContentRequestor() {
public void fileContentReceived(byte[] content, String serverAddress, String originalURL, String fileName,
int lineNumber) {
syncObject.set(new RemoteFileStorage(content, fileName, originalURL));
}
public void requestCompleted(Exception e) {
waitForContentLatch.countDown();
}
}, fileName, 0, originalURL);
try {
waitForContentLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
return syncObject.get();
}
}