Commit 9cbdf261 authored by Günter Hipler's avatar Günter Hipler
Browse files

implementation of from / until parameters for List* verbs

parent 65edcd8a
...@@ -6,7 +6,7 @@ import org.elasticsearch.action.get.GetRequest ...@@ -6,7 +6,7 @@ import org.elasticsearch.action.get.GetRequest
import org.elasticsearch.action.search.{SearchRequest, SearchResponse, SearchScrollRequest} import org.elasticsearch.action.search.{SearchRequest, SearchResponse, SearchScrollRequest}
import org.elasticsearch.client.{RequestOptions, RestHighLevelClient} import org.elasticsearch.client.{RequestOptions, RestHighLevelClient}
import org.elasticsearch.common.unit.TimeValue import org.elasticsearch.common.unit.TimeValue
import org.elasticsearch.index.query.QueryBuilders import org.elasticsearch.index.query.{QueryBuilders, RangeQueryBuilder}
import org.elasticsearch.search.builder.SearchSourceBuilder import org.elasticsearch.search.builder.SearchSourceBuilder
import org.joda.time.DateTime import org.joda.time.DateTime
import org.swissbib.memobase.oai.common.util.{ESResumptionTokenHelper, ResumptionToken} import org.swissbib.memobase.oai.common.util.{ESResumptionTokenHelper, ResumptionToken}
...@@ -36,83 +36,52 @@ trait ElasticsearchComponent extends OaiRepository { ...@@ -36,83 +36,52 @@ trait ElasticsearchComponent extends OaiRepository {
metadataPrefix: String): Option[ResultList] = { metadataPrefix: String): Option[ResultList] = {
//todo - more / better / tested handling for different parameters //todo - more / better / tested handling for different parameters
//scroll API: https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-search-scroll.html
val searchResponse:Try[SearchResponse] = (from, until, set, resumptionToken, metadataPrefix) match { val searchResponse:Try[SearchResponse] = (from, until, set, resumptionToken, metadataPrefix) match {
case (Some(from), Some(until),_,None,_) => case (Some(from), Some(until),_,None,_) =>
//for the moment
val searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())
val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
Try[SearchResponse] {client.get.search(searchRequest, RequestOptions.DEFAULT)}
case (_, _,_,Some(resumptionToken),_) =>
val scrollRequest = new SearchScrollRequest(resumptionToken.subject);
scrollRequest.scroll(TimeValue.timeValueMinutes(3L));
Try[SearchResponse] {client.get.scroll(scrollRequest, RequestOptions.DEFAULT);}
//only for the moment until we have better data
case (_, _,_,_,_) =>
//for the moment
val searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())
val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
Try[SearchResponse] {client.get.search(searchRequest, RequestOptions.DEFAULT)}
}
searchResponse match {
case Success(searchResponse) =>
val si = searchResponse.getScrollId
//scroll_id is always the same -> for this specific context
//condition to finish the fetching is to compare the length of the resultlist
//compare: https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-search-scrolling.html
//otherwise we will get into an empty loop
val scrollId = if (searchResponse.getHits.getHits.length == 0) Option.empty[String] else Option(searchResponse.getScrollId)
val resumptionToken: Option[ResumptionToken] = scrollId.map(
ESResumptionTokenHelper(_))
val rqB = QueryBuilders
.rangeQuery("lastUpdatedDate")
.gte(from).format("strict_date_time")
.lte(until).format("strict_date_time")
val myMap = searchResponse.getHits.getHits.map(hit => (hit.getId, hit.getSourceAsMap)).map( val searchSourceBuilder = new SearchSourceBuilder().query(rqB).size(30)
sourceTuple => (sourceTuple._1, Json2XML.singleJsonDoc2Xml( val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
Json.parse(play.libs.Json.toJson(sourceTuple._2).toString))))
Option( ResultList(resumptionToken, Option(myMap.toSeq))) Try {client.get.search(searchRequest, RequestOptions.DEFAULT)}
case Failure(exception) =>
//todo we have to do something!!
Option.empty
}
} case (Some(from), None ,_,None,_) =>
//for the moment
val rqB = QueryBuilders
.rangeQuery("lastUpdatedDate")
.gte(from).format("strict_date_time")
val searchSourceBuilder = new SearchSourceBuilder().query(rqB)
val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
override def listRecords1(from: Option[String], Try {client.get.search(searchRequest, RequestOptions.DEFAULT)}
until: Option[String],
set: Option[String],
resumptionToken: Option[ResumptionToken],
metadataPrefix: String): Option[ResultListNew] = {
val searchResponse:Try[SearchResponse] = (from, until, set, resumptionToken, metadataPrefix) match { case (None, Some(until) ,_,None,_) =>
case (Some(from), Some(until),_,None,_) =>
//for the moment //for the moment
val searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()) val rqB = QueryBuilders
.rangeQuery("lastUpdatedDate")
.lte(until).format("strict_date_time")
val searchSourceBuilder = new SearchSourceBuilder().query(rqB)
val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L)) val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
Try[SearchResponse] {client.get.search(searchRequest, RequestOptions.DEFAULT)} Try {client.get.search(searchRequest, RequestOptions.DEFAULT)}
case (_, _,_,Some(resumptionToken),_) => case (_, _,_,Some(resumptionToken),_) =>
val scrollRequest = new SearchScrollRequest(resumptionToken.subject); val scrollRequest = new SearchScrollRequest(resumptionToken.subject)
scrollRequest.scroll(TimeValue.timeValueMinutes(3L)); scrollRequest.scroll(TimeValue.timeValueMinutes(3L))
Try[SearchResponse] {client.get.scroll(scrollRequest, RequestOptions.DEFAULT);} Try[SearchResponse] {client.get.scroll(scrollRequest, RequestOptions.DEFAULT)}
//only for the moment until we have better data //only for the moment until we have better data
case (_, _,_,_,_) => case (_, _,_,_,_) =>
//for the moment //for the moment
val searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()) val searchSourceBuilder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())
val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L)) val searchRequest = new SearchRequest().source(searchSourceBuilder).indices(index).scroll(TimeValue.timeValueMinutes(3L))
Try[SearchResponse] {client.get.search(searchRequest, RequestOptions.DEFAULT)} Try {client.get.search(searchRequest, RequestOptions.DEFAULT)}
} }
...@@ -125,7 +94,7 @@ trait ElasticsearchComponent extends OaiRepository { ...@@ -125,7 +94,7 @@ trait ElasticsearchComponent extends OaiRepository {
//condition to finish the fetching is to compare the length of the resultlist //condition to finish the fetching is to compare the length of the resultlist
//compare: https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-search-scrolling.html //compare: https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-search-scrolling.html
//otherwise we will get into an empty loop //otherwise we will get into an empty loop
val scrollId = if (searchResponse.getHits.getHits.length == 0) Option.empty[String] else Option(searchResponse.getScrollId) val scrollId = if (searchResponse.getHits.getHits.length < 30) None else Option(searchResponse.getScrollId)
val resumptionToken: Option[ResumptionToken] = scrollId.map( val resumptionToken: Option[ResumptionToken] = scrollId.map(
ESResumptionTokenHelper(_)) ESResumptionTokenHelper(_))
...@@ -145,7 +114,7 @@ trait ElasticsearchComponent extends OaiRepository { ...@@ -145,7 +114,7 @@ trait ElasticsearchComponent extends OaiRepository {
).toSeq ).toSeq
Option( ResultListNew(resumptionToken, Option(contentList))) Option( ResultList(resumptionToken, Option(contentList)))
case Failure(exception) => case Failure(exception) =>
//todo we have to do something!! //todo we have to do something!!
Option.empty Option.empty
...@@ -154,6 +123,7 @@ trait ElasticsearchComponent extends OaiRepository { ...@@ -154,6 +123,7 @@ trait ElasticsearchComponent extends OaiRepository {
} }
override def listIdentiers(from: Option[String], override def listIdentiers(from: Option[String],
until: Option[String], until: Option[String],
set: Option[String], set: Option[String],
...@@ -167,21 +137,39 @@ trait ElasticsearchComponent extends OaiRepository { ...@@ -167,21 +137,39 @@ trait ElasticsearchComponent extends OaiRepository {
override def getRecord(identifier: String, override def getRecord(identifier: String,
metadataPrefix: String): Option[(String, Elem)] = { metadataPrefix: String): Option[(String, OAIContent)] = {
val getRequest = new GetRequest(index, identifier) val getRequest = new GetRequest(index, identifier)
val getResponse = client.get.get(getRequest, RequestOptions.DEFAULT) val getResponse = client.get.get(getRequest, RequestOptions.DEFAULT)
//todo: what if response is empty? //todo: what if response is empty?
val sM: Option[(String, util.Map[String, AnyRef])] = Option( (getResponse.getId,getResponse.getSourceAsMap)) val sM: Option[(String, util.Map[String, AnyRef])] = Option( (getResponse.getId,getResponse.getSourceAsMap))
sM.map(tuple => (tuple._1,Json2XML.singleJsonDoc2Xml(Json.parse(play.libs.Json.toJson(tuple._2).toString)))) //sM.map(tuple => (tuple._1,Json2XML.singleJsonDoc2Xml(Json.parse(play.libs.Json.toJson(tuple._2).toString))))
sM.map(tuple => {
val esId = tuple._1
val esSourceMap = tuple._2.asScala
(
esId,
OAIContent(esId,
esSourceMap.getOrElse("id","").toString,
esSourceMap.getOrElse("document","").toString,
esSourceMap.getOrElse("format","").toString,
esSourceMap.getOrElse("published",false).asInstanceOf[Boolean],
esSourceMap.getOrElse("recordset",new util.ArrayList[String]()).asInstanceOf[util.ArrayList[String]].asScala.toList,
esSourceMap.getOrElse("institution",new util.ArrayList[String]()).asInstanceOf[util.ArrayList[String]].asScala.toList,
esSourceMap.getOrElse("lastUpdatedDate", "").asInstanceOf[String])
)
//(tuple._1,Json2XML.singleJsonDoc2Xml(Json.parse(play.libs.Json.toJson(tuple._2).toString))))
//val myMap = searchResponse.getHits.getHits.map(hit => (hit.getId, hit.getSourceAsMap)).map( //val myMap = searchResponse.getHits.getHits.map(hit => (hit.getId, hit.getSourceAsMap)).map(
// sourceTuple => (sourceTuple._1, Json2XML.singleJsonDoc2Xml( // sourceTuple => (sourceTuple._1, Json2XML.singleJsonDoc2Xml(
// Json.parse(play.libs.Json.toJson(sourceTuple._2).toString)))) // Json.parse(play.libs.Json.toJson(sourceTuple._2).toString))))
//Option(myMap.toSeq) //Option(myMap.toSeq)
})
} }
} }
...@@ -17,12 +17,6 @@ trait OaiRepository { ...@@ -17,12 +17,6 @@ trait OaiRepository {
resumptionToken: Option[ResumptionToken], resumptionToken: Option[ResumptionToken],
metadataPrefix: String): Option[ResultList] metadataPrefix: String): Option[ResultList]
def listRecords1(from: Option[String],
until: Option[String],
set: Option[String],
resumptionToken: Option[ResumptionToken],
metadataPrefix: String): Option[ResultListNew]
def listIdentiers(from: Option[String], def listIdentiers(from: Option[String],
...@@ -33,7 +27,7 @@ trait OaiRepository { ...@@ -33,7 +27,7 @@ trait OaiRepository {
def getRecord(identifier: String, def getRecord(identifier: String,
metadataPrefix: String metadataPrefix: String
): Option[(String, Elem)] ): Option[(String, OAIContent)]
......
...@@ -80,7 +80,8 @@ case class GetRecordResponse(runner: GetRecordRunner) extends OaiResponse { ...@@ -80,7 +80,8 @@ case class GetRecordResponse(runner: GetRecordRunner) extends OaiResponse {
makeHeader(identifier = runner.result.get._1) } makeHeader(identifier = runner.result.get._1) }
{if (runner.result.nonEmpty) {if (runner.result.nonEmpty)
<metadata> <metadata>
{runner.result.get._2}
{XML.loadString(runner.result.get._2.document)}
</metadata>} </metadata>}
{if (runner.result.isEmpty) {if (runner.result.isEmpty)
<metadata/>} <metadata/>}
...@@ -154,7 +155,8 @@ case class ListIdentifiersResponse(runner: OaiRequestRunner) extends OaiResponse ...@@ -154,7 +155,8 @@ case class ListIdentifiersResponse(runner: OaiRequestRunner) extends OaiResponse
{ {
runner.resultList.get.result.get.map( runner.resultList.get.result.get.map(
node => { node => {
{makeHeader(identifier = node._1)} {makeHeader(identifier = node.docId,
datestamp = Instant.parse(node.updateDate))}
} }
) )
} }
...@@ -225,10 +227,11 @@ case class ListRecordsResponse(runner: OaiRequestRunner) extends OaiResponse { ...@@ -225,10 +227,11 @@ case class ListRecordsResponse(runner: OaiRequestRunner) extends OaiResponse {
elem ++ elem ++
<ListRecords> <ListRecords>
{ {
runner.resultListnew.get.result.get.map( runner.resultList.get.result.get.map(
node => { node => {
<record> <record>
{makeHeader(identifier = node.docId)} {makeHeader(identifier = node.docId,
datestamp = Instant.parse(node.updateDate))}
<metadata>{XML.loadString(node.document)}</metadata> <metadata>{XML.loadString(node.document)}</metadata>
</record> </record>
} }
...@@ -243,8 +246,8 @@ case class ListRecordsResponse(runner: OaiRequestRunner) extends OaiResponse { ...@@ -243,8 +246,8 @@ case class ListRecordsResponse(runner: OaiRequestRunner) extends OaiResponse {
) )
*/ */
{if (runner.resultListnew.get.repositoryToken.isDefined) { {if (runner.resultList.get.repositoryToken.isDefined) {
<resumptionToken>{runner.resultListnew.get.repositoryToken.get.token}</resumptionToken> <resumptionToken>{runner.resultList.get.repositoryToken.get.token}</resumptionToken>
}} }}
} }
......
package org.swissbib.memobase.oai.runner package org.swissbib.memobase.oai.runner
import modules.OaiRepository import modules.{OAIContent, OaiRepository}
import org.swissbib.memobase.oai.request.{BadArgumentsReq, GetRecordReq, IdentifyReq, ListIdentifiersReq, ListIdentifiersReqExclusive, ListMetadataFormatsReq, ListRecordsReq, ListRecordsReqExclusive, ListSetsReq, ListSetsReqExclusive} import org.swissbib.memobase.oai.request.{BadArgumentsReq, GetRecordReq, IdentifyReq, ListIdentifiersReq, ListIdentifiersReqExclusive, ListMetadataFormatsReq, ListRecordsReq, ListRecordsReqExclusive, ListSetsReq, ListSetsReqExclusive}
import org.swissbib.memobase.oai.response.{BadArgumentsResponse, GetRecordResponse, IdentifyResponse, ListIdentifiersResponse, ListMetadaFormatsResponse, ListRecordsResponse, ListSetsResponse, OaiResponse} import org.swissbib.memobase.oai.response.{BadArgumentsResponse, GetRecordResponse, IdentifyResponse, ListIdentifiersResponse, ListMetadaFormatsResponse, ListRecordsResponse, ListSetsResponse, OaiResponse}
import play.api.Configuration import play.api.Configuration
import scala.xml.{Elem, Node}
sealed abstract class OaiRequestRunner { sealed abstract class OaiRequestRunner {
var result: Option[(String, Elem)] = None var result: Option[(String, OAIContent)] = None
var resultList: Option[ResultList] = None var resultList: Option[ResultList] = None
var resultListnew: Option[ResultListNew] = None
//var result: Option[NodeSeq] = None //var result: Option[NodeSeq] = None
def run(): OaiResponse def run(): OaiResponse
val config: Configuration val config: Configuration
...@@ -77,21 +75,13 @@ case class ListRecordsRunner(config: Configuration, repository: OaiRepository, r ...@@ -77,21 +75,13 @@ case class ListRecordsRunner(config: Configuration, repository: OaiRepository, r
ListRecordsReq) extends OaiRequestRunner { ListRecordsReq) extends OaiRequestRunner {
override def run(): OaiResponse = { override def run(): OaiResponse = {
//ich benötige einen genrischen Typ result //ich benötige einen genrischen Typ result
/*
resultList = repository.listRecords( resultList = repository.listRecords(
from = request.parameter.from, from = request.parameter.from,
until = request.parameter.until, until = request.parameter.until,
set = request.parameter.set, set = request.parameter.set,
resumptionToken = Option.empty, resumptionToken = Option.empty,
metadataPrefix = request.parameter.metadataPrefix) metadataPrefix = request.parameter.metadataPrefix)
*/
resultListnew = repository.listRecords1(
from = request.parameter.from,
until = request.parameter.until,
set = request.parameter.set,
resumptionToken = Option.empty,
metadataPrefix = request.parameter.metadataPrefix)
......
...@@ -5,7 +5,7 @@ import org.swissbib.memobase.oai.common.util.ResumptionToken ...@@ -5,7 +5,7 @@ import org.swissbib.memobase.oai.common.util.ResumptionToken
import scala.xml.Node import scala.xml.Node
case class ResultList(repositoryToken:Option[ResumptionToken], result:Option[Seq[(String,Node)]]) case class ResultList(repositoryToken:Option[ResumptionToken], result:Option[Seq[OAIContent]])
case class ResultListNew(repositoryToken:Option[ResumptionToken], result:Option[Seq[OAIContent]]) case class ResultListNew(repositoryToken:Option[ResumptionToken], result:Option[Seq[OAIContent]])
......
1) GetRecord
https://services.dnb.de/oai/repository?verb=GetRecord&metadataPrefix=MARC21-xml&identifier=oai:dnb.de/authorities/118540238
wenn der record nicht vorhanden ist
<?xml version="1.0" encoding="UTF-8"?>
<OAI-PMH
xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"
xmlns="http://www.openarchives.org/OAI/2.0/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<responseDate>2021-03-16T10:56:05Z</responseDate>
<request metadataPrefix="MARC21-xml" verb="GetRecord"
identifier="oai:dnb.de/authorities/11854023"
>https://services.dnb.de/oai/repository</request>
<error code="idDoesNotExist"/>
</OAI-PMH>
- welche error codes verwende ich?
- wie implementiere ich das?
...@@ -11,7 +11,7 @@ mit anschliessendem resumption token (max. 2 Minuten Zeitdauer zwischen den Aufr ...@@ -11,7 +11,7 @@ mit anschliessendem resumption token (max. 2 Minuten Zeitdauer zwischen den Aufr
https://oai.memobase.k8s.unibas.ch/?verb=ListRecords&resumptionToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJtZW1vYmFzZSBPQUkgc2VydmVyIiwic3ViIjoiRG5GMVpYSjVWR2hsYmtabGRHTm9Bd0FBQUFBQUFzZjBGbFZLZERSeFJXZEdVazVYVmpSdGJsWXpNWGQ1VjBFQUFBQUFBQTRyTkJadVdWWXllV0ZCV2xORFF6aHNRM1JCZDBkRFoxQkJBQUFBQUFBT0t6VVdibGxXTW5saFFWcFRRME00YkVOMFFYZEhRMmRRUVE9PSIsImV4cCI6MTYwMTI5Mzk0OCwiaWF0IjoxNjAxMjkzNzY4fQ.CmuujiNhY8zzRtRnlpOsgplbyNpPqWjtVtf-dIvD6PLsJuhIwS5ETHGQW_KK4mcS https://oai.memobase.k8s.unibas.ch/?verb=ListRecords&resumptionToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJtZW1vYmFzZSBPQUkgc2VydmVyIiwic3ViIjoiRG5GMVpYSjVWR2hsYmtabGRHTm9Bd0FBQUFBQUFzZjBGbFZLZERSeFJXZEdVazVYVmpSdGJsWXpNWGQ1VjBFQUFBQUFBQTRyTkJadVdWWXllV0ZCV2xORFF6aHNRM1JCZDBkRFoxQkJBQUFBQUFBT0t6VVdibGxXTW5saFFWcFRRME00YkVOMFFYZEhRMmRRUVE9PSIsImV4cCI6MTYwMTI5Mzk0OCwiaWF0IjoxNjAxMjkzNzY4fQ.CmuujiNhY8zzRtRnlpOsgplbyNpPqWjtVtf-dIvD6PLsJuhIwS5ETHGQW_KK4mcS
ListIdentifiers ListIdentifiers
https://oai.memobase.k8s.unibas.ch/?verb=ListIdentifiers&metadataPrefi x=1234 https://oai.memobase.k8s.unibas.ch/?verb=ListIdentifiers&metadataPrefix=1234
mit anschliessendem resumptionToken - maximal 2 Minuten Zeitdauer zwischen den Aufrufen mit anschliessendem resumptionToken - maximal 2 Minuten Zeitdauer zwischen den Aufrufen
aktuell Fehler! aktuell Fehler!
https://oai.memobase.k8s.unibas.ch/?verb=ListIdentifiers&resumptionToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJtZW1vYmFzZSBPQUkgc2VydmVyIiwic3ViIjoiRG5GMVpYSjVWR2hsYmtabGRHTm9Bd0FBQUFBQUFFeWFGbHAyZVVwb2FXWnpVVVJUTUdSdGVUbDZiSE4wY2tFQUFBQUFBQTRyZVJadVdWWXllV0ZCV2xORFF6aHNRM1JCZDBkRFoxQkJBQUFBQUFBT0szb1dibGxXTW5saFFWcFRRME00YkVOMFFYZEhRMmRRUVE9PSIsImV4cCI6MTYwMTI5NDE2NywiaWF0IjoxNjAxMjkzOTg3fQ.6dKO-1VRY4gybSBiC-wpUNd8SHFqiBAkv1YqG5w9AJ-hQz6AnN4UkHLwRKX1tjN4 https://oai.memobase.k8s.unibas.ch/?verb=ListIdentifiers&resumptionToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJtZW1vYmFzZSBPQUkgc2VydmVyIiwic3ViIjoiRG5GMVpYSjVWR2hsYmtabGRHTm9Bd0FBQUFBQUFFeWFGbHAyZVVwb2FXWnpVVVJUTUdSdGVUbDZiSE4wY2tFQUFBQUFBQTRyZVJadVdWWXllV0ZCV2xORFF6aHNRM1JCZDBkRFoxQkJBQUFBQUFBT0szb1dibGxXTW5saFFWcFRRME00YkVOMFFYZEhRMmRRUVE9PSIsImV4cCI6MTYwMTI5NDE2NywiaWF0IjoxNjAxMjkzOTg3fQ.6dKO-1VRY4gybSBiC-wpUNd8SHFqiBAkv1YqG5w9AJ-hQz6AnN4UkHLwRKX1tjN4
......
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment