/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.dt.point;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import ucar.ma2.Array;
import ucar.ma2.ArrayChar;
import ucar.ma2.ArrayDouble;
import ucar.ma2.DataType;
import ucar.ma2.StructureData;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.NetcdfFileSubclass;
import ucar.nc2.Variable;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.conv.CF1Convention;
import ucar.nc2.dt.DataIterator;
import ucar.nc2.dt.PointObsDataset;
import ucar.nc2.dt.PointObsDatatype;
import ucar.nc2.dt.TypedDatasetFactory;
import ucar.nc2.ft.FeatureDatasetPoint;
import ucar.nc2.ft.DsgFeatureCollection;
import ucar.nc2.ft.PointFeatureCollection;
import ucar.nc2.ft.PointFeature;
import ucar.nc2.ft.PointFeatureCollection;
import ucar.nc2.iosp.netcdf3.N3outputStreamWriter;
import ucar.unidata.geoloc.EarthLocation;

/**
 * Write point data in CF point convention.
 * Also experiment with streaming netcdf.
 *
 * @deprecated use WriterCFPointCollection
 * @see "http://cf-pcmdi.llnl.gov/trac/wiki/PointObservationConventions"
 * @author caron
 */
public class WriterCFPointDataset {
  private static final String recordDimName = "record";
  private static final String latName = "latitude";
  private static final String lonName = "longitude";
  private static final String altName = "altitude";
  private static final String timeName = "time";

  private NetcdfFileStream ncfileOut;
  private List<Attribute> globalAtts;
  private String altUnits;

  private Set<Dimension> dimSet = new HashSet<Dimension>();
  private List<Variable> recordVars = new ArrayList<Variable>();

  private boolean useAlt = false;

  private boolean debug = false;

  public WriterCFPointDataset(DataOutputStream stream, List<Attribute> globalAtts, String altUnits) {
    ncfileOut = new NetcdfFileStream(stream);
    this.globalAtts = globalAtts;
    this.altUnits = altUnits;
    useAlt = (altUnits != null);
  }

  public void writeHeader(List<? extends VariableSimpleIF> vars, int numrec) throws IOException {
    createGlobalAttributes();
    createRecordVariables(vars);

    ncfileOut.finish(); // done with define mode
    ncfileOut.writeHeader(numrec);
  }

  private void createGlobalAttributes() {
    if (globalAtts != null) {
      for (Attribute att : globalAtts) {
        if (att.getShortName().equalsIgnoreCase("cdm_data_type")) continue;
        if (att.getShortName().equalsIgnoreCase("cdm_datatype")) continue;
        if (att.getShortName().equalsIgnoreCase("thredds_data_type")) continue;

        ncfileOut.addAttribute(null, att);
      }
    }
    ncfileOut.addAttribute(null, new Attribute(CDM.CONVENTIONS, "CF-1"));  // LOOK CF-1.?
    ncfileOut.addAttribute(null, new Attribute(CF.FEATURE_TYPE, CF.FeatureType.point.name()));
  }

  // private ArrayInt.D1 timeArray = new ArrayInt.D1(1);
  //private ArrayInt.D1 parentArray = new ArrayInt.D1(1);

  private void createRecordVariables(List<? extends VariableSimpleIF> dataVars) {

    ncfileOut.addDimension(null, new Dimension(recordDimName, 0, true, true, false));

    // time variable
    Variable timeVar = ncfileOut.addVariable(null, timeName, DataType.DOUBLE, recordDimName);
    timeVar.addAttribute(new Attribute(CDM.UNITS, "secs since 1970-01-01 00:00:00"));
    timeVar.addAttribute(new Attribute(CDM.LONG_NAME, "date/time of observation"));
    recordVars.add(timeVar);

    // latitude variable
    Variable latVar = ncfileOut.addVariable(null, latName, DataType.DOUBLE, recordDimName);
    latVar.addAttribute(new Attribute(CDM.UNITS, "degrees_north"));
    latVar.addAttribute(new Attribute(CDM.LONG_NAME, "latitude of observation"));
    latVar.addAttribute(new Attribute("standard_name", "latitude"));
    recordVars.add(latVar);

    // longitude variable
    Variable lonVar = ncfileOut.addVariable(null, lonName, DataType.DOUBLE, recordDimName);
    lonVar.addAttribute(new Attribute(CDM.UNITS, "degrees_east"));
    lonVar.addAttribute(new Attribute(CDM.LONG_NAME, "longitude of observation"));
    lonVar.addAttribute(new Attribute("standard_name", "longitude"));
    recordVars.add(lonVar);

    if (useAlt) {
      // altitude variable
      Variable altVar = ncfileOut.addVariable(null, altName, DataType.DOUBLE, recordDimName);
      altVar.addAttribute(new Attribute(CDM.UNITS, altUnits));
      altVar.addAttribute(new Attribute(CDM.LONG_NAME, "altitude of observation"));
      altVar.addAttribute(new Attribute("standard_name", "longitude"));
      altVar.addAttribute(new Attribute(CF.POSITIVE, CF1Convention.getZisPositive(altName, altUnits)));
      recordVars.add(altVar);
    }

    String coordinates = timeName + " " + latName + " " + lonName;
    if (useAlt) coordinates = coordinates + " " + altName;
    Attribute coordAtt = new Attribute(CF.COORDINATES, coordinates);

    // find all dimensions needed by the data variables
    for (VariableSimpleIF var : dataVars) {
      List<Dimension> dims = var.getDimensions();
      dimSet.addAll(dims);
    }

    // add them
    for (Dimension d : dimSet) {
      if (isExtraDimension(d))
        ncfileOut.addDimension(null, new Dimension(d.getShortName(), d.getLength(), true, false, d.isVariableLength()));
    }

    // add the data variables all using the record dimension
    for (VariableSimpleIF oldVar : dataVars) {
      if (ncfileOut.findVariable(oldVar.getShortName()) != null) continue;
      List<Dimension> dims = oldVar.getDimensions();
      StringBuilder dimNames = new StringBuilder(recordDimName);
      for (Dimension d : dims) {
        if (isExtraDimension(d))
          dimNames.append(" ").append(d.getShortName());
      }
      Variable newVar = ncfileOut.addVariable(null, oldVar.getShortName(), oldVar.getDataType(), dimNames.toString());
      recordVars.add(newVar);

      List<Attribute> atts = oldVar.getAttributes();
      for (Attribute att : atts)
        newVar.addAttribute(att);
      newVar.addAttribute(coordAtt);
    }

  }

  // LOOK kludge to identify time dimension
  private boolean isExtraDimension(Dimension d) {
    return (!d.isUnlimited() && !d.getShortName().equalsIgnoreCase("time"));
  }

  public void writeRecord(double lat, double lon, double alt, Date time, double[] vals, String[] svals) throws IOException {
    int count = writeCoordinates(lat, lon, alt, time);

    Variable v;

    // double data
    for (double val : vals) {
      ArrayDouble.D0 data = new ArrayDouble.D0();
      data.set(val);
      v = recordVars.get(count++);
      v.setCachedData(data, false);
    }

    // String data
    for (String sval : svals) {
      v = recordVars.get(count++);
      int strlen = v.getShape(1);
      ArrayChar data = ArrayChar.makeFromString(sval, strlen);
      v.setCachedData(data, false);
    }

    ncfileOut.writeRecordData(recordVars);
  }

  private int writeCoordinates(double lat, double lon, double alt, Date time) {
    int count = 0;

    // time
    ArrayDouble.D0 tdata = new ArrayDouble.D0();
    double secs = (double) (time.getTime() / 1000.);
    tdata.set(secs);
    Variable v = recordVars.get(count++);
    v.setCachedData(tdata, false);

    // lat
    ArrayDouble.D0 latData = new ArrayDouble.D0();
    latData.set(lat);
    v = recordVars.get(count++);
    v.setCachedData(latData, false);

    // lon
    ArrayDouble.D0 lonData = new ArrayDouble.D0();
    lonData.set(lon);
    v = recordVars.get(count++);
    v.setCachedData(lonData, false);

    // alt
    if (useAlt) {
      ArrayDouble.D0 altData = new ArrayDouble.D0();
      altData.set(alt);
      v = recordVars.get(count++);
      v.setCachedData(altData, false);
    }

    return count;
  }

  public void writeRecord(PointFeature pf, StructureData sdata) throws IOException {
    if (debug) System.out.println("PointFeature= " + pf);

    EarthLocation loc = pf.getLocation();
    int count = writeCoordinates(loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), pf.getObservationTimeAsCalendarDate().toDate());

    for (int i = count; i < recordVars.size(); i++) {
      Variable v = recordVars.get(i);
      v.setCachedData(sdata.getArray(v.getShortName()), false);
    }

