diff --git a/eddy/core/application.py b/eddy/core/application.py index 5e43b514..6026d28a 100644 --- a/eddy/core/application.py +++ b/eddy/core/application.py @@ -310,12 +310,14 @@ def start(self) -> None: @QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str, str, str, str) + @QtCore.pyqtSlot(str, str, str, str, str) def doCreateSession( self, path: Optional[str] = None, name: Optional[str] = None, iri: Optional[str] = None, prefix: Optional[str] = None, + owl_path: Optional[str] = None ) -> None: """ Create a session using the given project path. @@ -335,7 +337,7 @@ def doCreateSession( # If we do not have a session for the given project we'll create one. with BusyProgressDialog('Loading project: {0}'.format(path)): try: - session = Session(self, path, name=name, iri=iri, prefix=prefix) + session = Session(self, path, name=name, iri=iri, prefix=prefix, owl_path=owl_path) except ProjectStopLoadingError: pass except (ProjectNotFoundError, ProjectNotValidError, ProjectVersionError) as e: @@ -387,6 +389,8 @@ def doCreateSession( self.sessions.append(session) self.sgnSessionCreated.emit(session) session.show() + if owl_path: + session.sgnStartOwlImport[str].emit(owl_path) @QtCore.pyqtSlot() def doQuit(self) -> None: diff --git a/eddy/core/loaders/owl2.py b/eddy/core/loaders/owl2.py index d234113e..60d86517 100644 --- a/eddy/core/loaders/owl2.py +++ b/eddy/core/loaders/owl2.py @@ -1,17 +1,99 @@ +import os +import sys + from PyQt5 import QtCore from jpype import JImplements, JOverride -from eddy.core.common import HasThreadingSystem from eddy.core.datatypes.system import File from eddy.core.jvm import getJavaVM -from eddy.core.loaders.common import AbstractOntologyLoader +from eddy.core.loaders.common import AbstractProjectLoader from eddy.core.output import getLogger from eddy.core.owl import ImportedOntology +from eddy.core.project import Project from eddy.core.worker import AbstractWorker LOGGER = getLogger() +class OwlProjectLoader(AbstractProjectLoader): + """ + Extends AbstractProjectLoader with facilities to create a project from Owl file. + """ + def __init__(self, path, session): + """ + Initialize the loader. + :param path: path to the OWL 2 file + :param session: session + """ + super().__init__(path, session) + self.nproject = None + + def run(self): + """ + Perform project import. + """ + self.createProject() + self.projectLoaded() + + @classmethod + def filetype(cls): + """ + Returns the type of the file that will be used for the import. + :return: File + """ + return File.Owl + + def createProject(self): + """ + Create the Project by reading data from the parsed Owl File. + """ + ontologyIRI, ontologyV = self.getOntologyID() + self.nproject = Project( + parent=self.session, + profile=self.session.createProfile('OWL 2'), + ontologyIRI=ontologyIRI, + version=ontologyV + ) + LOGGER.info('Loaded project from ontology: %s...', self.path) + + def projectLoaded(self): + """ + Initialize the Session Project to be the loaded one. + """ + self.session.project = self.nproject + + def getOntologyID(self): + """ + Get Ontology IRI from Owl File. + """ + vm = getJavaVM() + if not vm.isRunning(): + vm.initialize() + vm.attachThreadToJVM() + + OWLManager = vm.getJavaClass('org.semanticweb.owlapi.apibinding.OWLManager') + MissingImportHandlingStrategy = vm.getJavaClass('org.semanticweb.owlapi.model.MissingImportHandlingStrategy') + JavaFileClass = vm.getJavaClass('java.io.File') + fileInstance = JavaFileClass(self.path) + manager = OWLManager().createOWLOntologyManager() + config = manager.getOntologyLoaderConfiguration() + config = config.setMissingImportHandlingStrategy(MissingImportHandlingStrategy.SILENT) + manager.setOntologyLoaderConfiguration(config) + ontology = manager.loadOntologyFromOntologyDocument(fileInstance) + ontologyID = ontology.getOntologyID() + if ontologyID.isAnonymous(): + ontologyIRI = None + ontologyV = None + else: + ontologyIRI = ontologyID.getOntologyIRI().get().toString() + if ontologyID.getVersionIRI().isPresent(): + ontologyV = ontologyID.getVersionIRI().get().toString() + else: + ontologyV = None + + return ontologyIRI, ontologyV + + class OwlOntologyImportWorker(AbstractWorker): """ Expose facilities to load an OWL ontology starting from the given parameters @@ -269,6 +351,7 @@ def run(self): else: self._loadCount += 1 impOnt.correctlyLoaded = True + self.project.sgnImportedOntologyLoaded.emit(impOnt) except Exception as e: LOGGER.exception('Fatal exception while resolving ontology imports: {}'.format(str(e))) else: diff --git a/eddy/plugins/ontology-importer/ontology_importer.py b/eddy/plugins/ontology-importer/ontology_importer.py index 9fd53749..fd3a95cb 100644 --- a/eddy/plugins/ontology-importer/ontology_importer.py +++ b/eddy/plugins/ontology-importer/ontology_importer.py @@ -37,6 +37,7 @@ import sqlite3 import sys import textwrap +from typing import Optional from PyQt5 import ( QtCore, @@ -83,10 +84,11 @@ from eddy.core.items.nodes.union import UnionNode from eddy.core.items.nodes.value_domain import ValueDomainNode from eddy.core.jvm import getJavaVM +from eddy.core.loaders.owl2 import OwlOntologyImportSetWorker from eddy.core.owl import ( AnnotationAssertion, IllegalNamespaceError, - Facet, + Facet, ImportedOntology, ) from eddy.core.owl import IRI from eddy.core.owl import Literal @@ -855,19 +857,54 @@ def importPrefixes(self): command = CommandProjectAddPrefix(self.project, prefix, namespace) self.session.undostack.push(command) - def doOpenOntologyFile(self): + def getImportedOntologies(self): + """ + Get set of Imported Ontologies. + """ + folder = self.JavaFileClass(str(os.path.expanduser('~'))) + iriMapper = self.AutoIRIMapper(folder, True) + self.manager.getIRIMappers().add(iriMapper) + + directImports = self.ontology.getDirectImports() + importedOnto = set() + for imp in directImports: + ontologyID = imp.getOntologyID() + if not ontologyID.isAnonymous(): + ontologyIRI = ontologyID.getOntologyIRI().get().toString() + ontologyURI = ontologyID.getOntologyIRI().get().toURI().toString() + if ontologyID.getVersionIRI().isPresent(): + ontologyV = ontologyID.getVersionIRI().get().toString() + else: + ontologyV = None + ontologyPath = iriMapper.getDocumentIRI(ontologyID.getOntologyIRI().get()) + if ontologyPath: + ontologyPath = ontologyPath.toString().replace('file:', '', 1) + impOnto = ImportedOntology(ontologyIRI, ontologyPath, ontologyV, True) + importedOnto.add(impOnto) + else: + impOnto = ImportedOntology(ontologyIRI, ontologyURI, ontologyV, False) + importedOnto.add(impOnto) + return importedOnto + + @QtCore.pyqtSlot(str) + @QtCore.pyqtSlot() + def doOpenOntologyFile(self, my_owl_file: Optional[str] = None): """ Starts the import process by selecting an OWL 2 ontology file. """ - dialog = FileDialog(self.session) - dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen) - dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) - dialog.setViewMode(QtWidgets.QFileDialog.Detail) - dialog.setNameFilters([File.Owl.value]) + if my_owl_file: + self.filePath = [my_owl_file] + else: + dialog = FileDialog(self.session) + dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen) + dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) + dialog.setViewMode(QtWidgets.QFileDialog.Detail) + dialog.setNameFilters([File.Owl.value]) - if dialog.exec_(): - self.filePath = dialog.selectedFiles() + if dialog.exec_(): + self.filePath = dialog.selectedFiles() + if self.filePath: ### SET SPACE BETWEEN ITEMS ### form = DiagramPropertiesForm(self.project, parent=self.session) if form.exec_(): @@ -889,6 +926,9 @@ def doOpenOntologyFile(self): ### IMPORT OWL API ### if True: self.OWLManager = self.vm.getJavaClass('org.semanticweb.owlapi.apibinding.OWLManager') + self.MissingImportHandlingStrategy = self.vm.getJavaClass( + 'org.semanticweb.owlapi.model.MissingImportHandlingStrategy') + self.AutoIRIMapper = self.vm.getJavaClass('org.semanticweb.owlapi.util.AutoIRIMapper') self.JavaFileClass = self.vm.getJavaClass('java.io.File') self.Type = self.vm.getJavaClass('org.semanticweb.owlapi.model.AxiomType') self.Set = self.vm.getJavaClass('java.util.Set') @@ -950,14 +990,17 @@ def doOpenOntologyFile(self): try: - for x in dialog.selectedFiles(): - + for x in self.filePath: QtCore.QCoreApplication.processEvents() self.fileInstance = self.JavaFileClass(x) QtCore.QCoreApplication.processEvents() self.manager = self.OWLManager().createOWLOntologyManager() + config = self.manager.getOntologyLoaderConfiguration() + config = config.setMissingImportHandlingStrategy( + self.MissingImportHandlingStrategy.SILENT) + self.manager.setOntologyLoaderConfiguration(config) QtCore.QCoreApplication.processEvents() self.ontology = self.manager.loadOntologyFromOntologyDocument( @@ -1039,6 +1082,18 @@ def doOpenOntologyFile(self): self.importAnnotations(diagram) QtCore.QCoreApplication.processEvents() + ### IMPORT IMPORTED ONTOLOGIES ### + imported = self.getImportedOntologies() + for imp in imported: + self.project.addImportedOntology(imp) + QtCore.QCoreApplication.processEvents() + worker = OwlOntologyImportSetWorker(self.project) + worker.run() + self.session.owlOntologyImportSize = worker.importSize + self.session.owlOntologyImportLoadedCount = worker.loadCount + if self.session.owlOntologyImportSize > self.session.owlOntologyImportLoadedCount: + self.session.owlOntologyImportErrors = worker.owlOntologyImportErrors + renderer = self.ManchesterOWLSyntaxOWLObjectRendererImpl() ### COUNT PROCESSED AXIOMS ### @@ -1202,7 +1257,7 @@ def start(self): # CONFIGURE SIGNALS/SLOTS connect(self.session.sgnNoSaveProject, self.onNoSave) - + connect(self.session.sgnStartOwlImport, self.doOpenOntologyFile) # importation in DB # class Importation(): diff --git a/eddy/ui/project.py b/eddy/ui/project.py index 241a19a7..aa5f59f4 100644 --- a/eddy/ui/project.py +++ b/eddy/ui/project.py @@ -191,3 +191,100 @@ def doValidateForm(self) -> None: self.caption.setVisible(not isEmpty(caption)) self.confirmationBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled) self.setFixedSize(self.sizeHint()) + + +class ProjectFromOWLDialog(QtWidgets.QDialog): + """ + This class is used to display a modal window to enter new project specific data. + """ + def __init__(self, parent: QtWidgets.QWidget = None) -> None: + """ + Initialize the project dialog. + :type parent: QWidget + """ + super().__init__(parent) + + ############################################# + # FORM AREA + ################################# + + self.nameLabel = QtWidgets.QLabel(self) + self.nameLabel.setText('Project name') + self.nameField = StringField(self) + self.nameField.setMinimumWidth(400) + self.nameField.setMaxLength(64) + + connect(self.nameField.textChanged, self.doValidateForm) + + self.formWidget = QtWidgets.QWidget(self) + self.formLayout = QtWidgets.QFormLayout(self.formWidget) + self.formLayout.addRow(self.nameLabel, self.nameField) + + ############################################# + # CONFIRMATION AREA + ################################# + + self.confirmationBox = QtWidgets.QDialogButtonBox(QtCore.Qt.Horizontal, self) + self.confirmationBox.addButton(QtWidgets.QDialogButtonBox.Ok) + self.confirmationBox.addButton(QtWidgets.QDialogButtonBox.Cancel) + self.confirmationBox.setContentsMargins(10, 0, 10, 10) + self.confirmationBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) + + ############################################# + # SETUP DIALOG LAYOUT + ################################# + + self.caption = QtWidgets.QLabel(self) + self.caption.setContentsMargins(8, 0, 8, 0) + self.caption.setProperty('class', 'invalid') + self.caption.setVisible(False) + + self.gridLayout = QtWidgets.QVBoxLayout(self) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.addWidget(self.formWidget) + self.gridLayout.addWidget(self.caption) + self.gridLayout.addWidget(self.confirmationBox, 0, QtCore.Qt.AlignRight) + + self.setFixedSize(self.sizeHint()) + self.setWindowIcon(QtGui.QIcon(':/icons/128/ic_eddy')) + self.setWindowTitle('New project from OWL file') + + connect(self.confirmationBox.accepted, self.accept) + connect(self.confirmationBox.rejected, self.reject) + + ############################################# + # INTERFACE + ################################# + + def name(self) -> str: + """ + Returns the value of the name field (trimmed). + """ + return self.nameField.value() + + ############################################# + # SLOTS + ################################# + + @QtCore.pyqtSlot() + def doValidateForm(self) -> None: + """ + Validate project settings. + """ + caption = '' + enabled = True + + ############################################# + # CHECK NAME + ################################# + + if not self.name(): + caption = '' + enabled = False + + ############################################# + + self.caption.setText(caption) + self.caption.setVisible(not isEmpty(caption)) + self.confirmationBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled) + self.setFixedSize(self.sizeHint()) diff --git a/eddy/ui/session.py b/eddy/ui/session.py index 42af57be..320cad39 100644 --- a/eddy/ui/session.py +++ b/eddy/ui/session.py @@ -166,7 +166,10 @@ GrapholIRIProjectLoader_v3, GrapholOntologyIRILoader_v3, ) -from eddy.core.loaders.owl2 import OwlOntologyImportSetWorker +from eddy.core.loaders.owl2 import ( + OwlOntologyImportSetWorker, + OwlProjectLoader, +) from eddy.core.network import NetworkManager from eddy.core.output import getLogger from eddy.core.owl import ( @@ -293,6 +296,8 @@ class Session( sgnIRIRemovedFromAllDiagrams = QtCore.pyqtSignal(IRI) sgnSingleNodeSwitchIRI = QtCore.pyqtSignal(AbstractNode, IRI) + sgnStartOwlImport = QtCore.pyqtSignal(str) + # Signals related to rendering options sgnRenderingModified = QtCore.pyqtSignal(str) @@ -314,6 +319,7 @@ def __init__( name: Optional[str] = None, iri: Optional[str] = None, prefix: Optional[str] = None, + owl_path: Optional[str] = None, **kwargs: str, ) -> None: """ @@ -381,15 +387,11 @@ def __init__( self.projectFromFile = True worker = self.createProjectLoader(File.Graphol, path, self) worker.run() - worker = OwlOntologyImportSetWorker(self.project) + elif owl_path: + self.projectFromFile = True + worker = self.createProjectLoader(File.Owl, owl_path, self) worker.run() - self.owlOntologyImportSize = worker.importSize - self.owlOntologyImportLoadedCount = worker.loadCount - if ( - self.owlOntologyImportSize > 0 - and self.owlOntologyImportSize != self.owlOntologyImportLoadedCount - ): - self.owlOntologyImportErrors = worker.owlOntologyImportErrors + self.project.name = name else: self.project = Project( parent=self, @@ -399,6 +401,14 @@ def __init__( profile=OWL2Profile(), ) + if self.projectFromFile: + worker = OwlOntologyImportSetWorker(self.project) + worker.run() + self.owlOntologyImportSize = worker.importSize + self.owlOntologyImportLoadedCount = worker.loadCount + if self.owlOntologyImportSize > self.owlOntologyImportLoadedCount: + self.owlOntologyImportErrors = worker.owlOntologyImportErrors + ############################################# # CONNECT PROJECT SIGNALS ################################# @@ -1159,6 +1169,7 @@ def initLoaders(self) -> None: self.addOntologyLoader(CsvLoader) self.addOntologyLoader(XlsxLoader) self.addProjectLoader(GrapholIRIProjectLoader_v3) + self.addProjectLoader(OwlProjectLoader) # noinspection PyArgumentList def initMenus(self) -> None: diff --git a/eddy/ui/welcome.py b/eddy/ui/welcome.py index 3656d92f..074124a7 100644 --- a/eddy/ui/welcome.py +++ b/eddy/ui/welcome.py @@ -71,14 +71,17 @@ ) from eddy.core.functions.signals import connect from eddy.ui.file import FileDialog -from eddy.ui.project import NewProjectDialog +from eddy.ui.project import ( + NewProjectDialog, + ProjectFromOWLDialog, +) class Welcome(QtWidgets.QDialog): """ This class is used to display the welcome screen of Eddy. """ - sgnCreateSession = QtCore.pyqtSignal([str], [str, str, str, str]) + sgnCreateSession = QtCore.pyqtSignal([str], [str, str, str, str], [str, str, str, str, str]) sgnOpenProject = QtCore.pyqtSignal(str) sgnUpdateRecentProjects = QtCore.pyqtSignal() @@ -152,6 +155,11 @@ def __init__( self.buttonOpenProject.setIconSize(QtCore.QSize(24, 24)) self.buttonOpenProject.setText('&Open project') connect(self.buttonOpenProject.clicked, self.doOpen) + self.buttonImportProject = PHCQPushButton(self) + self.buttonImportProject.setIcon(QtGui.QIcon(':/icons/24/ic_system_update')) + self.buttonImportProject.setIconSize(QtCore.QSize(24, 24)) + self.buttonImportProject.setText('Create project from OWL &file') + connect(self.buttonImportProject.clicked, self.doImportFromOWL) self.buttonHelp = PHCQToolButton(self) self.buttonHelp.setIcon(QtGui.QIcon(':/icons/24/ic_help_outline_black')) @@ -164,6 +172,7 @@ def __init__( self.buttonLayoutRT = QtWidgets.QVBoxLayout() self.buttonLayoutRT.addWidget(self.buttonNewProject) self.buttonLayoutRT.addWidget(self.buttonOpenProject) + self.buttonLayoutRT.addWidget(self.buttonImportProject) self.buttonLayoutRT.setContentsMargins(0, 38, 0, 0) self.buttonLayoutRT.setAlignment(QtCore.Qt.AlignHCenter) self.buttonLayoutRB = QtWidgets.QHBoxLayout() @@ -217,6 +226,7 @@ def __init__( connect(self.sgnCreateSession[str], application.doCreateSession) connect(self.sgnCreateSession[str, str, str, str], application.doCreateSession) + connect(self.sgnCreateSession[str, str, str, str, str], application.doCreateSession) connect(self.sgnOpenProject, self.doOpenProject) connect(self.sgnUpdateRecentProjects, self.doUpdateRecentProjects) @@ -326,6 +336,22 @@ def doOpenURL(self) -> None: if weburl: QtGui.QDesktopServices.openUrl(QtCore.QUrl(weburl)) + def doImportFromOWL(self): + """ + Bring up a modal dialog to create a new project from an OWL 2 file. + """ + dialog = FileDialog(self) + dialog.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen) + dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) + dialog.setViewMode(QtWidgets.QFileDialog.Detail) + dialog.setNameFilters([File.Owl.value]) + if dialog.exec_() == QtWidgets.QFileDialog.Accepted: + filePath = dialog.selectedFiles()[0] + form = ProjectFromOWLDialog(self) + if form.exec_() == ProjectFromOWLDialog.Accepted: + self.sgnCreateSession[str, str, str, str, str].emit( + None, form.name(), None, None, str(filePath)) + @QtCore.pyqtSlot(str) def doRemoveProject(self, path: str) -> None: """