Sorry. Last iteration. I understood what you meant by adding them directly to ResourceHash. I wonder why I didn't think of that!
Anyway. I'm committing this new version. Unless you have some objections? - Vishesh Handa On Wed, Jul 14, 2010 at 5:23 PM, Sebastian Trüg <[email protected]> wrote: > On 07/14/2010 01:30 PM, Vishesh Handa wrote: > > 8. You did it again: a static method named "toSparql". Please do > > not do > > that. :) > > > > > > But this time I added documentation as to what kind of query it > > creates. I understand that these kind of function names are bad, but > > I can't think of any alternative. Could you please suggest some name? > > well, you could always put the method in ResourceStruct and then keep > the name. Or you call it resourceStructToSparql or buildResourceQuery > > > As you can see I have no "real" comments since IMHO you did a > > great job. > > So please go ahead and commit that (maybe with some changes > > based on my > > comments) to trunk. Then testing can commence. :) > > > > > > Are you sure? Just say "Yes' and I'll commit it. > > yes >
Index: nepomukindexfeeder.cpp =================================================================== --- nepomukindexfeeder.cpp (revision 0) +++ nepomukindexfeeder.cpp (revision 0) @@ -0,0 +1,264 @@ +/* + Copyright (C) 2010 Vishesh Handa <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#include "nepomukindexfeeder.h" +#include "nrl.h" +#include "util.h" + +#include <QtCore/QDateTime> + +#include <Soprano/Model> +#include <Soprano/Statement> +#include <Soprano/QueryResultIterator> +#include <Soprano/Vocabulary/RDF> +#include <Soprano/Vocabulary/NAO> + +#include <Nepomuk/ResourceManager> +#include <Nepomuk/Resource> + +#include <KDebug> + + +Nepomuk::NepomukIndexFeeder::NepomukIndexFeeder(Soprano::Model* model, QObject* parent) + : QThread( parent ), + m_model( model ) +{ + m_stopped = false; + start(); +} + + +Nepomuk::NepomukIndexFeeder::~NepomukIndexFeeder() +{ + stop(); + wait(); +} + + +void Nepomuk::NepomukIndexFeeder::begin( const QUrl & url ) +{ + //kDebug() << "BEGINING"; + Request req; + req.uri = url; + + m_stack.push( req ); +} + + +void Nepomuk::NepomukIndexFeeder::addStatement(const Soprano::Statement& st) +{ + Q_ASSERT( !m_stack.isEmpty() ); + Request & req = m_stack.top(); + + const Soprano::Node & n = st.subject(); + + QUrl uriOrId; + if( n.isResource() ) + uriOrId = n.uri(); + else if( n.isBlank() ) + uriOrId = n.identifier(); + + if( !req.hash.contains( uriOrId ) ) { + ResourceStruct rs; + if( n.isResource() ) + rs.uri = n.uri(); + + req.hash.insert( uriOrId, rs ); + } + + ResourceStruct & rs = req.hash[ uriOrId ]; + rs.propHash.insert( st.predicate().uri(), st.object() ); +} + + +void Nepomuk::NepomukIndexFeeder::addStatement(const Soprano::Node& subject, const Soprano::Node& predicate, const Soprano::Node& object) +{ + addStatement( Soprano::Statement( subject, predicate, object, Soprano::Node() ) ); +} + + +void Nepomuk::NepomukIndexFeeder::end() +{ + if( m_stack.isEmpty() ) + return; + //kDebug() << "ENDING"; + + Request req = m_stack.pop(); + + m_queueMutex.lock(); + m_updateQueue.enqueue( req ); + + m_queueMutex.unlock(); + m_queueWaiter.wakeAll(); +} + + +void Nepomuk::NepomukIndexFeeder::stop() +{ + QMutexLocker lock( &m_queueMutex ); + m_stopped = true; + m_queueWaiter.wakeAll(); +} + + +QString Nepomuk::NepomukIndexFeeder::buildResourceQuery(const Nepomuk::NepomukIndexFeeder::ResourceStruct& rs) const +{ + QString query = QString::fromLatin1("select distinct ?r where { "); + + QList<QUrl> keys = rs.propHash.uniqueKeys(); + foreach( const QUrl & prop, keys ) { + const QList<Soprano::Node>& values = rs.propHash.values( prop ); + + foreach( const Soprano::Node & n, values ) { + query += " ?r " + Soprano::Node::resourceToN3( prop ) + " " + n.toN3() + " . "; + } + } + query += " } LIMIT 1"; + return query; +} + + +void Nepomuk::NepomukIndexFeeder::addToModel(const Nepomuk::NepomukIndexFeeder::ResourceStruct& rs) const +{ + QUrl context = generateGraph( rs.uri ); + QHashIterator<QUrl, Soprano::Node> iter( rs.propHash ); + while( iter.hasNext() ) { + iter.next(); + + Soprano::Statement st( rs.uri, iter.key(), iter.value(), context ); + //kDebug() << "ADDING : " << st; + m_model->addStatement( st ); + } +} + + +//BUG: When indexing a file, there is one main uri ( in Request ) and other additional uris +// If there is a statement connecting the main uri with the additional ones, it will be +// resolved correctly, but not if one of the additional one links to another additional one. +void Nepomuk::NepomukIndexFeeder::run() +{ + m_stopped = false; + while( !m_stopped ) { + + // lock for initial iteration + m_queueMutex.lock(); + + // work the queue + while( !m_updateQueue.isEmpty() ) { + Request request = m_updateQueue.dequeue(); + + // unlock after queue utilization + m_queueMutex.unlock(); + + // Search for the resources or create them + //kDebug() << " Searching for duplicates or creating them ... "; + QMutableHashIterator<QUrl, ResourceStruct> it( request.hash ); + while( it.hasNext() ) { + it.next(); + + // If it already exists + ResourceStruct & rs = it.value(); + if( !rs.uri.isEmpty() ) + continue; + + QString query = buildResourceQuery( rs ); + //kDebug() << query; + Soprano::QueryResultIterator it = m_model->executeQuery( query, Soprano::Query::QueryLanguageSparql ); + + if( it.next() ) { + //kDebug() << "Found exact match " << rs.uri << " " << it[0].uri(); + rs.uri = it[0].uri(); + } + else { + //kDebug() << "Creating .."; + rs.uri = ResourceManager::instance()->generateUniqueUri( QString() ); + + // Add to the repository + addToModel( rs ); + } + } + + // Fix links for main + ResourceStruct & rs = request.hash[ request.uri ]; + QMutableHashIterator<QUrl, Soprano::Node> iter( rs.propHash ); + while( iter.hasNext() ) { + iter.next(); + Soprano::Node & n = iter.value(); + + if( n.isEmpty() ) + continue; + + if( n.isBlank() ) { + const QString & id = n.identifier(); + if( !request.hash.contains( id ) ) + continue; + QUrl newUri = request.hash.value( id ).uri; + //kDebug() << id << " ---> " << newUri; + iter.value() = Soprano::Node( newUri ); + } + } + + // Add main file to the repository + addToModel( rs ); + + // lock for next iteration + m_queueMutex.lock(); + } + + // wait for more input + kDebug() << "Waiting..."; + m_queueWaiter.wait( &m_queueMutex ); + m_queueMutex.unlock(); + kDebug() << "Woke up."; + + } +} + + +QUrl Nepomuk::NepomukIndexFeeder::generateGraph( const QUrl& resourceUri ) const +{ + QUrl context = Nepomuk::ResourceManager::instance()->generateUniqueUri( "ctx" ); + + // create the provedance data for the data graph + // TODO: add more data at some point when it becomes of interest + QUrl metaDataContext = Nepomuk::ResourceManager::instance()->generateUniqueUri( "ctx" ); + m_model->addStatement( context, + Soprano::Vocabulary::RDF::type(), + Nepomuk::Vocabulary::NRL::DiscardableInstanceBase(), + metaDataContext ); + m_model->addStatement( context, + Soprano::Vocabulary::NAO::created(), + Soprano::LiteralValue( QDateTime::currentDateTime() ), + metaDataContext ); + m_model->addStatement( context, + Strigi::Ontology::indexGraphFor(), + resourceUri, + metaDataContext ); + m_model->addStatement( metaDataContext, + Soprano::Vocabulary::RDF::type(), + Nepomuk::Vocabulary::NRL::GraphMetadata(), + metaDataContext ); + m_model->addStatement( metaDataContext, + Nepomuk::Vocabulary::NRL::coreGraphMetadataFor(), + context, + metaDataContext ); + + return context; +} Index: nepomukindexwriter.cpp =================================================================== --- nepomukindexwriter.cpp (revision 1149492) +++ nepomukindexwriter.cpp (working copy) @@ -1,5 +1,6 @@ /* Copyright (C) 2007-2010 Sebastian Trueg <[email protected]> + Copyright (C) 2010 Vishesh Handa <[email protected]> This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -22,6 +23,7 @@ #include "nfo.h" #include "nie.h" #include "nrl.h" +#include "nepomukindexfeeder.h" #include <Soprano/Soprano> #include <Soprano/Vocabulary/RDF> @@ -118,6 +120,25 @@ namespace { return uri; } + class RegisteredFieldData + { + public: + RegisteredFieldData( const QUrl& prop, QVariant::Type t ) + : property( prop ), + dataType( t ), + isRdfType( prop == Vocabulary::RDF::type() ) { + } + + /// The actual property URI + QUrl property; + + /// the literal range of the property (if applicable) + QVariant::Type dataType; + + /// caching QUrl comparison + bool isRdfType; + }; + /** * Data objects that are used to store information relative to one * indexing run. @@ -128,7 +149,7 @@ namespace { FileMetaData( const Strigi::AnalysisResult* idx ); /// stores basic data including the nie:url and the nrl:GraphMetadata in \p model - void storeBasicData( Soprano::Model* model ); + void storeBasicData( Nepomuk::NepomukIndexFeeder* feeder ); /// map a blank node to a resource QUrl mapNode( const std::string& s ); @@ -142,37 +163,12 @@ namespace { /// The file info - saved to prevent multiple stats QFileInfo fileInfo; - /// The URI of the graph that contains all indexed statements - QUrl context; - /// a buffer for all plain-text content generated by strigi std::string content; private: /// The Strigi result const Strigi::AnalysisResult* m_analysisResult; - - /// mapping from blank nodes used in addTriplet to our urns - QMap<std::string, QUrl> m_blankNodeMap; - }; - - class RegisteredFieldData - { - public: - RegisteredFieldData( const QUrl& prop, QVariant::Type t ) - : property( prop ), - dataType( t ), - isRdfType( prop == Vocabulary::RDF::type() ) { - } - - /// The actual property URI - QUrl property; - - /// the literal range of the property (if applicable) - QVariant::Type dataType; - - /// caching QUrl comparison - bool isRdfType; }; FileMetaData::FileMetaData( const Strigi::AnalysisResult* idx ) @@ -185,92 +181,66 @@ namespace { // this will automatically find previous uses of the file in question // with backwards compatibility resourceUri = Nepomuk::Resource( fileUrl ).resourceUri(); - - // use a new random context URI - context = Nepomuk::ResourceManager::instance()->generateUniqueUri( "ctx" ); } - QUrl FileMetaData::mapNode( const std::string& s ) + void FileMetaData::storeBasicData( Nepomuk::NepomukIndexFeeder * feeder ) { - if ( s[0] == ':' ) { - if( m_blankNodeMap.contains( s ) ) { - return m_blankNodeMap[s]; - } - else { - QUrl urn = Nepomuk::ResourceManager::instance()->generateUniqueUri( QString() ); - m_blankNodeMap.insert( s, urn ); - return urn; - } - } - // special case to properly handle nie:isPartOf relations created for containers - else if ( s == m_analysisResult->path() ) { - return resourceUri; - } - else { - return QUrl::fromEncoded( s.c_str() ); - } - } - - void FileMetaData::storeBasicData( Soprano::Model* model ) - { - model->addStatement( resourceUri, Nepomuk::Vocabulary::NIE::url(), fileUrl, context ); + feeder->addStatement( resourceUri, Nepomuk::Vocabulary::NIE::url(), fileUrl ); // Strigi only indexes files and extractors mostly (if at all) store the nie:DataObject type (i.e. the contents) // Thus, here we go the easy way and mark each indexed file as a nfo:FileDataObject. - model->addStatement( resourceUri, + feeder->addStatement( resourceUri, Vocabulary::RDF::type(), - Nepomuk::Vocabulary::NFO::FileDataObject(), - context ); + Nepomuk::Vocabulary::NFO::FileDataObject() ); if ( fileInfo.isDir() ) { - model->addStatement( resourceUri, + feeder->addStatement( resourceUri, Vocabulary::RDF::type(), - Nepomuk::Vocabulary::NFO::Folder(), - context ); + Nepomuk::Vocabulary::NFO::Folder() ); } - - - // create the provedance data for the data graph - // TODO: add more data at some point when it becomes of interest - QUrl metaDataContext = Nepomuk::ResourceManager::instance()->generateUniqueUri( "ctx" ); - model->addStatement( context, - Vocabulary::RDF::type(), - Nepomuk::Vocabulary::NRL::DiscardableInstanceBase(), - metaDataContext ); - model->addStatement( context, - Vocabulary::NAO::created(), - LiteralValue( QDateTime::currentDateTime() ), - metaDataContext ); - model->addStatement( context, - Strigi::Ontology::indexGraphFor(), - resourceUri, - metaDataContext ); - model->addStatement( metaDataContext, - Vocabulary::RDF::type(), - Nepomuk::Vocabulary::NRL::GraphMetadata(), - metaDataContext ); - model->addStatement( metaDataContext, - Nepomuk::Vocabulary::NRL::coreGraphMetadataFor(), - context, - metaDataContext ); } FileMetaData* fileDataForResult( const Strigi::AnalysisResult* idx ) { return static_cast<FileMetaData*>( idx->writerData() ); } + + /** + * Creates a Blank or Resource Node based on the contents of the string provided. + * If the string is of the form ':identifier', a Blank node is created. + * Otherwise a Resource Node is returned. + */ + Soprano::Node createBlankOrResourceNode( const std::string & str ) { + QString identifier = QString::fromUtf8( str.c_str() ); + + if( !identifier.isEmpty() && identifier[0] == ':' ) { + identifier.remove( 0, 1 ); + return Soprano::Node::createBlankNode( identifier ); + } + + //Not a blank node + return Soprano::Node( QUrl(identifier) ); + } } class Strigi::NepomukIndexWriter::Private { public: - Private() + Private( Soprano::Model * model ) + : repository( model ) { literalTypes[FieldRegister::stringType] = QVariant::String; literalTypes[FieldRegister::floatType] = QVariant::Double; literalTypes[FieldRegister::integerType] = QVariant::Int; literalTypes[FieldRegister::binaryType] = QVariant::ByteArray; literalTypes[FieldRegister::datetimeType] = QVariant::DateTime; // Strigi encodes datetime as unsigned integer, i.e. addValue( ..., uint ) + + feeder = new Nepomuk::NepomukIndexFeeder( model ); + } + + ~Private() + { + delete feeder; } QVariant::Type literalType( const Strigi::FieldProperties& strigiType ) { @@ -310,6 +280,8 @@ public: QStack<const Strigi::AnalysisResult*> currentResultStack; + Nepomuk::NepomukIndexFeeder* feeder; + private: QHash<std::string, QVariant::Type> literalTypes; }; @@ -318,8 +290,7 @@ private: Strigi::NepomukIndexWriter::NepomukIndexWriter( Soprano::Model* model ) : Strigi::IndexWriter() { - d = new Private; - d->repository = model; + d = new Private( model ); Util::storeStrigiMiniOntology( d->repository ); } @@ -387,8 +358,11 @@ void Strigi::NepomukIndexWriter::startAn if ( data->resourceUri.isEmpty() ) data->resourceUri = Nepomuk::ResourceManager::instance()->generateUniqueUri( QString() ); + // Initialize the feeder to accept statements + d->feeder->begin( data->resourceUri ); + // store initial data to make sure newly created URIs are reused directly by libnepomuk - data->storeBasicData( d->repository ); + data->storeBasicData( d->feeder ); // remember the file data idx->setWriterData( data ); @@ -419,7 +393,7 @@ void Strigi::NepomukIndexWriter::addValu RegisteredFieldData* rfd = reinterpret_cast<RegisteredFieldData*>( field->writerData() ); // the statement we will create, we will determine the object below - Soprano::Statement statement( md->resourceUri, rfd->property, Soprano::Node(), md->context ); + Soprano::Statement statement( md->resourceUri, rfd->property, Soprano::Node() ); // // Strigi uses rdf:type improperly since it stores the value as a string. We have to @@ -461,12 +435,12 @@ void Strigi::NepomukIndexWriter::addValu if ( value[0] == ':' ) { Nepomuk::Types::Property property( rfd->property ); if ( property.range().isValid() ) { - statement.setObject( md->mapNode( value ) ); + statement.setObject( createBlankOrResourceNode( value ) ); } } } - d->repository->addStatement( statement ); + d->feeder->addStatement( statement ); } } @@ -504,10 +478,7 @@ void Strigi::NepomukIndexWriter::addValu val = QDateTime::fromTime_t( value ); } - d->repository->addStatement( Statement( md->resourceUri, - rfd->property, - val, - md->context) ); + d->feeder->addStatement( md->resourceUri, rfd->property, val); } @@ -522,10 +493,7 @@ void Strigi::NepomukIndexWriter::addValu FileMetaData* md = fileDataForResult( idx ); RegisteredFieldData* rfd = reinterpret_cast<RegisteredFieldData*>( field->writerData() ); - d->repository->addStatement( Statement( md->resourceUri, - rfd->property, - LiteralValue( value ), - md->context) ); + d->repository->addStatement( md->resourceUri, rfd->property, LiteralValue( value ) ); } @@ -540,10 +508,7 @@ void Strigi::NepomukIndexWriter::addValu FileMetaData* md = fileDataForResult( idx ); RegisteredFieldData* rfd = reinterpret_cast<RegisteredFieldData*>( field->writerData() ); - d->repository->addStatement( Statement( md->resourceUri, - rfd->property, - LiteralValue( value ), - md->context) ); + d->repository->addStatement( md->resourceUri, rfd->property, LiteralValue( value ) ); } @@ -555,17 +520,17 @@ void Strigi::NepomukIndexWriter::addTrip return; } - FileMetaData* md = fileDataForResult( d->currentResultStack.top() ); + //FileMetaData* md = fileDataForResult( d->currentResultStack.top() ); - QUrl subject = md->mapNode( s ); - Nepomuk::Types::Property property( md->mapNode( p ) ); + Soprano::Node subject( createBlankOrResourceNode( s ) ); + Nepomuk::Types::Property property( QUrl( QString::fromUtf8(p.c_str()) ) ); // Was mapped earlier Soprano::Node object; if ( property.range().isValid() ) - object = md->mapNode( o ); + object = Soprano::Node( createBlankOrResourceNode( o ) ); else object = Soprano::LiteralValue::fromString( QString::fromUtf8( o.c_str() ), property.literalRangeType().dataTypeUri() ); - d->repository->addStatement( subject, property.uri(), object, md->context ); + d->feeder->addStatement( subject, property.uri(), object ); } @@ -582,14 +547,13 @@ void Strigi::NepomukIndexWriter::finishA // store the full text of the file if ( md->content.length() > 0 ) { - d->repository->addStatement( Statement( md->resourceUri, + d->feeder->addStatement( md->resourceUri, Nepomuk::Vocabulary::NIE::plainTextContent(), - LiteralValue( QString::fromUtf8( md->content.c_str() ) ), - md->context ) ); - if ( d->repository->lastError() ) - kDebug() << "Failed to add" << md->resourceUri << "as text" << QString::fromUtf8( md->content.c_str() ); + LiteralValue( QString::fromUtf8( md->content.c_str() ) ) ); } + d->feeder->end(); + // cleanup delete md; idx->setWriterData( 0 ); Index: nepomukindexfeeder.h =================================================================== --- nepomukindexfeeder.h (revision 0) +++ nepomukindexfeeder.h (revision 0) @@ -0,0 +1,143 @@ +/* + Copyright (C) 2010 Vishesh Handa <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#ifndef NEPOMUKINDEXFEEDER_H +#define NEPOMUKINDEXFEEDER_H + +#include <QtCore/QThread> +#include <QtCore/QMutex> +#include <QtCore/QWaitCondition> +#include <QtCore/QUrl> +#include <QtCore/QQueue> +#include <QtCore/QStack> +#include <QtCore/QSet> + +namespace Soprano { + class Model; + class Statement; + class Node; +} + +namespace Nepomuk { + class NepomukIndexFeeder : public QThread + { + Q_OBJECT + public: + NepomukIndexFeeder( Soprano::Model* model, QObject* parent = 0); + virtual ~NepomukIndexFeeder(); + + void stop(); + void run(); + + public Q_SLOTS: + /** + * Starts the feeding process for a file with resourceUri \p uri. + * This function should be called before adding any statements. + * The function may be called repeatedly. + * + * Should be preceeded by an end() + * + * \sa end + */ + void begin( const QUrl & uri ); + + /** + * Adds \p st to the list of statements to be added. + * \p st may contain Blank Nodes.The context is ignored. + * Should be called between begin and end + * + * \sa begin end + */ + void addStatement( const Soprano::Statement & st ); + + /** + * Adds the subject, predicate, object to the list of statements + * to be added. The Subject or Object may contain Blank Nodes + * Should be called between begin and end + * + * \sa begin end + */ + void addStatement( const Soprano::Node & subject, + const Soprano::Node & predicate, + const Soprano::Node & object ); + + /** + * Finishes the feeding process, and starts with the resolution and merging + * of resources based on the statements provided. + * + * addStatement should not be called after this function, unless begin has already + * been called. + * + * \sa begin + */ + void end(); + + private: + + struct ResourceStruct { + QUrl uri; + QMultiHash<QUrl, Soprano::Node> propHash; + }; + + // Maps the uri to the ResourceStuct + typedef QHash<QUrl, ResourceStruct> ResourceHash; + + struct Request { + QUrl uri; + ResourceHash hash; + }; + + /// The thread uses this queue to check if it has any requests that need processing + QQueue<Request> m_updateQueue; + + /** + * The stack is used to store the internal state of the Feeder, a new item is pushed into + * the stack everytime begin() is called, and the top most item is poped and sent into the + * update queue when end() is called. + */ + QStack<Request> m_stack; + + Soprano::Model* m_model; + + QMutex m_queueMutex; + QWaitCondition m_queueWaiter; + bool m_stopped; + + /// Generates a discardable graph for \p resourceUri + QUrl generateGraph( const QUrl& resourceUri ) const; + + /** + * Creates a sparql query which returns 1 resource which matches all the properties, + * and objects present in the propHash of the ResourceStruct + */ + QString buildResourceQuery( const ResourceStruct & rs ) const; + + /** + * Adds all the statements present in the ResourceStruct to the internal model. + * The contex is created via generateGraph + * + * \sa generateGraph + */ + void addToModel( const ResourceStruct &rs ) const; + }; + +} + +#endif // NEPOMUKINDEXFEEDER_H Index: CMakeLists.txt =================================================================== --- CMakeLists.txt (revision 1149492) +++ CMakeLists.txt (working copy) @@ -11,6 +11,7 @@ set( strigi_nepomuk_indexer_SRCS nepomukindexmanager.cpp nepomukindexreader.cpp nepomukindexwriter.cpp + nepomukindexfeeder.cpp util.cpp )
_______________________________________________ Nepomuk mailing list [email protected] https://mail.kde.org/mailman/listinfo/nepomuk
