/*
* Copyright (C) 2012 Jan Pokorsky
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.cas.lib.proarc.z3950;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.dom.DOMResult;
import org.jzkit.a2j.codec.util.OIDRegister;
import org.jzkit.a2j.gen.AsnUseful.EXTERNAL_type;
import org.jzkit.search.util.QueryModel.InvalidQueryException;
import org.jzkit.search.util.QueryModel.PrefixString.PrefixString;
import org.jzkit.search.util.QueryModel.QueryModel;
import org.jzkit.z3950.Z3950Exception;
import org.jzkit.z3950.client.SynchronousOriginBean;
import org.jzkit.z3950.client.ZClient;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.InitializeResponse_type;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.NamePlusRecord_type;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.PresentResponse_type;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.Records_type;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.SearchResponse_type;
import org.jzkit.z3950.gen.v3.Z39_50_APDU_1995.record_inline13_type;
import org.jzkit.z3950.util.Z3950Constants;
import org.marc4j.MarcReader;
import org.marc4j.MarcStreamReader;
import org.marc4j.MarcXmlWriter;
import org.marc4j.marc.Record;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.w3c.dom.Document;
/**
* Z3950Client uses prefix queries to search meta data in Marc21 format.
* It wraps {@link SynchronousOriginBean} the same way as {@link ZClient}.
*
* <p>Not thread safe!</p>
* <p>See {@code src/main/resources/log4j.properties} to configure jzkit logging.</p>
*
* @author Jan Pokorsky
*/
public final class Z3950Client {
private static final Logger LOG = Logger.getLogger(Z3950Client.class.getName());
private static Level LEVEL = Level.FINE;
private static final int MAX_RESULTS = 100;
private static final int SEARCH_PAGE_SIZE = MAX_RESULTS;
private final SynchronousOriginBean client;
private final int port;
private final String host;
private String query;
private String base;
private String recordFormat;
private final String current_result_set_name = "RS0";
public Z3950Client(String host, int port, String base) {
ClassPathXmlApplicationContext c = new ClassPathXmlApplicationContext("/z3950/Z3950ApplicationContext.xml");
client = new SynchronousOriginBean(new OIDRegister("/a2j.properties"));
client.setApplicationContext(c);
this.host = host;
this.port = port;
this.recordFormat = "marc21";
this.base = base;
}
public void close() {
client.disconnect();
}
/**
* Queries Z39.50 server.
* @param query RPN query
* @return found records in Marc21 format
* @see <a href='http://www.indexdata.com/zebra/doc/querymodel-rpn.html'>
* RPN queries and semantics</a>
*/
public Iterable<byte[]> search(String query) throws Z3950ClientException {
this.query = query;
try {
InitializeResponse_type conn = connect(host, port);
logConnection(conn, host, port);
if (!conn.result.booleanValue()) {
throw new Z3950ClientException(this, "Connection failed.");
}
SearchResponse_type search = find(query);
logSearchResponse(search);
List<NamePlusRecord_type> result = Collections.emptyList();
if (search.records != null && search.numberOfRecordsReturned.intValue() > 0) {
result = getRecords(search.records);
}
SearchResult searchResult = new SearchResult(
Math.min(MAX_RESULTS, search.resultCount.intValue()),
search.nextResultSetPosition.intValue(),
result);
return searchResult;
} catch (Z3950ClientException ex) {
throw ex; // do not wrap
} catch (Exception ex) {
throw new Z3950ClientException(this, ex);
}
}
private InitializeResponse_type connect(String host, int port) {
client.clearAllDatabases();
client.addDatatabse(base);
client.setRecordSyntax(recordFormat);
InitializeResponse_type resp = client.connect(host, port);
return resp;
}
private SearchResponse_type find(String query) throws Z3950Exception, InvalidQueryException {
QueryModel qm = new PrefixString(query);
SearchResponse_type resp = client.sendSearch(qm, null, current_result_set_name);
return resp;
}
private PresentResponse_type fetchRecords(int resultIndex, int pageSize) {
PresentResponse_type resp = client.sendPresent(resultIndex, pageSize, "F", current_result_set_name);
logPresentResponse(resp);
return resp;
}
private List<NamePlusRecord_type> getRecords(PresentResponse_type resp) {
if (resp.numberOfRecordsReturned.intValue() > 0) {
Records_type records = resp.records;
return getRecords(records);
}
return Collections.emptyList();
}
private List<NamePlusRecord_type> getRecords(Records_type records) {
if (records.which == Records_type.responserecords_CID) {
@SuppressWarnings("unchecked")
List<NamePlusRecord_type> l = (List<NamePlusRecord_type>) records.o;
return l;
} else {
throw new IllegalStateException("Unexpected Response Record CID: " + records.which);
}
}
private static byte[] process(NamePlusRecord_type npr) throws UnsupportedEncodingException {
LOG.log(LEVEL, String.format("NPR: name: %s, type: %s,\ncontent: %s", npr.name, npr.record.which, npr.record.o));
if (npr.record.which == record_inline13_type.retrievalrecord_CID) {
EXTERNAL_type external = (EXTERNAL_type) npr.record.o;
LOG.log(LEVEL, String.format("External Descriptor: %s", external.data_value_descriptor));
byte[] content = (byte[]) external.encoding.o;
// LOG.info("Content:\n" + new String(content, "cp1250"));
return content;
}
throw new IllegalStateException("Unexpected record type: " + npr.record.which);
}
/** Converts Marc21 to MarcXML */
public static Document toMarcXml(byte[] marc21, String charset) {
MarcReader reader = new MarcStreamReader(new ByteArrayInputStream(marc21), charset);
DOMResult result = new DOMResult();
MarcXmlWriter writer = new MarcXmlWriter(result);
if (reader.hasNext()) {
Record record = reader.next();
writer.write(record);
}
writer.close();
Document retval = (Document) result.getNode();
return retval;
}
private static void logPresentResponse(PresentResponse_type resp) {
if (resp == null) {
throw new NullPointerException();
}
if (!LOG.isLoggable(LEVEL)) {
return ;
}
LOG.log(LEVEL, String.format(
"Reference ID: %s"
+ ",\nPresent Status: %s"
+ ",\nNumber of Records: %s"
+ ",\nNext RS Position: %s"
+ ",\nOther Info: %s",
toString(resp.referenceId),
resp.presentStatus,
resp.numberOfRecordsReturned,
resp.nextResultSetPosition,
resp.otherInfo));
}
private static void logSearchResponse(SearchResponse_type resp) {
if (resp == null) {
throw new NullPointerException();
}
if (!LOG.isLoggable(LEVEL)) {
return ;
}
LOG.log(LEVEL, String.format(
"Reference ID: %s"
+ "\nSearch Status: %s"
+ "\nResult Count: %s"
+ "\nNum Records Returned: %s"
+ "\nNext RS position: %s",
toString(resp.referenceId),
resp.searchStatus,
resp.resultCount,
resp.numberOfRecordsReturned,
resp.nextResultSetPosition));
}
private static void logConnection(InitializeResponse_type resp, String host, int port) {
if (!resp.result.booleanValue()) {
LOG.log(Level.SEVERE, "Connection failed! {0}:{1}", new Object[]{host, port});
return ;
}
if (!LOG.isLoggable(LEVEL)) {
return ;
}
StringBuilder options = new StringBuilder();
for (int i = 0; i < Z3950Constants.z3950_option_names.length; i++) {
if (options.length() > 0) {
options.append(", ");
}
if (resp.options.isSet(i)) {
options.append(Z3950Constants.z3950_option_names[i]);
}
}
LOG.log(LEVEL, String.format(
"Reference ID: %s"
+ ",\nImplementation ID: %s"
+ ",\nName: %s"
+ ",\nVersion: %s"
+ ",\nOptions: %s",
toString(resp.referenceId),
resp.implementationId,
resp.implementationName,
resp.implementationVersion,
options
));
}
@Override
public String toString() {
return String.format("Z3950Client{%s:%s/%s, format: %s, query: %s}",
host, port, base, recordFormat, query);
}
private static String toString(byte[] b) {
return b == null ? String.valueOf(b) : new String(b);
}
private final class SearchResult implements Iterator<byte[]>, Iterable<byte[]> {
private int resultCount;
private List<NamePlusRecord_type> records;
private int resultIndex = 1;
private int recordIndex = 0;
public SearchResult(int resultCount, int resultIndex, List<NamePlusRecord_type> records) {
this.resultCount = resultCount;
this.resultIndex = resultIndex;
this.records = records;
}
@Override
public boolean hasNext() {
return resultCount > 0;
}
@Override
public byte[] next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
try {
if (records.isEmpty() || recordIndex >= records.size()) {
PresentResponse_type resp = fetchRecords(resultIndex, Math.min(SEARCH_PAGE_SIZE, resultCount));
records = getRecords(resp);
recordIndex = 0;
if (records.isEmpty()) {
throw new IllegalStateException(
String.format("resultCount: %s, resultIndex: %s", resultCount, resultIndex));
}
resultIndex = resp.nextResultSetPosition.intValue();
}
--resultCount;
NamePlusRecord_type record = records.get(recordIndex++);
return process(record);
} catch (Exception ex) {
throw new IllegalStateException(this.toString(), ex);
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public Iterator<byte[]> iterator() {
return this;
}
}
}