Skip to content

Commit 2d9666e

Browse files
committed
add new utility application SampLoad
org.astrogrid.samp.util.SampLoad is intended as a standalone application that takes a file or URL representing a loadable thing (VOTable, FITS image or CDF file) and sends it to a selected SAMP client or clients using a suitable *.load.* MType.
1 parent b2ca7c1 commit 2d9666e

3 files changed

Lines changed: 960 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
package org.astrogrid.samp.util;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.UnsupportedEncodingException;
6+
import java.net.HttpURLConnection;
7+
import java.net.URL;
8+
import java.net.URLConnection;
9+
import java.util.LinkedHashMap;
10+
import java.util.Map;
11+
import java.util.logging.Level;
12+
import java.util.logging.Logger;
13+
import org.astrogrid.samp.Message;
14+
15+
/**
16+
* Defines one of the types of resource that can be turned into a
17+
* SAMP load-type message.
18+
*
19+
* @author Mark Taylor
20+
* @since 21 Aug 2019
21+
*/
22+
public abstract class ResourceType {
23+
24+
private final String name_;
25+
private final String mtype_;
26+
private final String[] ctypes_;
27+
28+
/** Resource type for table.load.votable. */
29+
public static final ResourceType RTYPE_VOTABLE;
30+
31+
/** Resource type for image.load.fits. */
32+
public static final ResourceType RTYPE_FITS;
33+
34+
/** Resource type for table.load.cdf. */
35+
public static final ResourceType RTYPE_CDF;
36+
37+
private static final ResourceType[] RESOURCE_TYPES = {
38+
RTYPE_VOTABLE = createVOTableResourceType(),
39+
RTYPE_FITS = createFitsImageResourceType(),
40+
RTYPE_CDF = createCdfTableResourceType(),
41+
};
42+
private static final int MAGIC_SIZE = 1024;
43+
private static final Logger logger_ =
44+
Logger.getLogger( ResourceType.class.getName() );
45+
46+
/**
47+
* Constructor.
48+
*
49+
* @param name identifying name
50+
* @param mtype MType of message that will be sent
51+
* @param ctypes MIME-types to which this corresponds,
52+
* supplied in normalised form
53+
* (lower case, no parameters, no whitespace)
54+
*/
55+
public ResourceType( String name, String mtype, String[] ctypes ) {
56+
name_ = name;
57+
mtype_ = mtype;
58+
ctypes_ = ctypes;
59+
}
60+
61+
/**
62+
* Returns the MType of the message to be constructed.
63+
*
64+
* @return MType string
65+
*/
66+
public String getMType() {
67+
return mtype_;
68+
}
69+
70+
/**
71+
* Returns a Message object that will forward a given URL to SAMP
72+
* clients.
73+
*
74+
* @param url URL of resource
75+
* @return message instance
76+
*/
77+
public Message createMessage( URL url ) {
78+
Map params = new LinkedHashMap();
79+
params.put( "url", url.toString() );
80+
return new Message( mtype_, params );
81+
}
82+
83+
/**
84+
* Indicates whether this resource type is suitable for use
85+
* with a given MIME type. Note that the submitted content type
86+
* may contain additional parameters and have embedded whitespace etc
87+
* as permitted by RFC 2045.
88+
*
89+
* @param ctype content-type header value
90+
* @return true iff this resource type is suitable for use with
91+
* the given content type
92+
*/
93+
public boolean isContentType( String ctype ) {
94+
ctype = ctype.replaceAll( " *;.*", "" )
95+
.replaceAll( "\\s+", "" )
96+
.toLowerCase();
97+
for ( int ic = 0; ic < ctypes_.length; ic++ ) {
98+
if ( ctype.startsWith( ctypes_[ ic ] ) ) {
99+
return true;
100+
}
101+
}
102+
return false;
103+
}
104+
105+
/**
106+
* Indicates whether this resource type is suitable for use
107+
* with a resource having a given magic number.
108+
*
109+
* @param magic buffer containing the first few bytes of
110+
* resource content
111+
* @return true iff this resource type is suitable for use
112+
* with the given content
113+
*/
114+
public abstract boolean isMagic( byte[] magic );
115+
116+
/**
117+
* Returns the name of this resource type.
118+
*
119+
* @return name
120+
*/
121+
public String getName() {
122+
return name_;
123+
}
124+
125+
public String toString() {
126+
return name_;
127+
}
128+
129+
/**
130+
* Returns the known resource types.
131+
*
132+
* @return known instances of this class
133+
*/
134+
public static ResourceType[] getKnownResourceTypes() {
135+
return (ResourceType[]) RESOURCE_TYPES.clone();
136+
}
137+
138+
/**
139+
* Attempts to determine the resource type of a given URL by
140+
* making an HTTP HEAD request and looking at the Content-Type.
141+
*
142+
* @param url resource location
143+
* @return good guess at resource type, or null if can't be determined
144+
*/
145+
public static ResourceType readHeadResourceType( URL url ) {
146+
try {
147+
URLConnection uconn = url.openConnection();
148+
if ( uconn instanceof HttpURLConnection ) {
149+
logger_.info( "HEAD " + url );
150+
HttpURLConnection hconn = (HttpURLConnection) uconn;
151+
hconn.setInstanceFollowRedirects( true );
152+
hconn.setRequestMethod( "HEAD" );
153+
hconn.connect();
154+
int code = hconn.getResponseCode();
155+
if ( code == 200 ) {
156+
String ctype = hconn.getContentType();
157+
logger_.info( "HEAD Content-Type: " + ctype );
158+
return getMimeResourceType( ctype );
159+
}
160+
else {
161+
logger_.warning( "HEAD response code " + code );
162+
return null;
163+
}
164+
}
165+
else {
166+
return null;
167+
}
168+
}
169+
catch ( IOException e ) {
170+
logger_.log( Level.WARNING, "HEAD failed", e );
171+
return null;
172+
}
173+
}
174+
175+
/**
176+
* Attempts to determine the resource type of a given URL by
177+
* downloading the first part of its content and examining the
178+
* magic number.
179+
*
180+
* @param url resource location
181+
* @return good guess at resource type, or null if it can't be determined
182+
*/
183+
public static ResourceType readContentResourceType( URL url ) {
184+
185+
// Acquire the magic number.
186+
byte[] buf = new byte[ MAGIC_SIZE ];
187+
InputStream in = null;
188+
logger_.info( "GET " + url );
189+
try {
190+
191+
// Open a GET connection.
192+
URLConnection uconn = url.openConnection();
193+
if ( uconn instanceof HttpURLConnection ) {
194+
HttpURLConnection hconn = (HttpURLConnection) uconn;
195+
hconn.setInstanceFollowRedirects( true );
196+
hconn.connect();
197+
int code = hconn.getResponseCode();
198+
if ( code != 200 ) {
199+
logger_.warning( "GET response code " + code );
200+
return null;
201+
}
202+
203+
// The content-type may be usable here, even if the
204+
// presumed earlier call to HEAD failed
205+
// (for instance HEAD not implemented).
206+
String ctype = hconn.getContentType();
207+
ResourceType rtype = getMimeResourceType( ctype );
208+
if ( rtype != null ) {
209+
logger_.info( "GET Content-Type: " + ctype );
210+
return rtype;
211+
}
212+
}
213+
else {
214+
uconn.connect();
215+
}
216+
217+
// Read the first few bytes into a buffer, then close the stream.
218+
in = uconn.getInputStream();
219+
for ( int off = 0; off < MAGIC_SIZE; ) {
220+
int nr = in.read( buf, off, MAGIC_SIZE - off );
221+
if ( nr > 0 ) {
222+
off += nr;
223+
}
224+
else {
225+
break;
226+
}
227+
}
228+
}
229+
catch ( IOException e ) {
230+
logger_.log( Level.WARNING, "GET failed", e );
231+
}
232+
finally {
233+
if ( in != null ) {
234+
try {
235+
in.close();
236+
}
237+
catch ( IOException e ) {
238+
}
239+
}
240+
}
241+
242+
// Try to determine type from magic number.
243+
return getMagicResourceType( buf );
244+
}
245+
246+
/**
247+
* Try to identify a resource type from its MIME type.
248+
*
249+
* @param contentType content-type header
250+
* @return resource type, or null if not known
251+
*/
252+
private static ResourceType getMimeResourceType( String contentType ) {
253+
if ( contentType != null ) {
254+
for ( int i = 0; i < RESOURCE_TYPES.length; i++ ) {
255+
ResourceType rtype = RESOURCE_TYPES[ i ];
256+
if ( rtype.isContentType( contentType ) ) {
257+
return rtype;
258+
}
259+
}
260+
}
261+
return null;
262+
}
263+
264+
/**
265+
* Try to identify a resource type from its magic number.
266+
*
267+
* @param magic buffer containing first few bytes of resource content
268+
* @return resource type, or null if not known
269+
*/
270+
private static ResourceType getMagicResourceType( byte[] magic ) {
271+
for ( int i = 0; i < RESOURCE_TYPES.length; i++ ) {
272+
ResourceType rtype = RESOURCE_TYPES[ i ];
273+
if ( rtype.isMagic( magic ) ) {
274+
logger_.info( "GET magic number looks like " + rtype );
275+
return rtype;
276+
}
277+
}
278+
return null;
279+
}
280+
281+
/**
282+
* Returns a ResourceType instance suitable for the table.load.votable
283+
* SAMP MType.
284+
*
285+
* @return VOTable resource type
286+
*/
287+
private static ResourceType createVOTableResourceType() {
288+
return new ResourceType( "VOTable", "table.load.votable",
289+
new String[] { "application/x-votable+xml",
290+
"text/xml" } ) {
291+
public boolean isMagic( byte[] buf ) {
292+
// Shocking hack that should work for UTF-8 and UTF-16*.
293+
String txt;
294+
try {
295+
txt = new String( buf, "US-ASCII" );
296+
}
297+
catch ( UnsupportedEncodingException e ) {
298+
return false;
299+
}
300+
return txt.contains( "<VOTABLE" )
301+
|| txt.contains( "<\0V\0O\0T\0A\0B\0L\0E" );
302+
}
303+
};
304+
}
305+
306+
/**
307+
* Returns a ResourceType instance suitable for the image.load.fits
308+
* SAMP MType.
309+
*
310+
* @return FITS image resource type
311+
*/
312+
private static ResourceType createFitsImageResourceType() {
313+
return new ResourceType( "FITS", "image.load.fits",
314+
new String[] { "image/fits",
315+
"application/fits" } ) {
316+
public boolean isMagic( byte[] buf ) {
317+
return buf.length >= 9 &&
318+
(char) buf[ 0 ] == 'S' &&
319+
(char) buf[ 1 ] == 'I' &&
320+
(char) buf[ 2 ] == 'M' &&
321+
(char) buf[ 3 ] == 'P' &&
322+
(char) buf[ 4 ] == 'L' &&
323+
(char) buf[ 5 ] == 'E' &&
324+
(char) buf[ 6 ] == ' ' &&
325+
(char) buf[ 7 ] == ' ' &&
326+
(char) buf[ 8 ] == '=';
327+
}
328+
};
329+
}
330+
331+
/**
332+
* Returns a ResourceType instance suitable for the table.load.cdf
333+
* SAMP Mtype.
334+
*
335+
* @return CDF table resource type
336+
*/
337+
private static ResourceType createCdfTableResourceType() {
338+
return new ResourceType( "CDF", "table.load.cdf", new String[ 0 ] ) {
339+
public boolean isMagic( byte[] buf ) {
340+
if ( buf.length >= 8 ) {
341+
int m1 = ( buf[ 0 ] & 0xff ) << 24
342+
| ( buf[ 1 ] & 0xff ) << 16
343+
| ( buf[ 2 ] & 0xff ) << 8
344+
| ( buf[ 3 ] & 0xff ) << 0;
345+
int m2 = ( buf[ 4 ] & 0xff ) << 24
346+
| ( buf[ 5 ] & 0xff ) << 16
347+
| ( buf[ 6 ] & 0xff ) << 8
348+
| ( buf[ 7 ] & 0xff ) << 0;
349+
// Version 2.6+ only.
350+
return ( m1 == 0xcdf30001 || m1 == 0xcdf26002 )
351+
&& ( m2 == 0x0000ffff || m2 == 0xcccc0001 );
352+
}
353+
else {
354+
return false;
355+
}
356+
}
357+
};
358+
}
359+
}

0 commit comments

Comments
 (0)