some merges back to the trunk that I forgot to commit earlier
This commit is contained in:
parent
4cd59ee32d
commit
4f3664b905
10 changed files with 102 additions and 29 deletions
|
@ -9,7 +9,7 @@ import java.util.Date;
|
||||||
from the entities, object property statements, properties, and ent2relationships tables
|
from the entities, object property statements, properties, and ent2relationships tables
|
||||||
bundled up in a usable object.
|
bundled up in a usable object.
|
||||||
*/
|
*/
|
||||||
public class PropertyInstance implements PropertyInstanceIface {
|
public class PropertyInstance implements PropertyInstanceIface, Comparable<PropertyInstance> {
|
||||||
|
|
||||||
private String propertyURI = null;
|
private String propertyURI = null;
|
||||||
private String objectEntURI = null;
|
private String objectEntURI = null;
|
||||||
|
@ -199,4 +199,18 @@ public class PropertyInstance implements PropertyInstanceIface {
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int compareTo(PropertyInstance pi) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.getDomainPublic().equals(pi.getDomainPublic())) {
|
||||||
|
return this.getRangeClassName().compareTo(pi.getRangeClassName());
|
||||||
|
} else {
|
||||||
|
return (this.getDomainPublic().compareTo(pi.getDomainPublic()));
|
||||||
|
}
|
||||||
|
} catch (NullPointerException npe) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.servlet.RequestDispatcher;
|
import javax.servlet.RequestDispatcher;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
@ -64,10 +65,28 @@ public class PropertyWebappsListingController extends BaseEditController {
|
||||||
VClassDao vcDao = vrequest.getFullWebappDaoFactory().getVClassDao();
|
VClassDao vcDao = vrequest.getFullWebappDaoFactory().getVClassDao();
|
||||||
PropertyGroupDao pgDao = vrequest.getFullWebappDaoFactory().getPropertyGroupDao();
|
PropertyGroupDao pgDao = vrequest.getFullWebappDaoFactory().getPropertyGroupDao();
|
||||||
|
|
||||||
|
String vclassURI = request.getParameter("vclassUri");
|
||||||
|
|
||||||
List props = new ArrayList();
|
List props = new ArrayList();
|
||||||
if (request.getParameter("propsForClass") != null) {
|
if (request.getParameter("propsForClass") != null) {
|
||||||
noResultsMsgStr = "There are no properties that apply to this class.";
|
noResultsMsgStr = "There are no properties that apply to this class.";
|
||||||
Collection propInsts = piDao.getAllPropInstByVClass(request.getParameter("vclassUri"));
|
|
||||||
|
// incomplete list of classes to check, but better than before
|
||||||
|
List<String> superclassURIs = vcDao.getAllSuperClassURIs(vclassURI);
|
||||||
|
superclassURIs.add(vclassURI);
|
||||||
|
superclassURIs.addAll(vcDao.getEquivalentClassURIs(vclassURI));
|
||||||
|
|
||||||
|
Map<String, PropertyInstance> propInstMap = new HashMap<String, PropertyInstance>();
|
||||||
|
for (String classURI : superclassURIs) {
|
||||||
|
Collection<PropertyInstance> propInsts = piDao.getAllPropInstByVClass(classURI);
|
||||||
|
for (PropertyInstance propInst : propInsts) {
|
||||||
|
propInstMap.put(propInst.getPropertyURI(), propInst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<PropertyInstance> propInsts = new ArrayList<PropertyInstance>();
|
||||||
|
propInsts.addAll(propInstMap.values());
|
||||||
|
Collections.sort(propInsts);
|
||||||
|
|
||||||
Iterator propInstIt = propInsts.iterator();
|
Iterator propInstIt = propInsts.iterator();
|
||||||
HashSet propURIs = new HashSet();
|
HashSet propURIs = new HashSet();
|
||||||
while (propInstIt.hasNext()) {
|
while (propInstIt.hasNext()) {
|
||||||
|
|
|
@ -117,12 +117,14 @@ public class BrowseController extends FreeMarkerHttpServlet {
|
||||||
// Get all classgroups, each populated with a list of their member vclasses
|
// Get all classgroups, each populated with a list of their member vclasses
|
||||||
List groups = vcgDao.getPublicGroupsWithVClasses(ORDER_BY_DISPLAYRANK, !INCLUDE_UNINSTANTIATED);
|
List groups = vcgDao.getPublicGroupsWithVClasses(ORDER_BY_DISPLAYRANK, !INCLUDE_UNINSTANTIATED);
|
||||||
|
|
||||||
|
// remove classes that have been configured to be hidden
|
||||||
|
// from search results
|
||||||
|
removeClassesHiddenFromSearch(groups);
|
||||||
|
|
||||||
// now cull out the groups with no populated classes
|
// now cull out the groups with no populated classes
|
||||||
//removeUnpopulatedClasses( groups);
|
//removeUnpopulatedClasses( groups);
|
||||||
vcgDao.removeUnpopulatedGroups(groups);
|
vcgDao.removeUnpopulatedGroups(groups);
|
||||||
|
|
||||||
removeClassesHiddenFromSearch(groups);
|
|
||||||
|
|
||||||
_groupListMap.put(portalId, groups);
|
_groupListMap.put(portalId, groups);
|
||||||
return groups;
|
return groups;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -45,6 +45,7 @@ import edu.cornell.mannlib.vitro.webapp.dao.InsertException;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao;
|
import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
|
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
|
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.event.EditEvent;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.pellet.PelletListener;
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.pellet.PelletListener;
|
||||||
|
|
||||||
public class DataPropertyDaoJena extends PropertyDaoJena implements
|
public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
|
@ -96,6 +97,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
// TODO check if used as onProperty of restriction
|
// TODO check if used as onProperty of restriction
|
||||||
ontModel.enterCriticalSection(Lock.WRITE);
|
ontModel.enterCriticalSection(Lock.WRITE);
|
||||||
try {
|
try {
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),true));
|
||||||
DatatypeProperty dp = ontModel.getDatatypeProperty(URI);
|
DatatypeProperty dp = ontModel.getDatatypeProperty(URI);
|
||||||
if (dp != null) {
|
if (dp != null) {
|
||||||
Iterator<Resource> restIt = ontModel.listSubjectsWithProperty(OWL.onProperty, dp);
|
Iterator<Resource> restIt = ontModel.listSubjectsWithProperty(OWL.onProperty, dp);
|
||||||
|
@ -109,6 +111,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
removeRulesMentioningResource(dp, ontModel);
|
removeRulesMentioningResource(dp, ontModel);
|
||||||
dp.remove();
|
dp.remove();
|
||||||
}
|
}
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),false));
|
||||||
} finally {
|
} finally {
|
||||||
ontModel.leaveCriticalSection();
|
ontModel.leaveCriticalSection();
|
||||||
}
|
}
|
||||||
|
@ -509,6 +512,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
public String insertDataProperty(DataProperty dtp, OntModel ontModel) throws InsertException {
|
public String insertDataProperty(DataProperty dtp, OntModel ontModel) throws InsertException {
|
||||||
ontModel.enterCriticalSection(Lock.WRITE);
|
ontModel.enterCriticalSection(Lock.WRITE);
|
||||||
try {
|
try {
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),true));
|
||||||
String errMsgStr = getWebappDaoFactory().checkURI(dtp.getURI());
|
String errMsgStr = getWebappDaoFactory().checkURI(dtp.getURI());
|
||||||
if (errMsgStr != null) {
|
if (errMsgStr != null) {
|
||||||
throw new InsertException(errMsgStr);
|
throw new InsertException(errMsgStr);
|
||||||
|
@ -565,6 +569,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
log.error("error linking data property "+dtp.getURI()+" to property group");
|
log.error("error linking data property "+dtp.getURI()+" to property group");
|
||||||
}
|
}
|
||||||
addPropertyStringValue(jDataprop,PROPERTY_CUSTOMENTRYFORMANNOT,dtp.getCustomEntryForm(),ontModel);
|
addPropertyStringValue(jDataprop,PROPERTY_CUSTOMENTRYFORMANNOT,dtp.getCustomEntryForm(),ontModel);
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),false));
|
||||||
} finally {
|
} finally {
|
||||||
ontModel.leaveCriticalSection();
|
ontModel.leaveCriticalSection();
|
||||||
}
|
}
|
||||||
|
@ -578,6 +583,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
public void updateDataProperty(DataProperty dtp, OntModel ontModel) {
|
public void updateDataProperty(DataProperty dtp, OntModel ontModel) {
|
||||||
ontModel.enterCriticalSection(Lock.WRITE);
|
ontModel.enterCriticalSection(Lock.WRITE);
|
||||||
try {
|
try {
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),true));
|
||||||
com.hp.hpl.jena.ontology.DatatypeProperty jDataprop = ontModel.getDatatypeProperty(dtp.getURI());
|
com.hp.hpl.jena.ontology.DatatypeProperty jDataprop = ontModel.getDatatypeProperty(dtp.getURI());
|
||||||
|
|
||||||
updateRDFSLabel(jDataprop, dtp.getPublicName());
|
updateRDFSLabel(jDataprop, dtp.getPublicName());
|
||||||
|
@ -614,6 +620,7 @@ public class DataPropertyDaoJena extends PropertyDaoJena implements
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePropertyStringValue(jDataprop,PROPERTY_CUSTOMENTRYFORMANNOT,dtp.getCustomEntryForm(),ontModel);
|
updatePropertyStringValue(jDataprop,PROPERTY_CUSTOMENTRYFORMANNOT,dtp.getCustomEntryForm(),ontModel);
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),false));
|
||||||
} finally {
|
} finally {
|
||||||
ontModel.leaveCriticalSection();
|
ontModel.leaveCriticalSection();
|
||||||
}
|
}
|
||||||
|
|
|
@ -379,6 +379,7 @@ public class ObjectPropertyDaoJena extends PropertyDaoJena implements ObjectProp
|
||||||
doUpdate(prop,p,inv,ontModel);
|
doUpdate(prop,p,inv,ontModel);
|
||||||
return 0;
|
return 0;
|
||||||
} finally {
|
} finally {
|
||||||
|
getOntModel().getBaseModel().notifyEvent(new EditEvent(getWebappDaoFactory().getUserURI(),false));
|
||||||
ontModel.leaveCriticalSection();
|
ontModel.leaveCriticalSection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,6 +268,7 @@ public class VClassDaoJena extends JenaBaseDao implements VClassDao {
|
||||||
getOntModel().enterCriticalSection(Lock.READ);
|
getOntModel().enterCriticalSection(Lock.READ);
|
||||||
try {
|
try {
|
||||||
OntClass ontClass = getOntClass(getOntModel(), classURI);
|
OntClass ontClass = getOntClass(getOntModel(), classURI);
|
||||||
|
System.out.println(classURI);
|
||||||
ClosableIterator equivalentOntClassIt = ontClass.listEquivalentClasses();
|
ClosableIterator equivalentOntClassIt = ontClass.listEquivalentClasses();
|
||||||
try {
|
try {
|
||||||
for (Iterator eqOntClassIt = equivalentOntClassIt; eqOntClassIt.hasNext(); ) {
|
for (Iterator eqOntClassIt = equivalentOntClassIt; eqOntClassIt.hasNext(); ) {
|
||||||
|
@ -394,7 +395,7 @@ public class VClassDaoJena extends JenaBaseDao implements VClassDao {
|
||||||
//if ("true".equalsIgnoreCase(infersTypes)) {
|
//if ("true".equalsIgnoreCase(infersTypes)) {
|
||||||
|
|
||||||
PelletListener pl = getWebappDaoFactory().getPelletListener();
|
PelletListener pl = getWebappDaoFactory().getPelletListener();
|
||||||
if (pl != null && pl.isConsistent() && !pl.isInErrorState()) {
|
if (pl != null && pl.isConsistent() && !pl.isInErrorState() && !pl.isReasoning()) {
|
||||||
superclassURIs = new ArrayList<String>();
|
superclassURIs = new ArrayList<String>();
|
||||||
OntClass cls = getOntClass(getOntModel(),classURI);
|
OntClass cls = getOntClass(getOntModel(),classURI);
|
||||||
StmtIterator superClassIt = getOntModel().listStatements(cls,RDFS.subClassOf,(RDFNode)null);
|
StmtIterator superClassIt = getOntModel().listStatements(cls,RDFS.subClassOf,(RDFNode)null);
|
||||||
|
@ -740,7 +741,9 @@ public class VClassDaoJena extends JenaBaseDao implements VClassDao {
|
||||||
String isInferencing = getWebappDaoFactory().getProperties().get("infersTypes");
|
String isInferencing = getWebappDaoFactory().getProperties().get("infersTypes");
|
||||||
// if this model infers types based on the taxonomy, adding the subclasses will only
|
// if this model infers types based on the taxonomy, adding the subclasses will only
|
||||||
// waste time for no benefit
|
// waste time for no benefit
|
||||||
if (isInferencing == null || "false".equalsIgnoreCase(isInferencing)) {
|
PelletListener pl = getWebappDaoFactory().getPelletListener();
|
||||||
|
if (pl == null || !pl.isConsistent() || pl.isInErrorState() || pl.isReasoning()
|
||||||
|
|| isInferencing == null || "false".equalsIgnoreCase(isInferencing)) {
|
||||||
Iterator classURIs = getAllSubClassURIs(getClassURIStr(superclass)).iterator();
|
Iterator classURIs = getAllSubClassURIs(getClassURIStr(superclass)).iterator();
|
||||||
while (classURIs.hasNext()) {
|
while (classURIs.hasNext()) {
|
||||||
String classURI = (String) classURIs.next();
|
String classURI = (String) classURIs.next();
|
||||||
|
@ -748,7 +751,6 @@ public class VClassDaoJena extends JenaBaseDao implements VClassDao {
|
||||||
if (vClass != null)
|
if (vClass != null)
|
||||||
vClasses.add(vClass);
|
vClasses.add(vClass);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,10 @@ public class PelletListener implements ModelChangedListener {
|
||||||
return this.inErrorState;
|
return this.inErrorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReasoning() {
|
||||||
|
return this.isReasoning;
|
||||||
|
}
|
||||||
|
|
||||||
public void closePipe() {
|
public void closePipe() {
|
||||||
pipeOpen = false;
|
pipeOpen = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,9 @@ import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyStatementDao;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
|
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao;
|
import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
|
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.WebappDaoFactoryJena;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.pellet.PelletListener;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.search.beans.ProhibitedFromSearch;
|
||||||
|
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.WebappDaoFactoryJena;
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.WebappDaoFactoryJena;
|
||||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.pellet.PelletListener;
|
import edu.cornell.mannlib.vitro.webapp.dao.jena.pellet.PelletListener;
|
||||||
|
@ -241,7 +244,28 @@ public class SelectListGenerator {
|
||||||
log.error("Cannot find owl:Class " + vclassUri + " in the model" );
|
log.error("Cannot find owl:Class " + vclassUri + " in the model" );
|
||||||
optionsMap.put("", "Could not find class " + vclassUri);
|
optionsMap.put("", "Could not find class " + vclassUri);
|
||||||
}else{
|
}else{
|
||||||
List<Individual> individuals = wDaoFact.getIndividualDao().getIndividualsByVClassURI(vclass.getURI(),-1,-1);
|
Map<String, Individual> individualMap = new HashMap<String, Individual>();
|
||||||
|
|
||||||
|
for (Individual ind : wDaoFact.getIndividualDao().getIndividualsByVClassURI(vclass.getURI(),-1,-1)) {
|
||||||
|
if (ind.getURI() != null) {
|
||||||
|
individualMap.put(ind.getURI(), ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inferenceAvailable) {
|
||||||
|
for (String subclassURI : wDaoFact.getVClassDao().getAllSubClassURIs(vclass.getURI())) {
|
||||||
|
for (Individual ind : wDaoFact.getIndividualDao().getIndividualsByVClassURI(subclassURI,-1,-1)) {
|
||||||
|
if (ind.getURI() != null) {
|
||||||
|
individualMap.put(ind.getURI(), ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Individual> individuals = new ArrayList<Individual>();
|
||||||
|
individuals.addAll(individualMap.values());
|
||||||
|
Collections.sort(individuals);
|
||||||
|
|
||||||
Map<String, Individual> individualMap = new HashMap<String, Individual>();
|
Map<String, Individual> individualMap = new HashMap<String, Individual>();
|
||||||
|
|
||||||
for (Individual ind : wDaoFact.getIndividualDao().getIndividualsByVClassURI(vclass.getURI(),-1,-1)) {
|
for (Individual ind : wDaoFact.getIndividualDao().getIndividualsByVClassURI(vclass.getURI(),-1,-1)) {
|
||||||
|
|
|
@ -163,27 +163,28 @@ public class TBoxUpdater {
|
||||||
if (!newObject.equals(oldObject)) {
|
if (!newObject.equals(oldObject)) {
|
||||||
NodeIterator siteObjects = siteModel.listObjectsOfProperty(subject,predicate);
|
NodeIterator siteObjects = siteModel.listObjectsOfProperty(subject,predicate);
|
||||||
|
|
||||||
if (siteObjects == null || !siteObjects.hasNext()) {
|
RDFNode siteObject = null;
|
||||||
continue;
|
|
||||||
|
if (siteObjects != null && siteObjects.hasNext()) {
|
||||||
|
|
||||||
|
siteObject = siteObjects.next();
|
||||||
|
|
||||||
|
i = 1;
|
||||||
|
while (siteObjects.hasNext()) {
|
||||||
|
i++;
|
||||||
|
siteObjects.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 1) {
|
||||||
|
logger.log("WARNING: found " + i +
|
||||||
|
" statements with subject = " + subject.getURI() +
|
||||||
|
" and property = " + predicate.getURI() +
|
||||||
|
" in the site annotations model. (maximum of one is expected). ");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RDFNode siteObject = siteObjects.next();
|
if (siteObject == null || siteObject.equals(oldObject)) {
|
||||||
|
|
||||||
i = 1;
|
|
||||||
while (siteObjects.hasNext()) {
|
|
||||||
i++;
|
|
||||||
siteObjects.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > 1) {
|
|
||||||
logger.log("WARNING: found " + i +
|
|
||||||
" statements with subject = " + subject.getURI() +
|
|
||||||
" and property = " + predicate.getURI() +
|
|
||||||
" in the site annotations model. (maximum of one is expected). ");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siteObject.equals(oldObject)) {
|
|
||||||
try {
|
try {
|
||||||
StmtIterator it = siteModel.listStatements(subject, predicate, (RDFNode)null);
|
StmtIterator it = siteModel.listStatements(subject, predicate, (RDFNode)null);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
|
|
|
@ -104,7 +104,6 @@ public class UpdateKnowledgeBase implements ServletContextListener {
|
||||||
if (ontologyUpdater.updateRequired()) {
|
if (ontologyUpdater.updateRequired()) {
|
||||||
ctx.setAttribute(LuceneSetup.INDEX_REBUILD_REQUESTED_AT_STARTUP, Boolean.TRUE);
|
ctx.setAttribute(LuceneSetup.INDEX_REBUILD_REQUESTED_AT_STARTUP, Boolean.TRUE);
|
||||||
doMiscAppMetadataReplacements(ctx.getRealPath(MISC_REPLACEMENTS_FILE), oms);
|
doMiscAppMetadataReplacements(ctx.getRealPath(MISC_REPLACEMENTS_FILE), oms);
|
||||||
System.out.println(ctx.getRealPath(MISC_REPLACEMENTS_FILE));
|
|
||||||
}
|
}
|
||||||
} catch (Throwable t){
|
} catch (Throwable t){
|
||||||
log.error("Unable to perform miscellaneous application metadata replacements", t);
|
log.error("Unable to perform miscellaneous application metadata replacements", t);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue