package org.autoplot.hapiserver; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; import org.das2.qds.DataSetUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * helpful functions. * @author jbf */ public class Util { private static final Logger LOGGER= Logger.getLogger("hapi"); /** * return the duration in a easily-human-consumable form. * @param dt the duration in milliseconds. * @return a duration like "2.6 hours" */ public static String getDurationForHumans( long dt ) { if ( dt<2*1000 ) { return dt+" milliseconds"; } else if ( dt<2*60000 ) { return String.format( Locale.US, "%.1f",dt/1000.)+" seconds"; } else if ( dt<2*3600000 ) { return String.format( Locale.US, "%.1f",dt/60000.)+" minutes"; } else if ( dt<2*86400000 ) { return String.format( Locale.US, "%.1f",dt/3600000.)+" hours"; } else { return String.format( Locale.US, "%.1f",dt/86400000.)+" days"; } } /** * if HAPI_HOME has not been set, then set it. * @param context */ public static void maybeInitialize( ServletContext context ) { if ( HAPI_HOME==null ) { String s= context.getInitParameter(HAPI_SERVER_HOME_PROPERTY); setHapiHome( new File( s ) ); } } private static volatile File HAPI_HOME=null; /** * return the root of the HAPI server. * @return the root of the HAPI server. */ public static File getHapiHome() { if ( Util.HAPI_HOME==null ) { throw new IllegalArgumentException("Util.HAPI_HOME is not set, load info page first."); } else { return Util.HAPI_HOME; } } public static void setHapiHome( File f ) { File HAPI_HOME_ = Util.HAPI_HOME; if ( HAPI_HOME_==null ) { synchronized ( Util.class ) { HAPI_HOME_ = Util.HAPI_HOME; if ( HAPI_HOME_==null ) { Util.HAPI_HOME= f; } } } else { // it has been set already. } } /** * This should point to the name of the directory containing HAPI configuration. * This directory should contain catalog.json, capabilities.json and a * subdirectory "info" which contains files with the name <ID>.json, * each containing the info response. Note these should also contain a * tag "x_uri" which is the Autoplot URI that serves this data. */ public static final String HAPI_SERVER_HOME_PROPERTY = "HAPI_SERVER_HOME"; /** * return the HAPI protocol version. * @return the HAPI protocol version. */ public static final String hapiVersion() { return "3.0"; } /** * return the server implementation version. * @return the server implementation version. */ public static final String serverVersion() { return "20210430.1221"; } static boolean isKey(String key) { Pattern p= Pattern.compile("\\d{8}+"); return p.matcher(key).matches(); } /** * this key can create place for the data * @param id * @param key * @return */ static boolean keyCanCreate(String id,String key) { if ( !isKey(key) ) { throw new IllegalArgumentException("is not a key: "+key); } File keyFile= new File( getHapiHome(), "keys" ); if ( !keyFile.exists() ) return false; keyFile= new File( keyFile, id + ".json" ); if ( !keyFile.exists() ) return false; try { JSONObject jo= HapiServerSupport.readJSON(keyFile); if ( jo.has(key) ) { jo= jo.getJSONObject(key); return jo.getBoolean("create"); } else { return false; } } catch ( IOException | JSONException ex ) { throw new RuntimeException(ex); } } /** * this key can modify or add records to the data * @param id * @param key * @return */ static boolean keyCanModify(String id,String key) { if ( !isKey(key) ) { throw new IllegalArgumentException("is not a key: "+key); } File keyFile= new File( getHapiHome(), "keys" ); if ( !keyFile.exists() ) return false; keyFile= new File( keyFile, id + ".json" ); if ( !keyFile.exists() ) return false; try { JSONObject jo= HapiServerSupport.readJSON(keyFile); if ( jo.has(key) ) { jo= jo.getJSONObject(key); return jo.getBoolean("modify") || jo.getBoolean("create"); } else { return false; } } catch ( IOException | JSONException ex ) { throw new RuntimeException(ex); } } /** * this key can delete records from the data * @param id * @param key * @return */ static boolean keyCanDelete(String id,String key) { if ( !isKey(key) ) { throw new IllegalArgumentException("is not a key: "+key); } File keyFile= new File( getHapiHome(), "keys" ); if ( !keyFile.exists() ) return false; keyFile= new File( keyFile, id + ".json" ); if ( !keyFile.exists() ) return false; try { JSONObject jo= HapiServerSupport.readJSON(keyFile); if ( jo.has(key) ) { jo= jo.getJSONObject(key); return jo.getBoolean("delete") || jo.getBoolean("create"); } else { return false; } } catch ( IOException | JSONException ex ) { throw new RuntimeException(ex); } } /** * transfers the data from one channel to another. src and dest are * closed after the operation is complete. * @param src * @param dest * @throws java.io.IOException */ public static void transfer( InputStream src, OutputStream dest ) throws IOException { final byte[] buffer = new byte[ 16 * 1024 ]; int i= src.read(buffer); while ( i != -1) { dest.write(buffer,0,i); i= src.read(buffer); } dest.close(); src.close(); } /** * return true if this is valid JSON, false otherwise, and log the exception at SEVERE. * @param json * @return */ public static boolean validateJSON( String json ) { try { new JSONObject( json ); return true; } catch (JSONException ex) { Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex); return false; } } /** * send a "bad id" response to the client. * @param id the id. * @param response the response object * @param out the print writer for the response object. */ public static void raiseBadId(String id, HttpServletResponse response, final PrintWriter out) { try { JSONObject jo= new JSONObject(); jo.put("HAPI",Util.hapiVersion()); jo.put("createdAt",String.format("%tFT%<tRZ",Calendar.getInstance(TimeZone.getTimeZone("Z")))); JSONObject status= new JSONObject(); status.put( "code", 1406 ); String msg= "unrecognized id: "+id; status.put( "message", msg ); jo.put("status",status); String s= jo.toString(4); response.setStatus(404); response.setContentType("application/json;charset=UTF-8"); out.write(s); } catch (JSONException ex) { throw new RuntimeException(ex); } } /** * return the total number of elements of each parameter. * @param info the info * @return an int array with the number of elements in each parameter. * @throws JSONException */ public static int[] getNumberOfElements( JSONObject info ) throws JSONException { JSONArray parameters= info.getJSONArray("parameters"); int[] result= new int[parameters.length()]; for ( int i=0; i<parameters.length(); i++ ) { int len=1; if ( parameters.getJSONObject(i).has("size") ) { JSONArray jarray1= parameters.getJSONObject(i).getJSONArray("size"); for ( int k=0; k<jarray1.length(); k++ ) { len*= jarray1.getInt(k); } } result[i]= len; } return result; } /** * return a new JSONObject for the info request, with the subset of parameters. * @param info the root node of the info response. * @param parameters comma-delimited list of parameters. * @return the new JSONObject, with special tag __indexmap__ showing which columns are to be included in a data response. * @throws JSONException */ public static JSONObject subsetParams( JSONObject info, String parameters ) throws JSONException { info= new JSONObject( info.toString() ); // force a copy String[] pps= parameters.split(","); Map<String,Integer> map= new HashMap(); // map from name to index in dataset. Map<String,Integer> iMap= new HashMap(); // map from name to position in csv. JSONArray jsonParameters= info.getJSONArray("parameters"); int index=0; int[] lens= getNumberOfElements(info); for ( int i=0; i<jsonParameters.length(); i++ ) { String name= jsonParameters.getJSONObject(i).getString("name"); map.put( name, i ); iMap.put( name, index ); index+= lens[i]; } JSONArray newParameters= new JSONArray(); int[] indexMap= new int[pps.length]; for ( int i=0; i<pps.length; i++ ) { indexMap[i]=-1; } int[] lengths= new int[pps.length]; //lengths for the new infos boolean hasTime= false; for ( int ip=0; ip<pps.length; ip++ ) { Integer i= map.get(pps[ip]); if ( i==null ) { throw new IllegalArgumentException("bad parameter: "+pps[ip]); } indexMap[ip]= iMap.get(pps[ip]); if ( i==0 ) { hasTime= true; } newParameters.put( ip, jsonParameters.get(i) ); lengths[ip]= lens[i]; } // add time if it was missing. This demonstrates a feature that is burdensome to implementors, I believe. if ( !hasTime ) { int[] indexMap1= new int[1+indexMap.length]; int[] lengths1= new int[1+lengths.length]; indexMap1[0]= 0; System.arraycopy( indexMap, 0, indexMap1, 1, indexMap.length ); lengths1[0]= 1; System.arraycopy( lengths, 0, lengths1, 1, indexMap.length ); indexMap= indexMap1; lengths= lengths1; for ( int k=newParameters.length()-1; k>=0; k-- ) { newParameters.put( k+1, newParameters.get(k) ); } newParameters.put(0,jsonParameters.get(0)); } // unpack the resort where the lengths are greater than 1. int[] indexMap1= new int[ DataSetUtil.sum(lengths) ]; int c= 0; if ( indexMap1.length>indexMap.length ) { for ( int k=0; k<lengths.length; k++ ) { if ( lengths[k]==1 ) { indexMap1[c]= indexMap[k]; c++; } else { for ( int l=0; l<lengths[k]; l++ ) { indexMap1[c]= indexMap[k]+l; c++; } } } indexMap= indexMap1; } if ( indexMap[indexMap.length-1]==-1 ) { throw new IllegalArgumentException("last index of index map wasn't set--server implementation error"); } jsonParameters= newParameters; info.put( "parameters", jsonParameters ); info.put( "x_indexmap", indexMap ); return info; } /** * split, but not when comma is within quotes. * @param line for example 'a,b,"c,d"' * @param nf number of fields, or -1 for no constraint * @return ['a','b','c,d'] */ public static String[] csvSplit(String line, int nf) { String[] result = line.split(",", -2); if (result.length == nf) { return result; } else { int j0 = 0; StringBuilder b = new StringBuilder(); boolean withinQuote = false; for (String result1 : result) { String[] f1 = result1.split("\"", -2); b.append(f1[0]); for (int k = 1; k < f1.length; k++) { b.append(f1[k]); withinQuote = !withinQuote; } if (!withinQuote) { result[j0] = b.toString(); b = new StringBuilder(); j0++; } else { b.append(","); } } if (nf > -1) { if (j0 < nf) { throw new IllegalArgumentException("expected " + nf + " fields"); } else if (j0 != nf) { LOGGER.log(Level.WARNING, "expected {0} fields, got {1}", new Object[]{nf, j0}); } } return Arrays.copyOfRange(result, 0, j0); } } public static void main( String[] args ) { String line= "a,b,\"c,d\""; String[] ss; ss= csvSplit( line, -1 ); for ( String s: ss ) { System.err.println( s ); } } }