/* See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Esri Inc. licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.esri.gpt.server.csw.client;

import com.esri.gpt.framework.robots.Bots;
import java.io.*;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import org.xml.sax.SAXException;

/**
 * CswSearchRequest class.
 * 
 * CswSearchRequest class is used to submit CSW search queries and to
 * return CSW search results. Before submiting a request, you need to specify a
 * catalog and provide search criteria.
 */
public class CswSearchRequest {

private static final Logger LOG = 
  Logger.getLogger(CswSearchRequest.class.getCanonicalName());

private CswCatalog        catalog;
private CswSearchCriteria criteria;
private CswClient         cswClient;
private CswSearchResponse response;
private final Bots bots;


public CswSearchRequest(Bots bots) {
  this(null, null, bots);
}

public CswSearchRequest(CswCatalog catalog, String searchText, Bots bots) {
  this.bots =bots;
  this.catalog = catalog;
  // Initialize the neccessary objects
  // create search criteria
  this.criteria = new CswSearchCriteria();
  this.criteria.setSearchText(searchText);
  // create csw client
  this.cswClient = new CswClient(bots);
  this.response  = new CswSearchResponse();
}

/**
 * Accessor methods
 */
public CswCatalog getCatalog() {
  return this.catalog;
}

public void setCatalog(CswCatalog catalog) {
  this.catalog = catalog;
  this.cswClient.setBatchHttpClient(catalog.getBatchHttpClient());
}

public CswSearchCriteria getCriteria() {
  return this.criteria;
}

public void setCriteria(CswSearchCriteria criteria) {
  this.criteria = criteria;
}

public CswClient getCswClient() {
  return this.cswClient;
}

public void setCswClient(CswClient cswClient) {
  this.cswClient = cswClient;
}

public CswSearchResponse getCswSearchResponse() {
  return this.response;
}

public void setCswSearchResponse(CswSearchResponse response) {
  this.response = response;
}

/**
 * Retrieve metadata from CSW service by its ID
 * 
 * @param DocID    Metadata document ID
 */
public void getMetadataByID(String DocID) throws NullReferenceException,
    IOException, InvalidOperationException, TransformerException {
  if (DocID == null || DocID.length() == 0) {
    throw new NullReferenceException("No DocID specified");
  }
  if (catalog == null) {
    throw new NullReferenceException("Catalog not specified.");
  }
  if (catalog.getCapabilities() == null) {
    throw new NullReferenceException("Catalog capabilities not initialized.");
  }
  if (catalog.getProfile() == null) {
    throw new NullReferenceException("Catalog profile not specified.");
  }

  if (catalog.getCapabilities().get_getRecordByIDGetURL() == null
      || catalog.getCapabilities().get_getRecordByIDGetURL().length() == 0) {
    throw new NullReferenceException(
        "GetRecordByID URL not specified for the catalog capabilities.");
  }

  CswProfile profile = catalog.getProfile();

  // generate request url
  String getRecordByIDBaseUrl = catalog.getCapabilities()
      .get_getRecordByIDGetURL();

  CswRecord record = this.getRecordById(getRecordByIDBaseUrl, DocID, profile);
  
 
  // add record to the response
  CswRecords records = new CswRecords();
  if (record != null) {
    records.add(record);
  }
  response.setRecords(records);

}

/**
 * Gets the record by id.
 * 
 * @param requestURL the request URL (CSW service url)
 * @param DocID the doc ID (GUID or UUID)
 * @param profile the profile
 * 
 * @return the record by id
 * 
 * @throws IOException Signals that an I/O exception has occurred.
 * @throws TransformerException the transformer exception
 * @throws NullReferenceException the null reference exception
 * @throws InvalidOperationException the invalid operation exception
 */
@SuppressWarnings("unchecked")
public CswRecord getRecordById(String requestURL, String DocID,
    CswProfile profile) throws IOException, TransformerException,
    NullReferenceException, InvalidOperationException {

  String requestUrl = profile.generateCSWGetMetadataByIDRequestURL(requestURL,
      DocID);

  if (cswClient == null) {
    cswClient = new CswClient(bots);
    cswClient.setBatchHttpClient(catalog.getBatchHttpClient());
  }

  BufferedInputStream bStream = null;
  InputStream istIntermidiateDoc = null;
  InputStream istRealDoc = null;
  CswRecord record = null;

  try {
    istIntermidiateDoc = 
      cswClient.submitHttpRequest("GET", requestUrl,
      "");

    
    String responseStr = Utils.getInputString2(istIntermidiateDoc);
    
    LOG.log(Level.FINER, "Get Record By Id intermidiate XML = {0}", responseStr);
    response.setResponseXML(Utils.chkStr(responseStr));

    record = new CswRecord();
    CswRecords recordList = null;
    try {
      LOG.finer("GetRecordById: Making csw record object using g" +
      		"etRecordsResponse operation");
      // making cswObject by going through getrecord response xslt
      CswResult results = new CswResult();
      profile.readGetRecordsResponse(responseStr, results);
      recordList = results.getRecords();
      Iterator iter = recordList.iterator();
      if(iter.hasNext()) {
        Object obj = iter.next();
        if(obj instanceof CswRecord ) {
          record = (CswRecord) obj;
        }
      } else  {
        LOG.log(Level.WARNING, 
            "Could not get csw metadata of metadata document");
      }
    } catch (ParserConfigurationException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    } catch (SAXException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    } catch (XPathExpressionException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    }
    record.setId(DocID);
    LOG.finer("GetRecordByID: Transforming intermidiate xml to populate xml " +
    		"into csw Record");
    profile.readCSWGetMetadataByIDResponse(getCswClient(),responseStr, record);
    if (record == null) {
      throw new NullReferenceException("Record not populated.");
    }
    

    // check if full metadata or resourceURL has been returned
    boolean hasFullMetadata = !(record.getFullMetadata() == null || record
        .getFullMetadata().equals(""));
    boolean hasResourceUrl = !(record.getMetadataResourceURL() == null || record
        .getMetadataResourceURL().equals(""));
    if(!hasResourceUrl && requestUrl != null ) {
      record.setMetadataResourceURL(requestUrl);
      hasResourceUrl = true;
    } 
    
    if (!hasFullMetadata && !hasResourceUrl) {
      throw new InvalidOperationException("Neither full metadata nor metadata"
          + " resource URL was found for the CSW record.");
    } 
  }
  finally {
    Utils.close(istIntermidiateDoc);
    Utils.close(bStream);
  }
  return record;

}

/**
 * Gets the record by id.
 * 
 * @param requestURL the request URL (CSW service url)
 * @param DocID the doc ID (GUID or UUID)
 * @param profile the profile
 * 
 * @return the record by id
 * 
 * @throws IOException Signals that an I/O exception has occurred.
 * @throws TransformerException the transformer exception
 * @throws NullReferenceException the null reference exception
 * @throws InvalidOperationException the invalid operation exception
 */
@SuppressWarnings("unchecked")
public CswRecord getRecordById(String requestURL, String DocID,
    CswProfile profile, String username, String password) 
    throws IOException, TransformerException,
    NullReferenceException, InvalidOperationException {

  String requestUrl = profile.generateCSWGetMetadataByIDRequestURL(requestURL,
      DocID);

  if (cswClient == null) {
    cswClient = new CswClient(bots);
    cswClient.setBatchHttpClient(catalog.getBatchHttpClient());
  }

  BufferedInputStream bStream = null;
  InputStream istIntermidiateDoc = null;
  InputStream istRealDoc = null;
  CswRecord record = null;

  try {
    istIntermidiateDoc = 
      cswClient.submitHttpRequest("GET", requestUrl,
      "", username, password);

    
    String responseStr = Utils.getInputString2(istIntermidiateDoc);
    
    LOG.log(Level.FINER, "Get Record By Id intermidiate XML = {0}", responseStr);
    response.setResponseXML(Utils.chkStr(responseStr));

    record = new CswRecord();
    CswRecords recordList = new CswRecords();
    try {
      LOG.finer("GetRecordById: Making csw record object using g" +
          "etRecordsResponse operation");
      // making cswObject by going through getrecord response xslt
      profile.readCSWGetRecordsResponse(responseStr, recordList);
      Iterator iter = recordList.iterator();
      if(iter.hasNext()) {
        Object obj = iter.next();
        if(obj instanceof CswRecord ) {
          record = (CswRecord) obj;
        }
      } else  {
        LOG.log(Level.WARNING, 
            "Could not get csw metadata of metadata document");
      }
    } catch (ParserConfigurationException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    } catch (SAXException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    } catch (XPathExpressionException e) {
      LOG.log(Level.INFO, 
        "Could not get csw metadata of metadata document (maybe this csw does " 
        + "not have csw metadata on getRecord by id. " + "{0}", e.getMessage());
    }
    record.setId(DocID);
    LOG.finer("GetRecordByID: Transforming intermidiate xml to populate xml " +
        "into csw Record");
    profile.readCSWGetMetadataByIDResponse(getCswClient(), responseStr, record);
    if (record == null) {
      throw new NullReferenceException("Record not populated.");
    }
    

    // check if full metadata or resourceURL has been returned
    boolean hasFullMetadata = !(record.getFullMetadata() == null || record
        .getFullMetadata() == "");
    boolean hasResourceUrl = !(record.getMetadataResourceURL() == null || record
        .getMetadataResourceURL() == "");
    
    if(!hasResourceUrl && requestUrl != null ) {
      record.setMetadataResourceURL(requestUrl);
      hasResourceUrl = true;
    }

    if (!hasFullMetadata && !hasResourceUrl) {
      throw new InvalidOperationException("Neither full metadata nor metadata"
          + " resource URL was found for the CSW record.");
    } else if (hasResourceUrl) {
      // need to load metadata from resource URL
      istRealDoc = cswClient.submitHttpRequest("GET", Utils.chkStr(record
          .getMetadataResourceURL()), "", username, password);

      responseStr = Utils.getInputString2(istRealDoc);

      record.setFullMetadata(responseStr);
    }
  }
  finally {
    Utils.close(istIntermidiateDoc);
    Utils.close(bStream);
  }
  return record;

}



/**
 * Get the CSW search response of a CSW search request
 * 
 * Get the CSW search response of a CSW search request
 * @return a CswSearchResponse object.
 */
public CswSearchResponse getResponse() {
  return this.response;
}

/**
 * Search CSW catalog using the provided criteria. Search result can be accessed
 * by calling GetResponse().
 * @throws IOException 
 * @throws SAXException 
 * @throws ParserConfigurationException 
 * @throws XPathExpressionException 
 * @throws TransformerException 
 */
public void search() throws NullReferenceException, XPathExpressionException,
    ParserConfigurationException, SAXException, IOException,
    TransformerException {
  // Check the necessary info. to search csw
  if (catalog == null) {
    throw new NullReferenceException("No catalog specified");
  }
  if (criteria == null) {
    throw new NullReferenceException("No specified Criteria");
  }
  if (catalog.getUrl() == null || catalog.getUrl().length() == 0) {
    throw new NullReferenceException("No specified url");
  }
  if (catalog.getProfile() == null) {
    throw new NullReferenceException("No specified profile");
  }

  // Generate getRecords query
  CswProfile profile = catalog.getProfile();

  catalog.connect(bots);
  CswCatalogCapabilities capabilities = catalog.getCapabilities();

  String requestUrl = capabilities.get_getRecordsPostURL();
  String requestQuery = profile.generateCSWGetRecordsRequest(criteria);

  
  // Submit search query and get response as an InputStream object
  InputStream responseStream = cswClient.submitHttpRequest("POST", requestUrl,
      requestQuery);
 
  BufferedInputStream bIStream = new BufferedInputStream(responseStream);
 
  try {
//    String responseStr = Utils.getInputString(bIStream);
    String responseStr = readCharacters(bIStream, "UTF-8");
    // Transform input xml to output xml with Profile
    CswRecords records = new CswRecords();

    profile.readCSWGetRecordsResponse(responseStr, records);

    response.set_requestStr(requestQuery);
    response.setRecords(records);
    response.setResponseXML(responseStr);
  } finally {
    Utils.close(bIStream);
    Utils.close(responseStream);
  }

}

/**
 * Fully reads the characters from an input stream.
 * @param stream the input stream
 * @param charset the encoding of the input stream
 * @return the characters read
 * @throws IOException if an exception occurs
 */
private String readCharacters(InputStream stream, String charset)
  throws IOException {
  StringBuilder sb = new StringBuilder();
  BufferedReader br = null;
  InputStreamReader ir = null;
  try {
    if ((charset == null) || (charset.trim().length() == 0)) charset = "UTF-8";
    char cbuf[] = new char[2048];
    int n = 0;
    int nLen = cbuf.length;
    ir = new InputStreamReader(stream,charset);
    br = new BufferedReader(ir);
    while ((n = br.read(cbuf,0,nLen)) > 0) sb.append(cbuf,0,n);
  } finally {
    try {if (br != null) br.close();} catch (Exception ef) {}
    try {if (ir != null) ir.close();} catch (Exception ef) {}
  }
  return sb.toString();
}

}