blob: fecc83ae1c3a8765e19861794252ef5ac9c1d7d4 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2010, 2017 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.core.rhelp;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImIdentitySet;
import org.eclipse.statet.jcommons.lang.Disposable;
import org.eclipse.statet.ecommons.preferences.core.PreferenceAccess;
import org.eclipse.statet.ecommons.preferences.core.PreferenceSetService;
import org.eclipse.statet.ecommons.preferences.core.PreferenceSetService.ChangeEvent;
import org.eclipse.statet.ecommons.preferences.core.util.PreferenceUtils;
import org.eclipse.statet.internal.r.core.RCorePlugin;
import org.eclipse.statet.internal.r.core.renv.REnvConfiguration;
import org.eclipse.statet.internal.r.core.renv.REnvManager;
import org.eclipse.statet.internal.r.core.rhelp.RHelpWebapp.ContentInfo;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.renv.IREnv;
import org.eclipse.statet.r.core.renv.IREnvConfiguration;
import org.eclipse.statet.r.core.renv.IREnvManager;
import org.eclipse.statet.r.core.rhelp.IRHelpManager;
import org.eclipse.statet.r.core.rhelp.IRHelpPage;
import org.eclipse.statet.r.core.rhelp.IRHelpSearchRequestor;
import org.eclipse.statet.r.core.rhelp.IRPkgHelp;
import org.eclipse.statet.r.core.rhelp.RHelpSearchQuery;
public class RHelpManager implements IRHelpManager, PreferenceSetService.ChangeListener, Disposable {
// Compatible to dynamic R help
// 1) With dynamic= TRUE from tools:::httpd()
// Here generated links are of the forms
// ../../pkg/help/topic
// file.html
// ../../pkg/html/file.html
// and links are never missing: topics are always linked as
// ../../pkg/help/topic for the current packages, and this means
// 'search this package then all the others, and show all matches
// if we need to go outside this packages'
private static final ImIdentitySet<String> PREF_QUALIFIERS= ImCollections.newIdentitySet(
IREnvManager.PREF_QUALIFIER );
private static class EnvItem {
final String id;
int state;
REnvHelp help;
String indexDir;
final Object helpLock= new Object();
final Object indexLock= new Object();
public EnvItem(final String id) {
this.id= id;
this.state= 0;
this.help= null;
this.indexDir= null;
}
}
private class InitJob extends Job {
public InitJob() {
super("Prepare R help");
setSystem(true);
setPriority(Job.DECORATE);
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
ensureIsRunning();
final IREnv rEnv= RHelpManager.this.rEnvManager.getDefault().resolve();
if (rEnv != null) {
final REnvHelp help= getHelp(rEnv);
if (help != null) {
help.unlock();
}
}
return Status.OK_STATUS;
}
}
private static final int HELP_LOADED= 1;
private static final int HELP_MISSING= -1;
private static final int RENV_DELETED= -2;
/**
* Searches topic in library
*/
private static final String RHELP_TOPIC_PATH= "/topic"; //$NON-NLS-1$
/**
* Shows page (package, package/name or package/topic)
*/
private static final String RHELP_PAGE_PATH= "/page"; //$NON-NLS-1$
private final PreferenceAccess prefAccess;
private final REnvManager rEnvManager= RCorePlugin.getInstance().getREnvManager();
private boolean running;
private boolean httpdStarted;
private JettyServer httpd;
private final Object indexLock= new Object();
private final SaveUtil saveUtil= new SaveUtil();
private final Map<String, EnvItem> helpIndexes= new HashMap<>();
public RHelpManager() {
this.prefAccess= PreferenceUtils.getInstancePrefs();
this.prefAccess.addPreferenceSetListener(this, PREF_QUALIFIERS);
new InitJob().schedule(1000);
}
@Override
public String getPageHttpUrl(final IRHelpPage page, final String target) {
final IRPkgHelp pkgHelp= page.getPackage();
return getPageHttpUrl(pkgHelp.getName(), page.getName(), pkgHelp.getREnv(), target);
}
@Override
public String getPageHttpUrl(final String packageName, final String pageName,
final IREnv rEnv, final String target) {
checkRunning();
final StringBuilder sb= new StringBuilder(64);
sb.append(RHelpWebapp.CONTEXT_PATH);
sb.append('/');
sb.append(target);
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
sb.append(RHelpWebapp.CAT_LIBRARY);
sb.append('/');
sb.append(packageName);
sb.append('/');
if (pageName != null) {
sb.append(RHelpWebapp.COMMAND_HTML_PAGE);
sb.append('/');
sb.append(pageName);
sb.append(".html"); //$NON-NLS-1$
}
return createUrl(sb.toString());
}
@Override
public String getTopicHttpUrl(final String topic, final String packageName,
final IREnv rEnv, final String target) {
checkRunning();
final StringBuilder sb= new StringBuilder(64);
sb.append(RHelpWebapp.CONTEXT_PATH);
sb.append('/');
sb.append(target);
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
sb.append(RHelpWebapp.CAT_LIBRARY);
sb.append('/');
sb.append(packageName);
sb.append('/');
sb.append(RHelpWebapp.COMMAND_HELP_TOPIC);
sb.append('/');
sb.append(topic);
return createUrl(sb.toString());
}
@Override
public String getREnvHttpUrl(final IREnv rEnv, final String target) {
checkRunning();
final StringBuilder sb= new StringBuilder(64);
sb.append(RHelpWebapp.CONTEXT_PATH);
sb.append('/');
sb.append(target);
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
return createUrl(sb.toString());
}
@Override
public String getPackageHttpUrl(final IRPkgHelp pkgHelp, final String target) {
return getPageHttpUrl(pkgHelp.getName(), null, pkgHelp.getREnv(), target);
}
@Override
public String toHttpUrl(final String url, final IREnv rEnv, final String target) {
checkRunning();
if (url.startsWith("rhelp:///")) { //$NON-NLS-1$
final StringBuilder sb= new StringBuilder(64);
sb.append(RHelpWebapp.CONTEXT_PATH);
sb.append('/');
sb.append(target);
final String path= url.substring(8);
final int idx1= (path.length() > 0) ? path.indexOf('/', 1) : -1;
if (idx1 > 0) {
final String command= path.substring(0, idx1);
if (command.equals(RHELP_PAGE_PATH)) {
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
sb.append(RHelpWebapp.CAT_LIBRARY);
final int idx2= path.indexOf('/', idx1 + 1);
if (idx2 > idx1 + 1 && idx2 < path.length() - 1) {
sb.append('/');
sb.append(path.substring(idx1 + 1, idx2));
sb.append('/');
sb.append(RHelpWebapp.COMMAND_HTML_PAGE);
sb.append('/');
sb.append(path.substring(idx2 + 1));
sb.append(".html"); //$NON-NLS-1$
}
else {
sb.append('/');
sb.append(path.substring(idx1 + 1, (idx2 > 0) ? idx2 : path.length()));
sb.append('/');
}
return createUrl(sb.toString());
}
else if (command.equals(RHELP_TOPIC_PATH)) {
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
sb.append(RHelpWebapp.CAT_LIBRARY);
sb.append('/');
sb.append('-');
sb.append('/');
sb.append(RHelpWebapp.COMMAND_HELP_TOPIC);
sb.append('/');
sb.append(path.substring(idx1 + 1));
return createUrl(sb.toString());
}
}
else if (path.length() == 1) { // start
sb.append('/');
sb.append(rEnv.getId());
sb.append('/');
return createUrl(sb.toString());
}
return null;
}
if (url.startsWith("http://") && (rEnv != null || target != null)) { //$NON-NLS-1$
try {
final URI uri= new URI(url);
if (isDynamic(uri)) {
final String path= uri.getPath();
if (path != null && path.startsWith(RHelpWebapp.CONTEXT_PATH)) {
final int idx2= path.indexOf('/', RHelpWebapp.CONTEXT_PATH.length() + 1);
if (idx2 >= 0) {
final StringBuilder sb= new StringBuilder(path.length() + 16);
sb.append(RHelpWebapp.CONTEXT_PATH);
sb.append('/');
sb.append((target != null) ? target :
path.substring(RHelpWebapp.CONTEXT_PATH.length() + 1, idx2));
final String info= path.substring(idx2 + 1);
if (rEnv != null) {
final int idx3= info.indexOf('/');
if (idx3 < 0) {
return null;
}
sb.append('/');
sb.append(rEnv.getId());
sb.append(info.substring(idx3));
}
else {
sb.append('/');
sb.append(info);
}
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
sb.toString(), uri.getQuery(), uri.getFragment()).toString();
}
}
return null;
}
}
catch (final Exception e) {}
}
return url;
}
@Override
public String toHttpUrl(final Object object, final String target) {
if (object == this) {
return "about:blank"; //$NON-NLS-1$
}
if (object instanceof IREnv) {
return getREnvHttpUrl((IREnv) object, target);
}
if (object instanceof IREnvConfiguration) {
return getREnvHttpUrl(((IREnvConfiguration) object).getReference(), target);
}
if (object instanceof IRPkgHelp) {
return getPackageHttpUrl((IRPkgHelp) object, target);
}
if (object instanceof IRHelpPage) {
return getPageHttpUrl((IRHelpPage) object, target);
}
if (object instanceof String) {
final String s= (String) object;
if (s.startsWith("http://")) { //$NON-NLS-1$
return s;
}
}
return null;
}
private String createUrl(final String path) {
try {
return new URI("http", null, this.httpd.getHost(), this.httpd.getPort(), path, null, null) //$NON-NLS-1$
.toASCIIString();
}
catch (final URISyntaxException e) {
throw new IllegalStateException(e);
}
}
@Override
public Object getContentOfUrl(final String url) {
try {
final URI uri= new URI(url);
final String path= uri.getPath();
if (uri.getScheme() != null && uri.getScheme().equals("http") //$NON-NLS-1$
&& path != null && path.startsWith(RHelpWebapp.CONTEXT_PATH)) {
final int idx2= path.indexOf('/', RHelpWebapp.CONTEXT_PATH.length() + 1);
if (idx2 >= 0) {
final ContentInfo info= RHelpWebapp.extractContent(path.substring(idx2));
if (info != null) {
final IREnv rEnv= this.rEnvManager.get(info.rEnvId, null);
if (rEnv != null && info.cat == RHelpWebapp.CAT_LIBRARY) {
final REnvHelp help= getHelp(rEnv);
if (help != null) {
try {
final IRPkgHelp pkgHelp= help.getPkgHelp(info.packageName);
if (pkgHelp != null && info.command == RHelpWebapp.COMMAND_HTML_PAGE) {
final IRHelpPage page= pkgHelp.getHelpPage(info.detail);
if (page != null) {
return page;
}
}
return pkgHelp;
}
finally {
help.unlock();
}
}
return null;
}
if (rEnv != null && info.cat == RHelpWebapp.CAT_DOC) {
return new Object[] { rEnv, null };
}
return rEnv;
}
}
}
} catch (final URISyntaxException e) {}
return null;
}
@Override
public boolean ensureIsRunning() {
if (!this.httpdStarted) {
startServer();
}
return this.running;
}
@Override
public boolean isDynamic(final URI url) {
checkRunning();
return ((this.httpd.getHost().equals(url.getHost()) && this.httpd.getPort() == url.getPort()
|| PORTABLE_URL_SCHEME.equals(url.getScheme()) ));
}
@Override
public URI toHttpUrl(final URI url) throws URISyntaxException {
if (isDynamic(url)) {
String path= url.getPath();
if (path != null && !path.startsWith("/rhelp")) {
path= "/rhelp" + path;
}
return new URI("http", null, this.httpd.getHost(), this.httpd.getPort(),
path, url.getQuery(), url.getFragment());
}
return url;
}
@Override
public URI toPortableUrl(final URI url) throws URISyntaxException {
if (isDynamic(url)) {
String path= url.getPath();
if (path != null && path.startsWith("/rhelp")) {
path= path.substring(6);
}
return new URI(PORTABLE_URL_SCHEME, null, null, -1,
path, url.getQuery(), url.getFragment());
}
return url;
}
private void checkRunning() {
if (!ensureIsRunning()) {
throw new UnsupportedOperationException("Help is not available.");
}
}
private synchronized void startServer() {
if (this.httpdStarted) {
return;
}
final JettyServer httpd= new JettyServer();
try {
httpd.startServer();
this.httpd= httpd;
this.running= true;
}
catch (final Exception e) {
RCorePlugin.log(new Status(IStatus.ERROR, RCore.BUNDLE_ID, -1,
"An error occured when starting webserver for R help.", e));
try {
httpd.stopServer();
}
catch (final Exception ignore) {}
}
this.httpdStarted= true;
}
private synchronized void stopServer() {
if (this.httpd == null) {
return;
}
this.running= false;
try {
this.httpd.stopServer();
this.httpd= null;
}
catch (final Exception e) {
RCorePlugin.log(new Status(IStatus.ERROR, RCore.BUNDLE_ID, -1,
"An error occured when stopping webserver for R help.", e));
}
}
@Override
public void search(final RHelpSearchQuery query, final IRHelpSearchRequestor requestor,
final IProgressMonitor monitor) throws CoreException {
if (query == null) {
throw new NullPointerException("query"); //$NON-NLS-1$
}
if (requestor == null) {
throw new NullPointerException("requestor"); //$NON-NLS-1$
}
final RHelpSearchQuery.Compiled compiledQuery= query.compile();
final REnvHelp help= getHelp(compiledQuery.getREnv());
if (help != null) {
try {
if (!help.search(compiledQuery, requestor)) {
throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, -1,
"An internal error occurend when performing R help search.", null));
}
}
finally {
help.unlock();
}
}
else {
final IREnv rEnv= query.getREnv();
if (rEnv == null || rEnv.getConfig() == null) {
throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, -1,
"The selected R environment doesn't exists.", null));
}
else {
throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, -1,
"The R library of the selected R environment <code>" + compiledQuery.getREnv().getName() +
"</code> is not yet indexed. Please run the indexer first to enable R help support.", null));
}
}
}
@Override
public void preferenceChanged(final ChangeEvent event) {
if (event.contains(IREnvManager.PREF_QUALIFIER)) {
final List<IREnvConfiguration> configurations= this.rEnvManager.getConfigurations();
final EnvItem[] items= new EnvItem[configurations.size()];
synchronized (this.indexLock) {
for (int i= 0; i < configurations.size(); i++) {
items[i]= this.helpIndexes.get(configurations.get(i).getReference().getId());
}
}
for (int i= 0; i < configurations.size(); i++) {
if (items[i] != null) {
final EnvItem item= items[i];
final IREnvConfiguration config= configurations.get(i);
REnvHelp oldHelp= null;
synchronized (item.helpLock) {
if (!((item.indexDir != null) ?
item.indexDir.equals(config.getIndexDirectoryPath()) :
null == config.getIndexDirectoryPath() )) {
item.state= 0;
oldHelp= item.help;
item.help= null;
}
}
if (oldHelp != null) {
oldHelp.dispose();
}
}
}
}
}
public boolean updateHelp(final IREnvConfiguration rEnvConfig,
final Map<String, String> rEnvSharedProperties, final REnvHelp help) {
final IREnv rEnv= help.getREnv();
final String rEnvId= rEnv.getId();
EnvItem item;
synchronized (this.indexLock) {
item= this.helpIndexes.get(rEnvId);
if (item == null) {
item= new EnvItem(rEnvId);
this.helpIndexes.put(rEnvId, item);
}
}
REnvHelp oldHelp= null;
try {
synchronized (item.helpLock) {
if (item.state == RENV_DELETED) {
oldHelp= help;
return false;
}
item.state= HELP_LOADED;
item.indexDir= rEnvConfig.getIndexDirectoryPath();
oldHelp= item.help;
item.help= help;
this.saveUtil.save(rEnvConfig, help);
if (rEnvConfig instanceof REnvConfiguration) {
((REnvConfiguration) rEnvConfig).updateSharedProperties(rEnvSharedProperties);
}
return true;
}
}
finally {
if (oldHelp != null) {
oldHelp.dispose();
}
}
}
public void delete(final String id) {
if (id != null && id.length() > 0) {
EnvItem item;
synchronized (this.indexLock) {
item= this.helpIndexes.get(id);
if (item == null) {
item= new EnvItem(id);
this.helpIndexes.put(id, item);
}
}
REnvHelp oldHelp= null;
synchronized (item.helpLock) {
item.state= RENV_DELETED;
oldHelp= item.help;
item.help= null;
}
if (oldHelp != null) {
oldHelp.dispose();
}
}
}
@Override
public List<IREnv> getREnvWithHelp() {
final List<IREnvConfiguration> configurations= this.rEnvManager.getConfigurations();
final EnvItem[] items= new EnvItem[configurations.size()];
synchronized (this.indexLock) {
for (int i= 0; i < configurations.size(); i++) {
final String id= configurations.get(i).getReference().getId();
EnvItem item= this.helpIndexes.get(id);
if (item == null) {
item= new EnvItem(id);
this.helpIndexes.put(id, item);
}
items[i]= item;
}
}
final List<IREnv> withHelp= new ArrayList<>(configurations.size());
for (int i= 0; i < items.length; i++) {
final EnvItem item= items[i];
final IREnvConfiguration rEnvConfig= configurations.get(i);
if (rEnvConfig.isDeleted()) {
continue;
}
synchronized (item.helpLock) {
switch (item.state) {
case HELP_LOADED:
withHelp.add(rEnvConfig.getReference());
continue;
case 0:
if (this.saveUtil.hasIndex(rEnvConfig)) {
withHelp.add(rEnvConfig.getReference());
}
else {
item.state= HELP_MISSING;
}
continue;
default:
continue;
}
}
}
return withHelp;
}
@Override
public boolean hasHelp(IREnv rEnv) {
if (rEnv != null) {
rEnv= rEnv.resolve();
if (rEnv != null) {
final String id= rEnv.getId();
EnvItem item;
synchronized (this.indexLock) {
item= this.helpIndexes.get(id);
if (item == null) {
item= new EnvItem(id);
this.helpIndexes.put(id, item);
}
}
synchronized (item.helpLock) {
switch (item.state) {
case HELP_LOADED:
return true;
case 0:
if (this.saveUtil.hasIndex(rEnv.getConfig())) {
return true;
}
else {
item.state= HELP_MISSING;
return false;
}
default:
return false;
}
}
}
}
return false;
}
@Override
public REnvHelp getHelp(IREnv rEnv) {
if (rEnv != null) {
rEnv= rEnv.resolve();
if (rEnv != null) {
final String rEnvId= rEnv.getId();
EnvItem item;
synchronized (this.indexLock) {
item= this.helpIndexes.get(rEnvId);
if (item == null) {
item= new EnvItem(rEnvId);
this.helpIndexes.put(rEnvId, item);
}
}
synchronized (item.helpLock) {
switch (item.state) {
case HELP_LOADED:
item.help.lock();
return item.help;
case 0:
final IREnvConfiguration rEnvConfig= rEnv.getConfig();
if (rEnvConfig != null) {
item.indexDir= rEnvConfig.getIndexDirectoryPath();
item.help= this.saveUtil.load(rEnvConfig);
}
if (item.help != null) {
item.state= HELP_LOADED;
item.help.lock();
return item.help;
}
else {
item.state= HELP_MISSING;
return null;
}
default:
return null;
}
}
}
}
return null;
}
public Object getIndexLock(final IREnv rEnv) {
final String id= rEnv.getId();
EnvItem item;
synchronized (this.indexLock) {
item= this.helpIndexes.get(id);
if (item == null) {
item= new EnvItem(id);
this.helpIndexes.put(id, item);
}
}
return item.indexLock;
}
@Override
public void dispose() {
this.prefAccess.removePreferenceSetListener(this);
stopServer();
}
}