package net.enilink.komma.edit.properties; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.enilink.commons.iterator.Filter; import net.enilink.commons.iterator.IExtendedIterator; import net.enilink.komma.common.adapter.IAdapterFactory; import net.enilink.komma.common.command.CommandResult; import net.enilink.komma.common.command.ICommand; import net.enilink.komma.common.command.IdentityCommand; import net.enilink.komma.common.command.SimpleCommand; import net.enilink.komma.core.IEntity; import net.enilink.komma.core.IEntityManager; import net.enilink.komma.core.INamespace; import net.enilink.komma.core.IReference; import net.enilink.komma.core.IStatement; import net.enilink.komma.core.IValue; import net.enilink.komma.core.URI; import net.enilink.komma.core.URIs; import net.enilink.komma.edit.assist.ContentProposal; import net.enilink.komma.edit.assist.IContentProposal; import net.enilink.komma.edit.assist.IContentProposalProvider; import net.enilink.komma.edit.properties.ResourceFinder.Match; import net.enilink.komma.edit.properties.ResourceFinder.Options; import net.enilink.komma.edit.provider.IItemLabelProvider; import net.enilink.komma.model.ModelUtil; import net.enilink.komma.parser.BaseRdfParser; import net.enilink.komma.parser.sparql.tree.IriRef; import net.enilink.komma.parser.sparql.tree.QName; import net.enilink.vocab.rdf.RDF; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.parboiled.Parboiled; import org.parboiled.Rule; import org.parboiled.parserunners.BasicParseRunner; import org.parboiled.support.IndexRange; import org.parboiled.support.ParsingResult; public class ResourceEditingSupport implements IEditingSupport { static class ConstructorParser extends BaseRdfParser { public Rule Constructor() { return sequence( firstOf(sequence(firstOf(IriRef(), PN_LOCAL()), ch('a')), ch('a')), WS_NO_COMMENT(), firstOf(IriRef(), PN_LOCAL(), sequence(EMPTY, push(""))), drop(), push(matchRange())); } } protected class ResourceProposal extends ContentProposal implements IResourceProposal { IEntity resource; boolean useAsValue; int score; public ResourceProposal(String content, int cursorPosition, IEntity resource) { super(content, ResourceEditingSupport.this.getLabel(resource), ResourceEditingSupport.this.getLabel(resource), cursorPosition, false); this.resource = resource; } @Override public IEntity getResource() { return resource; } public ResourceProposal setScore(int score) { this.score = score; return this; } public int getScore() { return score; } public ResourceProposal setUseAsValue(boolean useAsValue) { this.useAsValue = useAsValue; return this; } public boolean getUseAsValue() { return useAsValue; } } class ResourceProposalProvider implements IContentProposalProvider { IEntity subject; IReference predicate; ResourceProposalProvider(IEntity subject, IReference predicate) { this.subject = subject; this.predicate = predicate; } @Override public IContentProposal[] getProposals(String contents, int position) { ParsingResult<Object> ctor = new BasicParseRunner<Object>( createConstructorParser().Constructor()).run(contents); String prefix = ""; if (ctor.matched) { IndexRange range = (IndexRange) ctor.resultValue; prefix = contents.substring(0, range.start); contents = contents.substring(range.start, range.end); position = contents.length(); } int limit = 20; Set<IReference> predicates = new LinkedHashSet<>(); if (!ctor.matched) { predicates.add(predicate); } predicates.add(null); Map<IEntity, Match> allMatches = new LinkedHashMap<>(); Options options = Options.create(subject, contents.substring(0, position), limit); if (editPredicate) { options = options.ofType(RDF.TYPE_PROPERTY); } // ensures that resources which match the predicate's range are // added in front of the result list ResourceFinder finder = new ResourceFinder(); for (IReference p : predicates) { if (allMatches.size() >= limit) { break; } for (Match match : finder.findRestrictedResources(options .forPredicate(p))) { // globally filter duplicate proposals Match existing = allMatches.get(match.resource); if (existing == null || existing.score() < match.score()) { allMatches.put(match.resource, match); } } } List<ResourceProposal> resourceProposals = toProposals( allMatches.values(), prefix, !ctor.matched); Comparator<ResourceProposal> comparator = new Comparator<ResourceProposal>() { @Override public int compare(ResourceProposal p1, ResourceProposal p2) { // higher scores are better and hence should be inserted in // front of the list int scoreDiff = p2.score - p1.score; if (scoreDiff != 0) { return scoreDiff; } return p1.getLabel().compareTo(p2.getLabel()); } }; Collections.sort(resourceProposals, comparator); Collection<? extends IContentProposal> results = resourceProposals; if (resourceProposals.size() < limit && !contents.contains(":")) { final String contentsFinal = contents; List<IContentProposal> mixedProposals = new ArrayList<IContentProposal>( resourceProposals); try (IExtendedIterator<INamespace> it = subject .getEntityManager().getNamespaces() .filterKeep(new Filter<INamespace>() { public boolean accept(INamespace ns) { return ns.getPrefix().startsWith(contentsFinal); } })) { while (it.hasNext() && mixedProposals.size() < limit) { INamespace ns = it.next(); mixedProposals.add(new ContentProposal(ns.getPrefix() + ":", ns.getPrefix() + ":", ns.getURI() .toString())); } } results = mixedProposals; } return results.toArray(new IContentProposal[results.size()]); } protected List<ResourceProposal> toProposals(Iterable<Match> matches, String prefix, boolean useAsValue) { List<ResourceProposal> proposals = new ArrayList<>(); for (Match match : matches) { String content = getLabel(match.resource); if (content.length() > 0) { content = prefix + content; proposals.add(new ResourceProposal(content, content .length(), match.resource) .setUseAsValue(useAsValue).setScore(match.score())); } } return proposals; } } protected IAdapterFactory adapterFactory; private boolean editPredicate; public ResourceEditingSupport(IAdapterFactory adapterFactory) { this(adapterFactory, false); } public ResourceEditingSupport(IAdapterFactory adapterFactory, boolean editPredicate) { this.adapterFactory = adapterFactory; this.editPredicate = editPredicate; } protected String getLabel(Object value) { if (value == null) { return ""; } IItemLabelProvider labelProvider = (IItemLabelProvider) adapterFactory .adapt(value, IItemLabelProvider.class); if (labelProvider != null) { return labelProvider.getText(value); } return value.toString(); } protected ConstructorParser createConstructorParser() { return Parboiled.createParser(ConstructorParser.class); } /** * Returns the statement that is represented by this element. */ protected IStatement getStatement(Object element) { if (element instanceof IStatement) { return (IStatement) element; } else { return null; } } @Override public IProposalSupport getProposalSupport(Object element) { final IStatement stmt = getStatement(element); if (stmt == null) { return null; } return new IProposalSupport() { @Override public IContentProposalProvider getProposalProvider() { return new ResourceProposalProvider( (IEntity) stmt.getSubject(), stmt.getPredicate()); } @Override public IItemLabelProvider getLabelProvider() { return new IItemLabelProvider() { @Override public String getText(Object object) { if (object instanceof ResourceProposal) { IEntity resource = ((ResourceProposal) object).resource; IItemLabelProvider labelProvider = (IItemLabelProvider) adapterFactory .adapt(resource, IItemLabelProvider.class); if (labelProvider != null) { return labelProvider.getText(resource); } return ModelUtil.getLabel(resource); } return ((ContentProposal) object).getLabel(); } @Override public Object getImage(Object object) { if (object instanceof ResourceProposal) { IEntity resource = ((ResourceProposal) object).resource; IItemLabelProvider labelProvider = (IItemLabelProvider) adapterFactory .adapt(resource, IItemLabelProvider.class); if (labelProvider != null) { return labelProvider.getImage(resource); } } return null; } }; } @Override public char[] getAutoActivationCharacters() { return null; } }; } @Override public boolean canEdit(Object element) { return true; } @Override public Object getEditorValue(Object element) { Object value = null; IStatement stmt = getStatement(element); if (stmt != null) { value = editPredicate ? stmt.getPredicate() : stmt.getObject(); } return value != null ? getLabel(value) : ""; } protected URI toURI(IEntityManager manager, Object value) { if (value instanceof IriRef) { URI uri = URIs.createURI(((IriRef) value).getIri()); if (uri.isRelative()) { URI ns = manager.getNamespace(""); if (ns != null) { if (ns.fragment() != null) { uri = ns.appendLocalPart(uri.toString()); } else { uri = uri.resolve(ns); } } else { throw new IllegalArgumentException( "Relative IRIs are not supported."); } } return uri; } else if (value instanceof QName) { String prefix = ((QName) value).getPrefix(); String localPart = ((QName) value).getLocalPart(); URI ns; if (prefix == null || prefix.trim().length() == 0) { prefix = ""; } ns = manager.getNamespace(prefix); if (ns != null) { return ns.appendLocalPart(localPart); } throw new IllegalArgumentException("Unknown prefix for QName " + value); } else if (value != null) { // try to parse value as IRI ref or prefixed name ParsingResult<Object> iriRef = new BasicParseRunner<Object>( createConstructorParser().IriRef()).run(value.toString()); if (iriRef.matched) { return toURI(manager, iriRef.resultValue); } URI ns = manager.getNamespace(""); if (ns != null) { return ns.appendLocalPart(value.toString()); } else { throw new IllegalArgumentException( "Relative IRIs are not supported."); } } return null; } protected IEntity getSubject(Object element) { if (element instanceof IEntity) { return (IEntity) element; } IStatement stmt = getStatement(element); return (IEntity) ((stmt != null && stmt.getSubject() instanceof IEntity) ? stmt .getSubject() : null); } @Override public ICommand convertEditorValue(Object editorValue, final IEntityManager entityManager, Object element) { if (editorValue instanceof IValue) { // short-circuit if supplied value is already an RDF resource return new IdentityCommand(editorValue); } String valueStr = editorValue.toString().trim(); if (valueStr.isEmpty()) { return new IdentityCommand("Remove element."); } final URI[] name = { null }; final boolean[] createNew = { false }; ParsingResult<Object> ctor = new BasicParseRunner<Object>( createConstructorParser().Constructor()).run(valueStr); if (ctor.matched) { createNew[0] = true; IndexRange range = (IndexRange) ctor.resultValue; valueStr = valueStr.substring(range.start, range.end); // check if a name for the new resource is given if (ctor.valueStack.size() > 1) { name[0] = toURI(entityManager, ctor.valueStack.peek(1)); } } boolean forced = false; // allow to force the use of a resource by appending "!" even if it // does not exist if (valueStr.endsWith("!") && valueStr.length() > 1) { forced = true; valueStr = valueStr.substring(0, valueStr.length() - 1); } // allow to specify resources as full IRIs in the form // <http://example.org#resource> or as prefixed names // example:resource final URI uri = toURI(entityManager, valueStr); if (uri != null && (forced || entityManager.hasMatch(uri, null, null))) { return new SimpleCommand() { @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { if (createNew[0] && name[0] != null) { // create a new named resource return CommandResult.newOKCommandResult(entityManager .createNamed(name[0], uri)); } else if (createNew[0]) { // create a new blank node resource return CommandResult.newOKCommandResult(entityManager .create(uri)); } else { return CommandResult.newOKCommandResult(entityManager .find(uri)); } } }; } // try a full-text search to find the resource IStatement stmt = getStatement(element); Options options; Object oldValue = null; IReference property = null; if (stmt != null) { options = Options.create((IEntity) stmt.getSubject(), (String) editorValue, 1); property = stmt.getPredicate(); oldValue = property == null ? null : stmt.getObject(); } else { options = Options.create(entityManager, null, (String) editorValue, 1); } Iterator<Match> matches = new ResourceFinder().findAnyResources( options.forPredicate(createNew[0] ? null : property)).iterator(); if (matches.hasNext()) { final IEntity resource = matches.next().resource; if (createNew[0] && getLabel(resource).equals(valueStr)) { // create a new object return new SimpleCommand() { @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { return CommandResult.newOKCommandResult(entityManager .createNamed(name[0], resource)); } }; } else if (!resource.equals(oldValue) && getLabel(resource).equals(valueStr)) { // replace value with existing object return new IdentityCommand(resource); } } return null; } }