    ncfileOut.writeRecordData(recordVars);
  }

  public void writeRecord(PointObsDatatype pobs, StructureData sdata) throws IOException {
    if (debug) System.out.println("pobs= " + pobs);

    ucar.unidata.geoloc.EarthLocation loc = pobs.getLocation();
    int count = writeCoordinates(loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), pobs.getObservationTimeAsDate());

    for (int i = count; i < recordVars.size(); i++) {
      Variable v = recordVars.get(i);
      if (debug) System.out.println(" var= " + v.getShortName());
      //assert v.hasCachedData();  ??
      v.setCachedData( sdata.getArray(v.getShortName()), false);
    }

    ncfileOut.writeRecordData(recordVars);
  }

  public void finish() throws IOException {
    //writeDataFinish();
    ncfileOut.close();
    ncfileOut.stream.flush();
    ncfileOut.stream.close();
  }

  private class NetcdfFileStream extends NetcdfFileSubclass {
    N3outputStreamWriter swriter;
    DataOutputStream stream;

    NetcdfFileStream(DataOutputStream stream) {
      super();
      this.stream = stream;
      swriter = new N3outputStreamWriter(this);
    }

    void writeHeader(int numrec) throws IOException {
      swriter.writeHeader(stream, numrec);
    }

    void writeNonRecordData(String varName, Array data) throws IOException {
      swriter.writeNonRecordData(findVariable(varName), stream, data);
    }

    void writeRecordData(List<Variable> varList) throws IOException {
      swriter.writeRecordData(stream, varList);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////

  /**
   * Write a ucar.nc2.ft.PointFeatureCollection in CF point format.
   *
   * @param pfDataset find the first PointFeatureCollection, and write all data from it
   * @param fileOut write to this netcdf-3 file
   * @return number of records written
   * @throws IOException on read/write error, or if no PointFeatureCollection in pfDataset
   */
  public static int writePointFeatureCollection(FeatureDatasetPoint pfDataset, String fileOut) throws IOException {
    // extract the PointFeatureCollection
    PointFeatureCollection pointFeatureCollection = null;
    List<DsgFeatureCollection> featureCollectionList = pfDataset.getPointFeatureCollectionList();
    for ( DsgFeatureCollection featureCollection : featureCollectionList) {
      if (featureCollection instanceof PointFeatureCollection)
        pointFeatureCollection = (PointFeatureCollection) featureCollection;
    }
    if (null == pointFeatureCollection)
      throw new IOException("There is no PointFeatureCollection in  "+pfDataset.getLocation());

     long start = System.currentTimeMillis();

    FileOutputStream fos = new FileOutputStream(fileOut);
    DataOutputStream out = new DataOutputStream( new BufferedOutputStream(fos, 10000));
    WriterCFPointDataset writer = null;

    // LOOK BAD
    List<VariableSimpleIF> dataVars = new ArrayList<VariableSimpleIF>();
    ucar.nc2.NetcdfFile ncfile = pfDataset.getNetcdfFile();
    if ((ncfile == null) || !(ncfile instanceof NetcdfDataset))  {
      dataVars.addAll(pfDataset.getDataVariables());
    } else {
      NetcdfDataset ncd = (NetcdfDataset) ncfile;
      for (VariableSimpleIF vs : pfDataset.getDataVariables()) {
        if (ncd.findCoordinateAxis(vs.getShortName()) == null)
          dataVars.add(vs);
      }
    }

    int count = 0;
    for (PointFeature pointFeature : pointFeatureCollection) {
      StructureData data = pointFeature.getDataAll();
      if (count == 0) {
        EarthLocation loc = pointFeature.getLocation(); // LOOK we dont know this until we see the obs
        String altUnits = Double.isNaN(loc.getAltitude()) ? null : "meters"; // LOOK units may be wrong
        writer = new WriterCFPointDataset(out, pfDataset.getGlobalAttributes(), altUnits);
        writer.writeHeader( dataVars, -1);
      }
      writer.writeRecord(pointFeature, data);
      count++;
    }

    writer.finish();
    out.flush();
    out.close();

    long took = System.currentTimeMillis() - start;
    System.out.printf("Write %d records from %s to %s took %d msecs %n", count, pfDataset.getLocation(),fileOut,took);
    return count;
  }

  /**
   * Open a ucar.nc2.dt.PointObsDataset, write out in CF point format.
   *
   * @param fileIn open through TypedDatasetFactory.open(FeatureType.POINT, ..)
   * @param fileOut write to tehis netcdf-3 file
   * @param inMemory  if true, write in memory for efficiency
   * @throws IOException on read/write error
   */
  public static void rewritePointObsDataset(String fileIn, String fileOut, boolean inMemory) throws IOException {
    System.out.println("Rewrite .nc files from " + fileIn + " to " + fileOut + "inMem= " + inMemory);

    long start = System.currentTimeMillis();

    // do it in memory for speed
    NetcdfFile ncfile = inMemory ? NetcdfFile.openInMemory(fileIn) : NetcdfFile.open(fileIn);
    NetcdfDataset ncd = new NetcdfDataset(ncfile);

    StringBuilder errlog = new StringBuilder();
    PointObsDataset pobsDataset = (PointObsDataset) TypedDatasetFactory.open(FeatureType.POINT, ncd, null, errlog);

    FileOutputStream fos = new FileOutputStream(fileOut);
    DataOutputStream out = new DataOutputStream(fos);
    WriterCFPointDataset writer = null;

    DataIterator iter = pobsDataset.getDataIterator(1000 * 1000);
    while (iter.hasNext()) {
      PointObsDatatype pobsData = (PointObsDatatype) iter.nextData();
      StructureData sdata = pobsData.getData();
      if (writer == null) {
        ucar.unidata.geoloc.EarthLocation loc = pobsData.getLocation();
        String altUnits = Double.isNaN(loc.getAltitude()) ? null : "meters";
        writer = new WriterCFPointDataset(out, ncfile.getGlobalAttributes(), altUnits);
        writer.writeHeader(pobsDataset.getDataVariables(), -1);
      }
      writer.writeRecord(pobsData, sdata);
    }

    writer.finish();

    long took = System.currentTimeMillis() - start;
    System.out.println("Rewrite " + fileIn + " to " + fileOut + " took = " + took);
  }

  public static void main(String args[]) throws IOException {
    String location = "R:/testdata/point/netcdf/02092412_metar.nc";
    File file = new File(location);
    rewritePointObsDataset(location, "C:/TEMP/"+ file.getName(), true);
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.server.cdmvalidator;

import com.coverity.security.Escape;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.ModelAndView;
import org.slf4j.MDC;
import org.jdom2.Document;
import org.jdom2.output.XMLOutputter;
import org.jdom2.output.Format;
import org.jdom2.transform.XSLTransformer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;

import thredds.servlet.UsageLog;
import thredds.util.ContentType;
import ucar.nc2.constants.CDM;
import ucar.nc2.dataset.NetcdfDatasetInfo;
import ucar.unidata.util.StringUtil2;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * CdmValidator Spring Controller
 *
 * @author edavis
 * @since 4.0
 */
public class CdmValidatorController extends AbstractController {
  private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CdmValidatorController.class);

  private CdmValidatorContext cdmValidatorContext;

  public void setCdmValidatorContext(CdmValidatorContext cdmValidatorContext) {
    this.cdmValidatorContext = cdmValidatorContext;
  }

  protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
    log.info("handleRequestInternal(): " + UsageLog.setupRequestContext(request));

    // Get the request path.
    String reqPath = request.getServletPath();
    if (reqPath == null) {
      log.info("handleRequestInternal(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
      return null;
    }

    if (request.getMethod().equalsIgnoreCase("GET")) {
      if (reqPath.equals("/validate.html")) {
        Map<String, Object> model = new HashMap<>();
        model.put("contextPath", request.getContextPath());
        model.put("servletPath", request.getServletPath());

        this.cdmValidatorContext.getHtmlConfig().addHtmlConfigInfoToModel(model);

        log.info("handleRequestInternal(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, -1));
        return new ModelAndView("/thredds/server/cdmvalidator/cdmValidate", model);
      } else if (reqPath.equals("/validateHelp.html")) {
        Map<String, Object> model = new HashMap<>();
        model.put("contextPath", request.getContextPath());
        model.put("servletPath", request.getServletPath());

        this.cdmValidatorContext.getHtmlConfig().addHtmlConfigInfoToModel(model);

        log.info("handleRequestInternal(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, -1));
        return new ModelAndView("/thredds/server/cdmvalidator/cdmValidateHelp", model);
      } else if (reqPath.equals("/validate")) {
        this.doGet(request, response);
      } else {
        log.info("handleRequestInternal(): Unsupported path [" + reqPath + "] - " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return null;
      }
    } else if (request.getMethod().equalsIgnoreCase("POST")) {
      if (reqPath.equals("/validate")) {
        this.doPost(request, response);
      } else {
        log.info("handleRequestInternal(): Unsupported path [" + reqPath + "] - " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return null;
      }
    }

    return null;
  }

  /**
   * GET handles the case where its a remote URL (dods or http)
   *
   * @param req request
   * @param res response
   * @throws javax.servlet.ServletException
   * @throws java.io.IOException
   */
  public void doGet(HttpServletRequest req, HttpServletResponse res)
          throws ServletException, IOException {

    log.info("doGet(): " + UsageLog.setupRequestContext(req));

    String urlString = req.getParameter("URL");
    if (urlString == null) {
      log.info("doGet(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Must have a URL parameter");
      return;
    }

    // validate the url String
    try {
      URI uri = new URI(urlString);
      urlString = uri.toASCIIString(); // LOOK do we want just toString() ? Is this useful "input validation" ?
    } catch (URISyntaxException e) {
      log.info("doGet(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      res.sendError(HttpServletResponse.SC_BAD_REQUEST, "URISyntaxException on URU parameter");
      return;
    }

    String xml = req.getParameter("xml");
    boolean wantXml = (xml != null) && xml.equals("true");

    try {
      int len = showValidatorResults(res, urlString, wantXml);
      log.info("doGet(): URL = " + urlString);
      log.info("doGet(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, len));

    } catch (Exception e) {
      log.info("doGet(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid input");

    } catch (Throwable e) {
      log.error("doGet(): Validator internal error", e);
      log.info("doGet(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 0));
      res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Validator internal error");
    }

  }

  /**
   * POST handles uploaded files
   *
   * @param req request
   * @param res response
   * @throws ServletException
   * @throws IOException
   */
  public void doPost(HttpServletRequest req, HttpServletResponse res)
          throws ServletException, IOException {

    log.info("doPost(): " + UsageLog.setupRequestContext(req));

    // Check that we have a file upload request
    boolean isMultipart = ServletFileUpload.isMultipartContent(req);
    if (!isMultipart) {
      log.info("doPost(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      res.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }

    //Create a new file upload handler
    ServletFileUpload upload = new ServletFileUpload(this.cdmValidatorContext.getFileuploadFileItemFactory());
    upload.setSizeMax(this.cdmValidatorContext.getMaxFileUploadSize());  // maximum bytes before a FileUploadException will be thrown

    List<FileItem> fileItems;
    try {
      fileItems = (List<FileItem>) upload.parseRequest(req);
    } catch (FileUploadException e) {
      log.info("doPost(): Validator FileUploadException", e);
      log.info("doPost(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }

    //Process the uploaded items
    String username = null;
    boolean wantXml = false;
    for (FileItem item : fileItems) {
      if (item.isFormField()) {
        if ("username".equals(item.getFieldName()))
          username = item.getString();
        if ("xml".equals(item.getFieldName()))
          wantXml = item.getString().equals("true");
      }
    }

    for (FileItem item : fileItems) {
      if (!item.isFormField()) {
        try {
          processUploadedFile(req, res, (DiskFileItem) item, username, wantXml);
          return;
        } catch (Exception e) {
          log.info("doPost(): Validator processUploadedFile", e);
          log.info("doPost(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
          res.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
        }
      }
    }

  }

  private void processUploadedFile(HttpServletRequest req, HttpServletResponse res, DiskFileItem item,
                                   String username, boolean wantXml) throws Exception {

    if ((username == null) || (username.length() == 0))
      username = "none";
    username = Escape.html(StringUtil2.filter(username, "_"));
    String filename = Escape.html(item.getName());
    filename = StringUtil2.replace(filename, "/", "-");
    filename = StringUtil2.filter(filename, ".-_");

    File uploadedFile = new File(this.cdmValidatorContext.getCacheDir() + "/" + username + "/" + filename);
    uploadedFile.getParentFile().mkdirs();
    item.write(uploadedFile);

    int len = showValidatorResults(res, uploadedFile.getPath(), wantXml);

    if (this.cdmValidatorContext.isDeleteImmediately()) {
      try {
        uploadedFile.delete();
      } catch (Exception e) {
        log.error("processUploadedFile(): Uploaded File = " + uploadedFile.getPath() + " delete failed = " + e.getMessage());
      }
    }

    if (req.getRemoteUser() == null) {
      if (username != null)
        MDC.put("userid", username);
    }

    log.info("processUploadedFile(): Uploaded File = " + item.getName() + " sent to " + uploadedFile.getPath() + " size= " + uploadedFile.length());
    log.info("processUploadedFile(): " + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, len));
  }

  private int showValidatorResults(HttpServletResponse res, String location, boolean wantXml) throws Exception {

    try (NetcdfDatasetInfo info = new NetcdfDatasetInfo(location)) {

      String infoString;

      if (wantXml) {
        infoString = info.writeXML();
        res.setContentLength(infoString.getBytes(CDM.utf8Charset).length);
        res.setContentType(ContentType.xml.getContentHeader());

      } else {
        Document xml = info.makeDocument();
        InputStream is = getXSLT();
        XSLTransformer transformer = new XSLTransformer(is);

        Document html = transformer.transform(xml);
        XMLOutputter fmt = new XMLOutputter(Format.getPrettyFormat());
        infoString = fmt.outputString(html);

        res.setContentType(ContentType.html.getContentHeader());
      }

      res.setContentLength(infoString.getBytes(CDM.utf8Charset).length);

      OutputStream out = res.getOutputStream();
      out.write(infoString.getBytes(CDM.utf8Charset));
      out.flush();

      return infoString.length();
    }
  }

  private InputStream getXSLT() {
    Class c = CdmValidatorController.class;
    //String resource = "/WEB-INF/classes/resources/xsl/cdmValidation.xsl";
    String resource = "/resources/xsl/cdmValidation.xsl";
    InputStream is = c.getResourceAsStream(resource);
    if (null == is)
      log.error("getXSLT(): Cant load XSLT resource = " + resource);

    return is;
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.ft.point.standard;

import java.io.IOException;
import java.util.Iterator;
import javax.annotation.Nonnull;

import ucar.ma2.StructureData;
import ucar.ma2.StructureDataIterator;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft.PointFeatureCollection;
import ucar.nc2.ft.PointFeatureCollectionIterator;
import ucar.nc2.ft.PointFeatureIterator;
import ucar.nc2.ft.ProfileFeature;
import ucar.nc2.ft.TrajectoryFeature;
import ucar.nc2.ft.TrajectoryFeatureCollection;
import ucar.nc2.ft.point.CollectionInfo;
import ucar.nc2.ft.point.CollectionIteratorAdapter;
import ucar.nc2.ft.point.PointCollectionIteratorFiltered;
import ucar.nc2.ft.point.PointFeatureCCImpl;
import ucar.nc2.ft.point.TrajectoryFeatureImpl;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.util.IOIterator;
import ucar.unidata.geoloc.LatLonRect;

/**
 * TrajectoryFeatureCollection using nested tables.
 * @author caron
 * @since Dec 31, 2008
 */
public class StandardTrajectoryCollectionImpl extends PointFeatureCCImpl implements TrajectoryFeatureCollection {
  private NestedTable ft;

  protected StandardTrajectoryCollectionImpl(String name, CalendarDateUnit timeUnit, String altUnits) {
    super(name, timeUnit, altUnits, FeatureType.TRAJECTORY);
  }

  StandardTrajectoryCollectionImpl(NestedTable ft, CalendarDateUnit timeUnit, String altUnits) {
    super(ft.getName(), timeUnit, altUnits, FeatureType.TRAJECTORY);
    this.ft = ft;
    this.extras = ft.getExtras();
  }

  @Override
  public TrajectoryFeatureCollection subset(LatLonRect boundingBox) throws IOException {
    return new StandardTrajectoryCollectionSubset( this, boundingBox);
  }

  ///////////////////////////////////////
  // TrajectoryFeature using nested tables.
  private class StandardTrajectoryFeature extends TrajectoryFeatureImpl {
    Cursor cursor;
    StructureData trajData;

    StandardTrajectoryFeature(Cursor cursor, StructureData trajData) {
      super(ft.getFeatureName(cursor), StandardTrajectoryCollectionImpl.this.getTimeUnit(), StandardTrajectoryCollectionImpl.this.getAltUnits(), -1);
      this.cursor = cursor;
      this.trajData = trajData;
    }

    @Override
    public PointFeatureIterator getPointFeatureIterator() throws IOException {
      Cursor cursorIter = cursor.copy();
      StructureDataIterator siter = ft.getLeafFeatureDataIterator(cursorIter);
      return new StandardPointFeatureIterator(this, ft, timeUnit, siter, cursorIter);
    }

    @Nonnull
    @Override
    public StructureData getFeatureData() {
      return trajData;
    }
  }

  ///////////////////////////////////////
  private static class StandardTrajectoryCollectionSubset extends StandardTrajectoryCollectionImpl {
    TrajectoryFeatureCollection from;
    LatLonRect boundingBox;

    StandardTrajectoryCollectionSubset(TrajectoryFeatureCollection from, LatLonRect boundingBox) {
      super(from.getName()+"-subset", from.getTimeUnit(), from.getAltUnits());
      this.from = from;
      this.boundingBox = boundingBox;
    }

    @Override
    public PointFeatureCollectionIterator getPointFeatureCollectionIterator() throws IOException {
      return new PointCollectionIteratorFiltered( from.getPointFeatureCollectionIterator(), new FilterBB());
    }

    private class FilterBB implements PointFeatureCollectionIterator.Filter {

      @Override
      public boolean filter(PointFeatureCollection pointFeatureCollection) {
        ProfileFeature profileFeature = (ProfileFeature) pointFeatureCollection;
        return boundingBox.contains(profileFeature.getLatLon());
      }
    }
  }

  /////////////////////////////////////////////////////////////////////////////////////

  @Override
  public Iterator<TrajectoryFeature> iterator() {
    try {
      PointFeatureCollectionIterator pfIterator = getPointFeatureCollectionIterator();
      return new CollectionIteratorAdapter<>(pfIterator);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public PointFeatureCollectionIterator getPointFeatureCollectionIterator() throws IOException {
    return new TrajCollectionIterator( ft.getRootFeatureDataIterator());
  }

  @Override
  public IOIterator<PointFeatureCollection> getCollectionIterator() throws IOException {
    return new TrajCollectionIterator( ft.getRootFeatureDataIterator());
  }

  private class TrajCollectionIterator implements PointFeatureCollectionIterator, IOIterator<PointFeatureCollection> {
    StructureDataIterator structIter;
    StructureData nextTraj;
    StandardTrajectoryFeature prev;
    CollectionInfo calcInfo;

    TrajCollectionIterator(ucar.ma2.StructureDataIterator structIter) throws IOException {
      this.structIter = structIter;
      CollectionInfo info = getInfo();
      if (!info.isComplete())
        calcInfo = info;
    }

    @Override
    public boolean hasNext() throws IOException {
      while (true) {
        if (prev != null && calcInfo != null)
          calcInfo.extend(prev.getInfo());

        if(!structIter.hasNext()) {
          structIter.close();
          if (calcInfo != null) calcInfo.setComplete();
          return false;
        }
        nextTraj = structIter.next();
        if (!ft.isFeatureMissing(nextTraj)) break;
      }
      return true;
    }

    @Override
    public TrajectoryFeature next() throws IOException {
      Cursor cursor = new Cursor(ft.getNumberOfLevels());
      cursor.recnum[1] = structIter.getCurrentRecno();
      cursor.tableData[1] = nextTraj;
      cursor.currentIndex = 1;
      ft.addParentJoin(cursor); // there may be parent joins

      prev = new StandardTrajectoryFeature(cursor, nextTraj);
      return prev;
    }

    @Override
    public void close() {
      structIter.close();
    }
  }

  /////////////////////////////////////////////////////////////////////////////////////
  // deprecated

  private TrajCollectionIterator localIterator = null;

  @Override
  public boolean hasNext() throws IOException {
    if (localIterator == null) resetIteration();
    return localIterator.hasNext();
  }

  // need covariant return to allow superclass to implement
  @Override
  public TrajectoryFeature next() throws IOException {
    return localIterator.next();
  }

  @Override
  public void resetIteration() throws IOException {
    localIterator = (TrajCollectionIterator) getPointFeatureCollectionIterator();
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.ui.geoloc;

import ucar.nc2.util.ListenerManager;
import ucar.unidata.geoloc.*;

import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

/** Consider this a private inner class of NavigatedPanel.
    Handle display to world coordinate transformation, always linear.
    Call np.newMapAreaEvent() when MapArea changes.
    setMapArea() trigger a NewMapAreaEvent also.

 * @author John Caron
 **/

public class Navigation  {
  private NavigatedPanel np;

    // fundamental quantities
  private double pwidth = 0, pheight = 0;       // current display size
  private double pix_per_world = 1.0;           // scale always the same in both directions
  private double pix_x0 = 0.0, pix_y0 = 0.0;    // offset from world origin, in pixels

    // derived
  private ProjectionRect bb;                    // current world bounding box
  private AffineTransform at;                   // affine transform for graphics2D
    //misc
  private boolean mapAreaIsSet = false;         // cant initialize until screen size is known
  private boolean screenSizeIsSet = false;      // and initial bounding box is known
  private ZoomStack zoom = new ZoomStack();

  private static boolean debug = false, debugZoom = false, debugTransform = false;
  private static boolean debugRecalc = false;

  Navigation(NavigatedPanel np) {
    this.np = np;
    bb = new ProjectionRect();
    at = new AffineTransform();
  }

  // screen size
  public double getScreenWidth() { return pwidth; }
  public double getScreenHeight() { return pheight; }
  public void setScreenSize(double pwidth, double pheight) {
    if ((pwidth == 0) || (pheight == 0))
      return;

    if (mapAreaIsSet && screenSizeIsSet) {
      // make sure bb is current
      bb.setRect( getMapArea( null));
    }

    this.pwidth = pwidth;
    this.pheight = pheight;
    screenSizeIsSet = true;
    if (debugRecalc) System.out.println("navigation/setScreenSize "+ pwidth+" "+pheight);

    if (mapAreaIsSet) {
      recalcFromBoundingBox();
    }

    fireMapAreaEvent();
  }

    /** Get the affine transform based on screen size and world bounding box */
  public AffineTransform getTransform() {
    at.setTransform( pix_per_world, 0.0, 0.0, -pix_per_world, pix_x0, pix_y0);

    if (debug) {
      System.out.println("Navigation getTransform = "+ pix_per_world +" "+ pix_x0+" "+ pix_y0);
      System.out.println("  transform = "+ at);
    }
    return at;
  }
    // calculate if we want to rotate based on aspect ratio
  public boolean wantRotate(double displayWidth, double displayHeight) {
    getMapArea( bb); // current world bounding box
    boolean aspectDisplay = displayHeight < displayWidth;
    boolean aspectWorldBB = bb.getHeight() < bb.getWidth();
    return (aspectDisplay ^ aspectWorldBB); // aspects are different
 }

  /** Calculate an affine transform based on the display size parameters - used for printing.
   * @param rotate should the page be rotated?
   * @param displayX            upper right corner of display area
   * @param displayY            upper right corner of display area
   * @param displayWidth   display area
   * @param displayHeight   display area
   *
   * see Navigation.calcTransform
   */
  public AffineTransform calcTransform(boolean rotate, double displayX, double displayY, double displayWidth, double displayHeight) {
    getMapArea( bb); // current world bounding box
       // scale to limiting dimension
    double pxpsx, pypsy;
    if (rotate) {
      pxpsx = displayHeight/bb.getWidth();
      pypsy = displayWidth/bb.getHeight();
    } else {
      pxpsx = displayWidth/bb.getWidth();
      pypsy = displayHeight/bb.getHeight();
    }
    double pps = Math.min( pxpsx, pypsy);

      // calc offset: based on center point staying in center
    double wx0 = bb.getX() + bb.getWidth()/2;  // world midpoint
    double wy0 = bb.getY() + bb.getHeight()/2;
    double x0 = displayX + displayWidth/2 - pps * wx0;
    double y0 = displayY + displayHeight/2 + pps * wy0;

    AffineTransform cat = new AffineTransform(pps, 0.0, 0.0, -pps, x0, y0);

    // rotate if we need to
    if (rotate)
        cat.rotate(Math.PI/2, wx0, wy0);

    if (debug) {
      System.out.println("Navigation calcTransform = "+ displayX +" "+ displayY+
                " "+ displayWidth+" "+displayHeight);
      System.out.println("  world = "+ bb);
      System.out.println("  scale/origin = "+ pps +" "+ x0+" "+ y0);
      System.out.println("  transform = "+ cat);
    }
    return cat;
  }

    /** Get current MapArea .
     * @param rect : place results here, or null to create new Object
     */
  public ProjectionRect getMapArea(ProjectionRect rect) {
    if (rect == null)
      rect = new ProjectionRect();

    double width = pwidth/pix_per_world;
    double height = pheight/pix_per_world;

        // center point
    double wx0 = (pwidth/2-pix_x0)/pix_per_world;
    double wy0 = (pix_y0-pheight/2)/pix_per_world;

    rect.setRect(wx0-width/2, wy0-height/2,             // minx, miny
             width, height);                            // width, height

    return rect;
  }

  public void setMapArea(ProjectionRect ma) {
    if (debugRecalc) System.out.println("navigation/setMapArea "+ ma);

    bb.setRect(ma);
    zoom.push();

    mapAreaIsSet = true;
    if (screenSizeIsSet) {
      recalcFromBoundingBox();
      fireMapAreaEvent();
    }
  }
  // kludgy thing used to deal with cylindrical seams: package private
  void setWorldCenterX( double wx_center) {
    pix_x0 = pwidth/2 - pix_per_world * wx_center;
  }

    /** convert a world coordinate to a display point */
  public Point2D worldToScreen(ProjectionPointImpl w, Point2D p) {
    p.setLocation( pix_per_world*w.getX() + pix_x0,
                   -pix_per_world*w.getY() + pix_y0);
    return p;
  }

    /** convert a display point to a world coordinate */
  public ProjectionPointImpl screenToWorld(Point2D p, ProjectionPointImpl w) {
    w.setLocation((p.getX() - pix_x0) / pix_per_world,
                  (pix_y0 - p.getY()) / pix_per_world);
    return w;
  }

  public double getPixPerWorld() { return pix_per_world; }

    /** convert screen Rectangle to a projection (world) rectangle */
  public ProjectionRect screenToWorld(Point2D start, Point2D end) {
    ProjectionPointImpl p1 = new ProjectionPointImpl();
    ProjectionPointImpl p2 = new ProjectionPointImpl();

    screenToWorld( start, p1);
    screenToWorld( end, p2);

    return new ProjectionRect(p1.getX(), p1.getY(), p2.getX(), p2.getY());
  }

    /** convert a projection (world) rectangle to a screen Rectangle */
  public java.awt.Rectangle worldToScreen(ProjectionRect projRect) {
    Point2D p1 = new Point2D.Double();
    Point2D p2 = new Point2D.Double();

    worldToScreen( (ProjectionPointImpl) projRect.getMaxPoint(), p1);
    worldToScreen( (ProjectionPointImpl) projRect.getMinPoint(), p2);
    return new java.awt.Rectangle((int)p1.getX(), (int)p1.getY(), (int)p2.getX(), (int)p2.getY());
  }

  /************* domain changing calls ****************************/
  /** call this to change the center of the screen's world coordinates.
     deltax, deltay in display coordinates */
  public void pan( double deltax, double deltay) {
    zoom.push();

    pix_x0 -= deltax;
    pix_y0 -= deltay;
    fireMapAreaEvent();
  }

  /** call this to zoom into a subset of the screen.
    startx, starty are the upper left corner of the box in display coords
    width, height the size of the box in display coords */
  public void zoom(double startx, double starty, double width, double height) {
    if (debugZoom)
      System.out.println("zoom "+ startx+ " "+starty+ " "+width+ " "+height+ " ");

    if ((width < 5) || (height < 5))
      return;
    zoom.push();

    pix_x0 -= startx+width/2 - pwidth/2;
    pix_y0 -= starty+height/2 - pheight/2;
    zoom(pwidth / width);
  }

  public void zoomIn(double x, double y) {
    zoomIn();
  }
  public void zoomOut(double x, double y) {
    zoomOut();
  }
  public void zoomIn() {
    zoom(2.0);
  }
  public void zoomOut() {
    zoom(.5);
  }
  private void zoom(double scale) {
    zoom.push();

    // change scale, but leave center point fixed
    // get these equations by solving for pix_x0, pix_y0
    // that leaves center point invariant
    double fac = (1 - scale);
    pix_x0 = scale*pix_x0 + fac*pwidth/2;
    pix_y0 = scale*pix_y0 + fac*pheight/2;
    pix_per_world *= scale;
    fireMapAreaEvent();
  }
  public void moveDown() {
    zoom.push();

    pix_y0 -= pheight/2;
    fireMapAreaEvent();
  }
  public void moveUp() {
    zoom.push();

    pix_y0 += pheight/2;
    fireMapAreaEvent();
  }
  public void moveRight() {
    zoom.push();

    pix_x0 -= pwidth/2;
    fireMapAreaEvent();
  }
  public void moveLeft() {
    zoom.push();

    pix_x0 += pwidth/2;
    fireMapAreaEvent();
  }
  public void zoomPrevious() {
    zoom.pop();
    fireMapAreaEvent();
  }

  /////////////////////////////////////////////////////////////////
  // private methods

     // calculate scale and offset based on the current screen size and bounding box
     // adjust bounding box to fit inside the screen size
  private void recalcFromBoundingBox() {
    if (debugRecalc) {
      System.out.println("Navigation recalcFromBoundingBox= "+ bb);
      System.out.println("  "+ pwidth +" "+ pheight);
    }

      // decide which dimension is limiting
    double pixx_per_wx = (bb.getWidth() == 0.0) ? 1 : pwidth / bb.getWidth();
    double pixy_per_wy = (bb.getHeight() == 0.0) ? 1 : pheight / bb.getHeight();
    pix_per_world = Math.min(pixx_per_wx, pixy_per_wy);

      // calc the center point
    double wx0 = bb.getX() + bb.getWidth()/2;
    double wy0 = bb.getY() + bb.getHeight()/2;

      // calc offset based on center point
    pix_x0 = pwidth/2 - pix_per_world * wx0;
    pix_y0 = pheight/2 + pix_per_world * wy0;

    if (debugRecalc) {
      System.out.println("Navigation recalcFromBoundingBox done= "+ pix_per_world +" "+ pix_x0+" "+ pix_y0);
      System.out.println("  "+ pwidth +" "+ pheight+" "+ bb);
    }
  }

  private synchronized void fireMapAreaEvent() {
    // send out event to Navigated Panel
    np.fireMapAreaEvent();
  }

  // keep stack of previous zooms
  // this should probably be made into a circular buffer
  private class ZoomStack extends java.util.ArrayList {
    private int current = -1;
    ZoomStack() {
      super(20); // stack size
    }

    void push() {
      current++;
      add( current, new Zoom( pix_per_world, pix_x0, pix_y0));
    }

    void pop() {
      if (current < 0)
        return;
      Zoom zoom = (Zoom) get( current);
      pix_per_world = zoom.pix_per_world;
      pix_x0 = zoom.pix_x0;
      pix_y0 = zoom.pix_y0;
      current--;
    }

    private class Zoom {
      double pix_per_world;
      double pix_x0;
      double pix_y0;
      Zoom(double p1, double p2, double p3) {
        pix_per_world = p1;
        pix_x0 = p2;
        pix_y0 = p3;
      }
    }

  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.util;

import org.slf4j.Logger;

import javax.annotation.concurrent.ThreadSafe;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Helper class for managing event listeners.
 * It is thread safe, but better not to be adding/deleting listeners while sending events.
 * LOOK Probably could replace with guava eventbus
 *
 * Example:
 *

 *
  private void createListenerManager() {
    lm = new ListenerManager(
            "ucar.nc2.util.DatasetCollectionManager$EventListener",
            "ucar.nc2.util.DatasetCollectionManager$Event",
            "setMessage");
  }

  public void addEventListener(EventListener l) {
    lm.addListener(l);
  }

  public void removeEventListener(EventListener l) {
    lm.removeListener(l);
  }

  public class Event extends java.util.EventObject {
    private String message;

    Event(String message) {
      super(DatasetCollectionManager.this);
      this.message = message;
    }

    public String getMessage() {
      return message;
    }
  }

  public static interface EventListener {
    public void setMessage(DatasetCollectionManager.Event event);
  }

 lm.sendEvent(event);

 *
 * @author John Caron
 */
@ThreadSafe
public class ListenerManager {
  static private final Logger logger = org.slf4j.LoggerFactory.getLogger(ListenerManager.class);

  private final List<Object> listeners = new CopyOnWriteArrayList<>(); // cf http://www.ibm.com/developerworks/java/library/j-jtp07265/index.html
  private final java.lang.reflect.Method method;
  private boolean hasListeners = false;
  private boolean enabled = true;

  /**
   * Constructor.
   *
   * @param listener_class the name of the EventListener class, eg "ucar.unidata.ui.UIChangeListener"
   * @param event_class    the name of the Event class, eg "ucar.unidata.ui.UIChangeEvent"
   * @param method_name    the name of the EventListener method, eg "processChange".

   *                          This method must have the signature     public void method_name( event_class e)

   */
  public ListenerManager(String listener_class, String event_class, String method_name) {

    try {
      Class lc = Class.forName(listener_class);
      Class ec = Class.forName(event_class);
      Class[] params = new Class[1];
      params[0] = ec;
      this.method = lc.getMethod(method_name, params);

    } catch (Exception ee) {
      logger.error("ListenerManager failed on " + listener_class + "." + method_name + "( " + event_class + " )", ee);
      throw new RuntimeException(ee);
    }

  }

  public void setEnabled(boolean enabled) {
    this.enabled = enabled;
  }

  public boolean getEnabled() {
    return enabled;
  }

  /**
   * Add a listener.
   *
   * @param l listener must be of type "listener_class"
   */
  public synchronized void addListener(Object l) {
    if (!listeners.contains(l)) {
      listeners.add(l);
      hasListeners = true;
    } else
      logger.warn("ListenerManager.addListener already has Listener " + l);
  }

  /**
   * Remove a listener.
   * @param l listener must be of type "listener_class"
   */
  public synchronized void removeListener(Object l) {
    if (listeners.contains(l)) {
      listeners.remove(l);
      hasListeners = (listeners.size() > 0);
    } else
      logger.warn("ListenerManager.removeListener couldnt find Listener " + l);
  }

  public synchronized boolean hasListeners() {
    return hasListeners;
  }

  /**
   * Send an event to all registered listeners. If an exception is thrown, remove
   * the Listener from the list
   *
   * @param event the event to be sent: public void method_name( event_class event)
   */
  public synchronized void sendEvent(java.util.EventObject event) {
    if (!hasListeners || !enabled)
      return;

    Object[] args = new Object[1];
    args[0] = event;

    // send event to all listeners
    ListIterator iter = listeners.listIterator();
    while (iter.hasNext()) {
      Object client = iter.next();
      try {
        method.invoke(client, args);
      } catch (IllegalAccessException e) {
        logger.error("ListenerManager IllegalAccessException", e);
        iter.remove();
      } catch (IllegalArgumentException e) {
        logger.error("ListenerManager IllegalArgumentException", e);
        iter.remove();
      } catch (InvocationTargetException e) {
        // logger.error("ListenerManager InvocationTargetException on " + method+ " threw exception " + e.getTargetException(), e);
        throw new RuntimeException(e.getCause()); // pass exception to the caller of sendEvent()
      }
    }
  }

  /**
   * Send an event to all registered listeners, except the named one.
   *
   * @param event the event to be sent: public void method_name( event_class event)
   */
  public synchronized void sendEventExcludeSource(java.util.EventObject event) {
    if (!hasListeners || !enabled)
      return;

    Object source = event.getSource();
    Object[] args = new Object[1];
    args[0] = event;

    // send event to all listeners except the source
    ListIterator iter = listeners.listIterator();
    while (iter.hasNext()) {
      Object client = iter.next();
      if (client == source)
        continue;

      try {
        method.invoke(client, args);
      } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) {
        e.printStackTrace();
        if (e.getCause() != null)
          e.getCause().printStackTrace();

        // iter.remove();
        logger.error("ListenerManager calling " + method + " threw exception ", e);
      }
    }
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import org.jdom2.Element;
import ucar.ma2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.ncml.NcMLWriter;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.Indent;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.StringTokenizer;

/**
 * Print contents of an existing netCDF file, using a Writer.
 * <p/>
 * A difference with ncdump is that the nesting of multidimensional array data is represented by nested brackets,
 * so the output is not legal CDL that can be used as input for ncgen. Also, the default is header only (-h)
 * LOOK XML routines should go away in 5.0
 *
 * @author caron
 * @since Nov 4, 2007
 */

public class NCdumpW {
  private static String usage =
          "usage: NCdumpW <filename> [-cdl | -ncml] [-c | -vall] [-v varName1;varName2;..] [-v varName(0:1,:,12)]\n";

  /**
   * Tell NCdumpW if you want values printed.
   */
  public enum WantValues {
    none, coordsOnly, all
  }

  /**
   * ncdump that parses a command string.
   *
   * @param command command string
   * @param out     send output here
   * @param ct      allow task to be cancelled; may be null.
   * @return true if successful
   * @throws IOException on write error
   */
  public static boolean print(String command, Writer out, ucar.nc2.util.CancelTask ct) throws IOException {
    // pull out the filename from the command
    String filename;
    StringTokenizer stoke = new StringTokenizer(command);
    if (stoke.hasMoreTokens())
      filename = stoke.nextToken();
    else {
      out.write(usage);
      return false;
    }

    try (NetcdfFile nc = NetcdfDataset.openFile(filename, ct)) {
      // the rest of the command
      int pos = command.indexOf(filename);
      command = command.substring(pos + filename.length());
      return print(nc, command, out, ct);

    } catch (java.io.FileNotFoundException e) {
      out.write("file not found= ");
      out.write(filename);
      return false;

    } finally {
      out.close();
    }
  }

  /**
   * ncdump, parsing command string, file already open.
   *
   * @param nc      apply command to this file
   * @param command : command string
   * @param out     send output here
   * @param ct      allow task to be cancelled; may be null.
   * @return true if successful
   * @throws IOException on write error
   */
  public static boolean print(NetcdfFile nc, String command, Writer out, ucar.nc2.util.CancelTask ct)
          throws IOException {
    WantValues showValues = WantValues.none;
    boolean ncml = false;
    boolean strict = false;
    String varNames = null;
    String trueDataset = null;
    String fakeDataset = null;

    if (command != null) {
      StringTokenizer stoke = new StringTokenizer(command);

      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("-help")) {
          out.write(usage);
          out.write('\n');
          return true;
        }
        if (toke.equalsIgnoreCase("-vall"))
          showValues = WantValues.all;
        if (toke.equalsIgnoreCase("-c") && (showValues == WantValues.none))
          showValues = WantValues.coordsOnly;
        if (toke.equalsIgnoreCase("-ncml"))
          ncml = true;
        if (toke.equalsIgnoreCase("-cdl") || toke.equalsIgnoreCase("-strict"))
          strict = true;
        if(toke.equalsIgnoreCase("-v") && stoke.hasMoreTokens())
          varNames = stoke.nextToken();
        if (toke.equalsIgnoreCase("-datasetname") && stoke.hasMoreTokens()) {
          fakeDataset = stoke.nextToken();
          if(fakeDataset.length() == 0) fakeDataset = null;
          if(fakeDataset != null) {
            trueDataset = nc.getLocation();
            nc.setLocation(fakeDataset);
          }
        }
      }
    }

    boolean ok = print(nc, out, showValues, ncml, strict, varNames, ct);
    if(trueDataset != null && fakeDataset != null)
      nc.setLocation(trueDataset);
    return ok;
  }

  /**
   * ncdump-like print of netcdf file.
   *
   * @param filename   NetcdfFile to open
   * @param out        print to this stream
   * @param showAll    dump all variable data
   * @param showCoords only print header and coordinate variables
   * @param ncml       print NcML representation (other arguments are ignored)
   * @param strict     print strict CDL representation
   * @param varNames   semicolon delimited list of variables whose data should be printed
   * @param ct         allow task to be cancelled; may be null.
   * @return true if successful
   * @throws IOException on write error
   */
  public static boolean print(String filename, Writer out, boolean showAll, boolean showCoords, boolean ncml,
          boolean strict, String varNames, ucar.nc2.util.CancelTask ct) throws IOException {
    try (NetcdfFile nc = NetcdfDataset.openFile(filename, ct)){
      return print(nc, out, showAll, showCoords, ncml, strict, varNames, ct);

    } catch (java.io.FileNotFoundException e) {
      out.write("file not found= ");
      out.write(filename);
      out.flush();
      return false;

    }
  }

  /**
   * ncdump-like print of netcdf file.
   *
   * @param nc         already opened NetcdfFile
   * @param out        print to this stream
   * @param showAll    dump all variable data
   * @param showCoords only print header and coordinate variables
   * @param ncml       print NcML representation (other arguments are ignored)
   * @param strict     print strict CDL representation
   * @param varNames   semicolon delimited list of variables whose data should be printed. May have
   *                   Fortran90 like selector: eg varName(1:2,*,2)
   * @param ct         allow task to be cancelled; may be null.
   * @return true if successful
   * @throws IOException on write error
   */
  public static boolean print(NetcdfFile nc, Writer out, boolean showAll, boolean showCoords,
                              boolean ncml, boolean strict, String varNames, ucar.nc2.util.CancelTask ct) throws IOException {

    WantValues showValues = WantValues.none;
    if (showAll)
      showValues = WantValues.all;
    else if (showCoords)
      showValues = WantValues.coordsOnly;

    return print(nc, out, showValues, ncml, strict, varNames, ct);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////////
  // heres where the work is done

  /**
   * ncdump-like print of netcdf file.
   *
   * @param nc         already opened NetcdfFile
   * @param out        print to this stream
   * @param showValues do you want the variable values printed?
   * @param ncml       print NcML representation (other arguments are ignored)
   * @param strict     print strict CDL representation
   * @param varNames   semicolon delimited list of variables whose data should be printed. May have
   *                   Fortran90 like selector: eg varName(1:2,*,2)
   * @param ct         allow task to be cancelled; may be null.
   * @return true if successful
   * @throws IOException on write error
   */
  public static boolean print(NetcdfFile nc, Writer out, WantValues showValues, boolean ncml, boolean strict,
          String varNames, ucar.nc2.util.CancelTask ct) throws IOException {
    boolean headerOnly = (showValues == WantValues.none) && (varNames == null);

    try {
      if (ncml)
        writeNcML(nc, out, showValues, null); // output schema in NcML
      else if (headerOnly)
        nc.writeCDL(new PrintWriter(out), strict); // output schema in CDL form (like ncdump)
      else {
        PrintWriter ps = new PrintWriter(out);
        nc.toStringStart(ps, strict);

        Indent indent = new Indent(2);
        indent.incr();
        ps.printf("%n%sdata:%n", indent);
        indent.incr();

        if (showValues == WantValues.all) { // dump all data
          for (Variable v : nc.getVariables()) {
            printArray(v.read(), v.getFullName(), ps, indent, ct);
            if (ct != null && ct.isCancel()) return false;
          }
        } else if (showValues == WantValues.coordsOnly) { // dump coordVars
          for (Variable v : nc.getVariables()) {
            if (v.isCoordinateVariable())
              printArray(v.read(), v.getFullName(), ps, indent, ct);
            if (ct != null && ct.isCancel()) return false;
          }
        }

        if ((showValues != WantValues.all) && (varNames != null)) { // dump the list of variables
          StringTokenizer stoke = new StringTokenizer(varNames, ";");
          while (stoke.hasMoreTokens()) {
            String varSubset = stoke.nextToken(); // variable name and optionally a subset

            if (varSubset.indexOf('(') >= 0) { // has a selector
              Array data = nc.readSection(varSubset);
              printArray(data, varSubset, ps, indent, ct);

            } else {   // do entire variable
              Variable v = nc.findVariable(varSubset);
              if (v == null) {
                ps.print(" cant find variable: " + varSubset + "\n   " + usage);
                continue;
              }
              // dont print coord vars if they are already printed
              if ((showValues != WantValues.coordsOnly) || v.isCoordinateVariable())
                printArray(v.read(), v.getFullName(), ps, indent, ct);
            }
            if (ct != null && ct.isCancel()) return false;
          }
        }

        indent.decr();
        indent.decr();
        nc.toStringEnd(ps);
      }

    } catch (Exception e) {
      e.printStackTrace();
      out.write(e.getMessage());
      out.flush();
      return false;
    }

    out.flush();
    return true;
  }

  /**
   * Print all the data of the given Variable.
   *
   * @param v  variable to print
   * @param ct allow task to be cancelled; may be null.
   * @return String result
   * @throws IOException on write error
   */
  static public String printVariableData(VariableIF v, ucar.nc2.util.CancelTask ct) throws IOException {
    Array data = v.read();
    /* try {
      data = v.isMemberOfStructure() ? v.readAllStructures(null, true) : v.read();
    }
    catch (InvalidRangeException ex) {
      return ex.getMessage();
    } */

    StringWriter writer = new StringWriter(10000);
    printArray(data, v.getFullName(), new PrintWriter(writer), new Indent(2), ct);
    return writer.toString();
  }

  /**
   * Print a section of the data of the given Variable.
   *
   * @param v           variable to print
   * @param sectionSpec string specification
   * @param ct          allow task to be cancelled; may be null.
   * @return String result formatted data ouptut
   * @throws IOException           on write error
   * @throws InvalidRangeException is specified section doesnt match variable shape
   */
  static public String printVariableDataSection(Variable v, String sectionSpec, ucar.nc2.util.CancelTask ct) throws IOException, InvalidRangeException {
    Array data = v.read(sectionSpec);

    StringWriter writer = new StringWriter(20000);
    printArray(data, v.getFullName(), new PrintWriter(writer), new Indent(2), ct);
    return writer.toString();
  }

  static public String toString(Array array, String name, CancelTask ct) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    printArray(array, name, null, pw, new Indent(2), ct, true);
    return sw.toString();
  }

  static private void printArray(Array array, String name, PrintWriter out, Indent indent, CancelTask ct)
          throws IOException {
    printArray(array, name, null, out, indent, ct, true);
    out.flush();
  }

  static private void printArray(Array array, String name, String units, PrintWriter out, Indent ilev, CancelTask ct, boolean printSeq) { // throws IOException {
    if (ct != null && ct.isCancel()) return;

    if (name != null) out.print(ilev + name + " = ");
    ilev.incr();

    if (array == null) {
      out.println("null array for " + name);
      ilev.decr();
      // throw new IllegalArgumentException("null array for " + name);
      return;
    }

    if ((array instanceof ArrayChar) && (array.getRank() > 0)) {
      printStringArray(out, (ArrayChar) array, ilev, ct);

    } else if (array.getElementType() == String.class) {
      printStringArray(out, array, ilev, ct);

    } else if (array instanceof ArraySequence) {
      if (printSeq) printSequence(out, (ArraySequence) array, ilev, ct);

    } else if (array instanceof ArrayStructure) {
        printStructureDataArray(out, (ArrayStructure) array, ilev, ct);

    } else if (array.getElementType() == ByteBuffer.class) { // opaque type
      array.resetLocalIterator();
      while (array.hasNext()) {
        printByteBuffer(out, (ByteBuffer) array.next(), ilev);
        out.println(array.hasNext() ? "," : ";");// peek ahead
        if (ct != null && ct.isCancel()) return;
      }
    } else if (array instanceof ArrayObject) {
      printVariableArray(out, (ArrayObject) array, ilev, ct);
    } else {
      printArray(array, out, ilev, ct);
    }

    if (units != null)
      out.print(" " + units);
    out.print("\n");
    ilev.decr();
    out.flush();
  }

  static private void printArray(Array ma, PrintWriter out, Indent indent, CancelTask ct) {
    if (ct != null && ct.isCancel()) return;

    int rank = ma.getRank();
    Index ima = ma.getIndex();

    // scalar
    if (rank == 0) {
      Object value = ma.getObject(ima);

      if (ma.isUnsigned()) {
        assert value instanceof Number : "A data type being unsigned implies that it is numeric.";

        // "value" is an unsigned number, but it will be treated as signed when we print it below, because Java only
        // has signed types. If it's large enough ( >= 2^(BIT_WIDTH-1) ), its most-significant bit will be interpreted
        // as the sign bit, which will result in an invalid (negative) value being printed. To prevent that, we're
        // going to widen the number before printing it, but only if the unsigned number is being seen as negative.
        value = DataType.widenNumberIfNegative((Number) value);
      }

      out.print(value.toString());
      return;
    }

    int[] dims = ma.getShape();
    int last = dims[0];

    out.print("\n" + indent + "{");

    if ((rank == 1) && (ma.getElementType() != StructureData.class)) {
      for (int ii = 0; ii < last; ii++) {
        Object value = ma.getObject(ima.set(ii));

        if (ma.isUnsigned()) {
          assert value instanceof Number : "A data type being unsigned implies that it is numeric.";
          value = DataType.widenNumberIfNegative((Number) value);
        }

        if(ii > 0)
          out.print(", ");
        out.print(value.toString());
        if (ct != null && ct.isCancel()) return;
      }
      out.print("}");
      return;
    }

    indent.incr();
    for (int ii = 0; ii < last; ii++) {
      Array slice = ma.slice(0, ii);
      if(ii > 0)
        out.print(",");
      printArray(slice, out, indent, ct);
      if (ct != null && ct.isCancel()) return;
    }
    indent.decr();

    out.print("\n" + indent + "}");
  }

  static void printStringArray(PrintWriter out, ArrayChar ma, Indent indent, ucar.nc2.util.CancelTask ct) {
    if (ct != null && ct.isCancel()) return;

    int rank = ma.getRank();

    if (rank == 1) {
      out.print("  \"" + ma.getString() + "\"");
      return;
    }

    if (rank == 2) {
      boolean first = true;
      ArrayChar.StringIterator iter = ma.getStringIterator();
      while (iter.hasNext()) {
        if (!first) out.print(", ");
        out.print("\"" + iter.next() + "\"");
        first = false;
        if (ct != null && ct.isCancel()) return;
      }
      return;
    }

    int[] dims = ma.getShape();
    int last = dims[0];

    out.print("\n" + indent + "{");
    indent.incr();
    for (int ii = 0; ii < last; ii++) {
      ArrayChar slice = (ArrayChar) ma.slice(0, ii);
      if (ii > 0) out.print(",");
      printStringArray(out, slice, indent, ct);
      if (ct != null && ct.isCancel()) return;
    }
    indent.decr();

    out.print("\n" + indent + "}");
  }

  private static void printByteBuffer(PrintWriter out, ByteBuffer bb, Indent indent) {
    out.print(indent + "0x");
    int last = bb.limit() - 1;
    if(last < 0)
        out.printf("00");
    else
        for (int i = bb.position(); i <= last; i++) {
          out.printf("%02x", bb.get(i));
        }
  }

  static void printStringArray(PrintWriter out, Array ma, Indent indent, ucar.nc2.util.CancelTask ct) {
    if (ct != null && ct.isCancel()) return;

    int rank = ma.getRank();
    Index ima = ma.getIndex();

    if (rank == 0) {
      out.print("  \"" + ma.getObject(ima) + "\"");
      return;
    }

    if (rank == 1) {
      boolean first = true;
      for (int i = 0; i < ma.getSize(); i++) {
        if (!first) out.print(", ");
        out.print("  \"" + ma.getObject(ima.set(i)) + "\"");
        first = false;
      }
      return;
    }

    int[] dims = ma.getShape();
    int last = dims[0];

    out.print("\n" + indent + "{");
    indent.incr();
    for (int ii = 0; ii < last; ii++) {
      ArrayObject slice = (ArrayObject) ma.slice(0, ii);
      if (ii > 0) out.print(",");
      printStringArray(out, slice, indent, ct);
      //out.print("\n");
    }
    indent.decr();

    out.print("\n" + indent + "}");
  }

  static private void printStructureDataArray(PrintWriter out, ArrayStructure array, Indent indent, ucar.nc2.util.CancelTask ct) { // throws IOException {
    try (StructureDataIterator sdataIter = array.getStructureDataIterator()) {
      int count = 0;
      while (sdataIter.hasNext()) {
        StructureData sdata = sdataIter.next();
        out.println("\n" + indent + "{");
        printStructureData(out, sdata, indent, ct);
        //ilev.setIndentLevel(saveIndent);
        out.print(indent + "} " + sdata.getName() + "(" + count + ")");
        if (ct != null && ct.isCancel()) return;
        count++;
      }
    } catch (IOException e) {
      e.printStackTrace();  // shouldnt happen ?
    }
  }

  static private void printVariableArray(PrintWriter out, ArrayObject array, Indent indent, CancelTask ct) { // throws IOException {
    out.print("\n" + indent + "{");
    indent.incr();
    IndexIterator iter = array.getIndexIterator();
    boolean first = true;
    while (iter.hasNext()) {
      Array data = (Array) iter.next();
      if(!first) { out.print(", "); }
      printArray(data, out, indent, ct);
      first = false;
    }
    indent.decr();
    out.print("\n" + indent + "}");
  }

  static private void printSequence(PrintWriter out, ArraySequence seq, Indent indent, CancelTask ct) { // throws IOException {
    try (StructureDataIterator iter = seq.getStructureDataIterator()) {
      while (iter.hasNext()) {
        StructureData sdata = iter.next();
        out.println("\n" + indent + "{");
        printStructureData(out, sdata, indent, ct);
        out.print(indent + "} " + sdata.getName());
        if (ct != null && ct.isCancel()) return;
      }
    } catch (IOException e) {
      e.printStackTrace();  // shouldnt happen ??
    }
  }

  /**
   * Print contents of a StructureData.
   *
   * @param out   send output here.
   * @param sdata StructureData to print.
   * @throws IOException on read error
   */
  static public void printStructureData(PrintWriter out, StructureData sdata) throws IOException {
    printStructureData(out, sdata, new Indent(2), null);
    out.flush();
  }

  static private void printStructureData(PrintWriter out, StructureData sdata, Indent indent, CancelTask ct) { // throws IOException {
    indent.incr();
    for (StructureMembers.Member m : sdata.getMembers()) {
      Array sdataArray = sdata.getArray(m);
      printArray(sdataArray, m.getName(), m.getUnitsString(), out, indent, ct, true);
      if (ct != null && ct.isCancel()) return;
    }
    indent.decr();
  }

  static public String toString(StructureData sdata) throws IOException {
    CharArrayWriter carray = new CharArrayWriter(1000);
    PrintWriter pw = new PrintWriter(carray);
    for (StructureMembers.Member m : sdata.getMembers()) {
      Array memData = sdata.getArray(m);
      if (memData instanceof ArrayChar)
        pw.print(((ArrayChar) memData).getString());
      else
        printArray(memData, pw);
      pw.print(',');
    }
    return carray.toString();
  }

  /**
   * Print array as undifferentiated sequence of values.
   *
   * @param ma  any Array except ArrayStructure
   * @param out print to here
   */
  static public void printArrayPlain(Array ma, PrintWriter out) {
    ma.resetLocalIterator();
    while (ma.hasNext()) {
      out.print(ma.next());
      out.print(' ');
    }
  }

  /**
   * Print array to PrintWriter
   */
  static public void printArray(Array array, PrintWriter pw) {
    printArray(array, null, null, pw, new Indent(2), null, true);
  }

  static public String toString(Array ma) {
    return toString(ma, "", null);
  }

  //////////////////////////////////////////////////////////////////////////////////////
  // standard NCML writing.

  /**
   * Write the NcML representation for a file.
   * Note that ucar.nc2.dataset.NcMLWriter has a JDOM implementation, for complete NcML.
   * This method implements only the "core" NcML for plain ole netcdf files.
   *
   * @param ncfile     write NcML for this file
   * @param writer     write to this Writer. Must be using UTF-8 encoding (where applicable)
   * @param showValues do you want the variable values printed?
   * @param url        use this for the url attribute; if null use getLocation(). // ??
   * @throws IOException on write error
   */
  static public void writeNcML(NetcdfFile ncfile, Writer writer, WantValues showValues, String url) throws IOException {
    Preconditions.checkNotNull(ncfile);
    Preconditions.checkNotNull(writer);
    Preconditions.checkNotNull(showValues);

    Predicate<Variable> writeVarsPred;
    switch (showValues) {
      case none:
        writeVarsPred = NcMLWriter.writeNoVariablesPredicate;
        break;
      case coordsOnly:
        writeVarsPred = NcMLWriter.writeCoordinateVariablesPredicate;
        break;
      case all:
        writeVarsPred = NcMLWriter.writeAllVariablesPredicate;
        break;
      default:
        String message = String.format(
            "CAN'T HAPPEN: showValues (%s) != null and checked all possible enum values.", showValues);
        throw new AssertionError(message);
    }

    NcMLWriter ncmlWriter = new NcMLWriter();
    ncmlWriter.setWriteVariablesPredicate(writeVarsPred);

    Element netcdfElement = ncmlWriter.makeNetcdfElement(ncfile, url);
    ncmlWriter.writeToWriter(netcdfElement, writer);
  }

  ////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Main program.
   * <p><strong>ucar.nc2.NCdumpW filename [-cdl | -ncml] [-c | -vall] [-v varName1;varName2;..] [-v varName(0:1,:,12)]
   * </strong>
   * <p>where: <ul>
   * <li> filename : path of any CDM readable file
   * <li> cdl or ncml: output format is CDL or NcML
   * <li> -vall : dump all variable data
   * <li> -c : dump coordinate variable data
   * <li> -v varName1;varName2; : dump specified variable(s)
   * <li> -v varName(0:1,:,12) : dump specified variable section
   * </ul>
   * Default is to dump the header info only.
   *
   * @param args arguments
   */
  public static void main(String[] args) {
    if (args.length == 0) {
      System.out.println(usage);
      return;
    }

    StringBuilder sbuff = new StringBuilder();
    for (String arg : args) {
      sbuff.append(arg);
      sbuff.append(" ");
    }

    try {
      Writer writer = new BufferedWriter(new OutputStreamWriter(System.out, CDM.utf8Charset));
      NCdumpW.print(sbuff.toString(), writer, null);

    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.dataset.transform;

import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.units.SimpleUnit;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.*;
import ucar.unidata.geoloc.Earth;
import ucar.unidata.util.Parameter;

import java.util.StringTokenizer;
import java.util.List;
import java.util.Formatter;

/**
 * Abstract superclass for implementations of HorizTransformBuilderIF and VertTransformBuilderIF
 *
 * @author caron
 */
public abstract class AbstractTransformBuilder {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTransformBuilder.class);

   /*
   from CF: false_easting(false_northing):
    The value added to all abscissa(ordinate) values in the rectangular coordinates for a map projection.
    This value frequently is assigned to eliminate negative numbers.
    Expressed in the unit of the coordinate variable identified by the standard name projection_x_coordinate (projection_y_coordinate).
   */
  static public double getFalseEastingScaleFactor(NetcdfDataset ds, AttributeContainer ctv) {
    String units = getGeoCoordinateUnits(ds, ctv);
    return getFalseEastingScaleFactor(units);
  }

  static public String getGeoCoordinateUnits(NetcdfDataset ds, AttributeContainer ctv) {
    String units = ctv.findAttValueIgnoreCase(CDM.UNITS, null);
    if (units == null) {
      List<CoordinateAxis> axes = ds.getCoordinateAxes();
      for (CoordinateAxis axis : axes) {
        if (axis.getAxisType() == AxisType.GeoX) { // kludge - what if there's multiple ones?
          Variable v = axis.getOriginalVariable(); // LOOK why original variable ?
          units = (v == null) ? axis.getUnitsString() : v.getUnitsString();
          break;
        }
      }
    }
    return units;
  }

  static public double getFalseEastingScaleFactor(String geoCoordinateUnits) {
    if (geoCoordinateUnits != null) {
      try {
        SimpleUnit unit = SimpleUnit.factoryWithExceptions(geoCoordinateUnits);
        return unit.convertTo(1.0, SimpleUnit.kmUnit);
      } catch (Exception e) {
        log.warn(geoCoordinateUnits + " not convertible to km");
      }
    }
    return 1.0;
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  protected Formatter errBuffer = null;
  protected double lat0, lon0, false_easting, false_northing, earth_radius;
  protected Earth earth;

  public void setErrorBuffer(Formatter errBuffer) {
    this.errBuffer = errBuffer;
  }

  public abstract String getTransformName();

  protected void readStandardParams(AttributeContainer ctv, String units) {
    lon0 = readAttributeDouble(ctv, CF.LONGITUDE_OF_CENTRAL_MERIDIAN, Double.NaN);
    if (Double.isNaN(lon0))
      lon0 = readAttributeDouble(ctv, CF.LONGITUDE_OF_PROJECTION_ORIGIN, Double.NaN);
    lat0 = readAttributeDouble(ctv, CF.LATITUDE_OF_PROJECTION_ORIGIN, Double.NaN);
    false_easting = readAttributeDouble(ctv, CF.FALSE_EASTING, 0.0);
    false_northing = readAttributeDouble(ctv, CF.FALSE_NORTHING, 0.0);

    if ((false_easting != 0.0) || (false_northing != 0.0)) {
      double scalef = getFalseEastingScaleFactor(units);
      false_easting *= scalef;
      false_northing *= scalef;
    }

    double semi_major_axis = readAttributeDouble(ctv, CF.SEMI_MAJOR_AXIS, Double.NaN);
    double semi_minor_axis = readAttributeDouble(ctv, CF.SEMI_MINOR_AXIS, Double.NaN);
    double inverse_flattening = readAttributeDouble(ctv, CF.INVERSE_FLATTENING, 0.0);

    earth_radius = getEarthRadiusInKm(ctv);
    // check for ellipsoidal earth
    if (!Double.isNaN(semi_major_axis) && (!Double.isNaN(semi_minor_axis) || inverse_flattening != 0.0)) {
      earth = new Earth(semi_major_axis, semi_minor_axis, inverse_flattening);
    }
  }

  /**
   * Read a variable attribute as a double.
   *
   * @param v        the variable
   * @param attname  name of variable
   * @param defValue default value if attribute is not found
   * @return attribute value as a double, else NaN if not found
   */
  protected double readAttributeDouble(AttributeContainer v, String attname, double defValue) {
    Attribute att = v.findAttributeIgnoreCase(attname);
    if (att == null) return defValue;
    if (att.isString())
      return Double.parseDouble(att.getStringValue());
    else
      return att.getNumericValue().doubleValue();
  }

  /**
   * Read an attribute as double[2]. If only one value, make second same as first.
   *
   * @param att the attribute. May be numeric or String.
   * @return attribute value as a double[2]
   */
  protected double[] readAttributeDouble2(Attribute att) {
    if (att == null) return null;

    double[] val = new double[2];
    if (att.isString()) {
      StringTokenizer stoke = new StringTokenizer(att.getStringValue());
      val[0] = Double.parseDouble(stoke.nextToken());
      val[1] = stoke.hasMoreTokens() ? Double.parseDouble(stoke.nextToken()) : val[0];
    } else {
      val[0] = att.getNumericValue().doubleValue();
      val[1] = (att.getLength() > 1) ? att.getNumericValue(1).doubleValue() : val[0];
    }
    return val;
  }

  /**
   * Add a Parameter to a CoordinateTransform.
   * Make sure that the variable exists. If readData is true, read the data and use it as the value of the
   * parameter, otherwise use the variable name as the value of the parameter.
   *
   * @param rs             the CoordinateTransform
   * @param paramName      the parameter name
   * @param ds             dataset
   * @param varNameEscaped escaped variable name
   * @return true if success, false is failed
   */
  protected boolean addParameter(CoordinateTransform rs, String paramName, NetcdfFile ds, String varNameEscaped) {
    if (null == (ds.findVariable(varNameEscaped))) {
      if (null != errBuffer)
        errBuffer.format("CoordTransBuilder %s: no Variable named %s%n", getTransformName(), varNameEscaped);
      return false;
    }

    rs.addParameter(new Parameter(paramName, varNameEscaped));
    return true;
  }

  protected String getFormula(AttributeContainer ctv) {
    String formula = ctv.findAttValueIgnoreCase("formula_terms", null);
    if (null == formula) {
      if (null != errBuffer)
        errBuffer.format("CoordTransBuilder %s: needs attribute 'formula_terms' on Variable %s%n", getTransformName(), ctv.getName());
      return null;
    }
    return formula;
  }

  public String[] parseFormula(String formula_terms, String termString) {
    String[] formulaTerms = formula_terms.split("[\\s:]+");  // split on 1 or more whitespace or ':'
    String[] terms = termString.split("[\\s]+");             // split on 1 or more whitespace
    String[] values = new String[terms.length];

    for (int i = 0; i < terms.length; i++) {
      for (int j = 0; j < formulaTerms.length; j += 2) {  // look at every other formula term
        if (terms[i].equals(formulaTerms[j])) {     // if it matches
          values[i] = formulaTerms[j + 1];            // next term is the value
          break;
        }
      }
    }

    boolean ok = true;
    for (int i = 0; i < values.length; i++) {
      if (values[i] == null) {
        if (null != errBuffer)
          errBuffer.format("Missing term=%s in the formula '%s' for the vertical transform= %s%n", terms[i], formula_terms, getTransformName());
        ok = false;
      }
    }

    return ok ? values : null;
  }

  /**
   * Get the earth radius in km from the attribute "earth_radius".
   * Normally this is in meters, convert to km if its > 10,000.
   * Use Earth.getRadius() as default.
   *
   * @param ctv coord transform variable
   * @return earth radius in km
   */
  protected double getEarthRadiusInKm(AttributeContainer ctv) {
    double earth_radius = readAttributeDouble(ctv, CF.EARTH_RADIUS, Earth.getRadius());
    if (earth_radius > 10000.0) earth_radius *= .001;
    return earth_radius;
  }

  //////////////////////////////////////////
  /*
    //////////////////////////////////////////
  static public double getFalseEastingScaleFactor(NetcdfDataset ds, Variable ctv) {
    double factor = 1.0;
    String units = ds.findAttValueIgnoreCase(ctv, CDM.UNITS, null);
    if (units == null) {
      List<CoordinateAxis> axes = ds.getCoordinateAxes();
      for (CoordinateAxis axis : axes) {
        if (axis.getAxisType() == AxisType.GeoX) { // kludge - what if there's multiple ones?
          Variable v = axis.getOriginalVariable(); // LOOK why original variable ?
          units = v.getUnitsString();
          break;
        }
      }
    }

    if (units != null) {
      try {
        SimpleUnit unit = SimpleUnit.factoryWithExceptions(units);
        factor =  unit.convertTo(1.0, SimpleUnit.kmUnit);
      } catch (Exception e) {
        log.error(units + " not convertible to km");
      }
    }

    return factor;
  }
   */

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.logs;

import java.util.regex.*;
import java.util.Date;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.SimpleDateFormat;

/**
 * Read TDS access logs
 *
 * @author caron
 * @since Apr 10, 2008
 */
public class AccessLogParser implements LogReader.LogParser {

  ///////////////////////////////////////////////////////
  // log reading

  // sample
  // 1                2   3                           4                                                                                5   6  7     8               9
  // 128.117.140.75 - - [02/May/2008:00:46:26 -0600] "HEAD /thredds/dodsC/model/NCEP/DGEX/CONUS_12km/DGEX_CONUS_12km_20080501_1800.grib2.dds HTTP/1.1" 200 - "null" "Java/1.6.0_05" 21
  // 24.18.236.132 - - [04/Feb/2011:17:49:03 -0700] "GET /thredds/fileServer//nexrad/level3/N0R/YUX/20110205/Level3_YUX_N0R_20110205_0011.nids "       200 10409 "-" "-" 17

  // an access log without the extra fields at the end
  // 1           2  3                           4                        5   6                                                                      5   6  7     8               9
  // 127.0.0.1 - - [17/Jun/2015:13:48:32 -0600] "GET /thredds/ HTTP/1.1" 302 -

  // 30/Sep/2009:23:50:47 -0600
  private SimpleDateFormat formatFrom = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z");

  private static Pattern regPattern =
                         // 1                             2       3         4      5      6             7        8      9                                                         5   6  7     8               9
          Pattern.compile("^(\\d+\\.\\d+\\.\\d+\\.\\d+) - (.*) \\[(.*)\\] \"(.*)\" (\\d+) ([\\-\\d]+) \"(.*)\" \"(.*)\" (\\d+)");

  // pattern without the extra fields at the end         1                             2       3         4      5      6                                                                  5   6  7     8               9
  private static Pattern regPattern2 = Pattern.compile("^(\\d+\\.\\d+\\.\\d+\\.\\d+) - (.*) \\[(.*)\\] \"(.*)\" (\\d+) ([\\-\\d]+)");

  public LogReader.Log nextLog(BufferedReader dataIS) throws IOException {
    String line = dataIS.readLine();
    if (line == null) return null;
    LogReader.Log resultLog = parseLog(line);
    if (resultLog != null) {
      return resultLog;
    } else {
      // todo: FIX ME...no likey recursy.
      return nextLog(dataIS);
    }
    /* try {
      //System.out.println("\n"+line);
      Matcher m = regPattern.matcher(line);
      if (m.matches()) {
        LogReader.Log log = new LogReader.Log();
        log.ip = m.group(1);
        log.date = convertDate( m.group(3));
        String request = m.group(4);
        log.returnCode = parse(m.group(5));
        log.sizeBytes = parseLong(m.group(6));
        log.referrer = m.group(7);
        log.client = m.group(8);
        log.msecs = parseLong(m.group(9));

        String[] reqss = request.split(" ");
        if (reqss.length == 3) {
          log.verb = reqss[0].intern();
          log.path = reqss[1];
          log.http = reqss[2].intern();
        }

        return log;
      }
    } catch (Exception e) {
      e.printStackTrace();
      System.out.println("Cant parse " + line);
    }
    return null;  */
  }

  LogReader.Log parseLog(String line) throws IOException {
    try {
      // the enhanced log
      Matcher m = regPattern.matcher(line);
      if (m.matches()) {
        LogReader.Log log = new LogReader.Log();
        log.ip = m.group(1);
        log.date = convertDate( m.group(3));
        String request = m.group(4);
        log.returnCode = parse(m.group(5));
        log.sizeBytes = parseLong(m.group(6));
        log.referrer = m.group(7);
        log.client = m.group(8);
        log.msecs = parseLong(m.group(9));

        String[] reqss = request.split(" ");
        if (reqss.length == 3) {
          log.verb = reqss[0].intern();
          log.path = reqss[1];
          log.http = reqss[2].intern();

        } else if (reqss.length == 2) { // no HTTP/1.x
          log.verb = reqss[0].intern();
          log.path = reqss[1];
        }
        return log;

      } else {
        // the non-enhanced log
         m = regPattern2.matcher(line);
         if (m.matches()) {
           LogReader.Log log = new LogReader.Log();
           log.ip = m.group(1);
           log.date = convertDate(m.group(3));
           String request = m.group(4);
           log.returnCode = parse(m.group(5));
           log.sizeBytes = parseLong(m.group(6));

           log.referrer = "-";
           log.client = "-";
           log.msecs = -1;

           String[] reqss = request.split(" ");
           if (reqss.length == 3) {
             log.verb = reqss[0].intern();
             log.path = reqss[1];
             log.http = reqss[2].intern();

           } else if (reqss.length == 2) { // no HTTP/1.x
             log.verb = reqss[0].intern();
             log.path = reqss[1];
           }
           return log;
         }
      }

    } catch (Exception e) {
      e.printStackTrace();
      System.out.println("Cant parse " + line);
    }
    return null;
  }

  private long convertDate(String accessDateFormat) {
    // 30/Sep/2009:23:50:47 -0600
    try {
      Date d = formatFrom.parse(accessDateFormat);
      return d.getTime(); // formatTo.toDateTimeStringISO(d);
    } catch (Throwable t) {
      System.out.printf("Bad date format = %s err = %s%n", accessDateFormat, t.getMessage());
    }
    return -1;
  }

  private int parse(String s) {
    if (s.equals("-")) return 0;
    return Integer.parseInt(s);
  }

  private long parseLong(String s) {
    if (s.equals("-")) return 0;
    return Long.parseLong(s);
  }

  // try problem logs
  public static void main(String[] args) throws IOException {
    AccessLogParser p = new AccessLogParser();
    String line = "24.18.236.132 - - [04/Feb/2011:17:49:03 -0700] \"GET /thredds/fileServer//nexrad/level3/N0R/YUX/20110205/Level3_YUX_N0R_20110205_0011.nids \" 200 10409 \"-\" \"-\" 17";
    Matcher m = regPattern.matcher(line);
    System.out.printf("%s %s%n", m.matches(), m);
    for (int i=0; i<m.groupCount(); i++) {
      System.out.println(" "+i+ " "+m.group(i));
    }

    LogReader.Log log = p.parseLog(line);
    System.out.printf("%s%n", log);
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.catalog;

/**
 * Concrete access element.
 * @see InvAccess
 *
 * @author john caron
 */

public class InvAccessImpl extends InvAccess {

  private StringBuilder log = new StringBuilder();

  protected String serviceName;
  private String serviceTypeName, dataFormatName;

  /**
   * Construct from an existing InvService.
   * @param dataset : the containing dataset.
   * @param urlPath : the urlPath.
   * @param service : reference to an existing service.
   */
  public InvAccessImpl( InvDataset dataset, String urlPath, InvService service) {
    this.dataset = dataset;
    this.urlPath = urlPath.trim();
    this.service = service;
  }

  // for Aggregation.FileAccess
  protected InvAccessImpl( InvDataset dataset, String urlPath) {
    this.dataset = dataset;
    this.urlPath = urlPath.trim();
  }

  /**
   * Construct from fields in XML catalog. Either serviceName or typeName must be specified, not both.
   *
   * @param dataset : the containing dataset.
   * @param urlPath : the urlPath.
   * @param serviceName : name of an existing service, or null.
   * @param typeName : ServiceType name or null. If specified, this creates an anonymous
   *   Service (one that is only used by this access element).
   * @param dataFormatName  optional
   * @param dataSize optional
   */
  public InvAccessImpl( InvDataset dataset, String urlPath, String serviceName, String typeName,
                        String dataFormatName, double dataSize) {
    this.dataset = dataset;
    this.urlPath = urlPath;
    this.serviceName = serviceName;
    this.serviceTypeName = typeName;
    this.dataFormatName = dataFormatName;
    this.dataSize = dataSize;

    // may define an anonymous service
    if (typeName != null) {
      if (serviceName != null)
        log.append("**InvAccess in (").append(dataset.getFullName()).append("):cannot declare both service (").append(serviceName).append(")" + " and serviceType <").append(typeName).append(">\n");
      else {
        this.service = new InvService("", typeName, "", "", null); // "anonymous" service
        //if ((url != null) && !url.isAbsolute())
        //  log.append("  **InvAccess (4) in ("+dataset.getFullName()+"): urlPath must be absolute ("+urlPath+") for anonymous service\n");
      }
    }

  }

  /*** Finish constructing after all elements have been added.
   * @return true if successful.
   **/
  public boolean finish() {
    if (serviceName != null) {
      this.service = dataset.findService(serviceName);
      if (this.service == null)
        log.append("**InvAccess in (").append(dataset.getFullName()).append("): has unknown service named (").append(serviceName).append(")\n");
    }

      // check urlPath is ok
    try {
      new java.net.URI(urlPath);
    } catch (java.net.URISyntaxException e) {
      log.append("**InvAccess in (").append(dataset.getFullName()).append("):\n" + "   urlPath= ").append(urlPath).append(")\n  URISyntaxException=").append(e.getMessage());
    }

    return true;
  }

    /** String representation */
  public String toString() {
    return "service:("+service.getName()+") path:("+urlPath+")";
  }

    /** @return the service name if specified */
  public String getServiceName() { return serviceName; }
    /** @return the service type name if specified */
  public String getServiceType() { return serviceTypeName; }
    /** @return the data format type name if specified */
  public String getDataFormatName() { return dataFormatName; }

  public void setSize( double dataSize) { this.dataSize = dataSize; }

  /** InvAccessImpl elements with same values are equal. */
  public boolean equals(Object o) {
     if (this == o) return true;
     if (!(o instanceof InvAccessImpl)) return false;
     return o.hashCode() == this.hashCode();
  }
  /** Override Object.hashCode() to implement equals. */
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      if (urlPath != null)
        result = 37*result + urlPath.hashCode();
      if (service != null)
        result = 37*result + service.hashCode();
      if (hasDataSize())
        result = 37*result + (int) getDataSize();
      hashCode = result;
    }
    return hashCode;
  }
  private volatile int hashCode = 0; // Bloch, item 8

  //////////////////////////////////////////////////////////////////////////////

  boolean check(StringBuilder out, boolean show) {
    boolean isValid = true;

    if (log.length() > 0) {
      isValid = false;
      out.append( log);
    }

    if (getService() == null) {
      out.append("**InvAccess in (").append(dataset.getFullName()).append("): with urlPath= (").append(urlPath).append(") has no valid service\n");
      isValid = false;
    }

    else if (getStandardUrlName() == null) {
      out.append("**InvAccess in (").append(dataset.getFullName()).append("): with urlPath= (").append(urlPath).append(") has invalid URL\n");
      isValid = false;
    }

    if (show) System.out.println("   access "+urlPath+" valid = "+isValid);

    return isValid;
  }

}