source: main/trunk/gli/src/org/greenstone/gatherer/metadata/FilenameEncoding.java@ 33728

Last change on this file since 33728 was 33728, checked in by ak19, 4 years ago

Introducing method that I've tested separately to decode a string that contains html hex entities back to unicode characters. Don't know if it's the most optimal solution, because it's my own code. Didn't know how to google for existing Java solutions, which surely must be out there.

File size: 22.3 KB
Line 
1/**
2 *############################################################################
3 * A component of the Greenstone Librarian Interface, part of the Greenstone
4 * digital library suite from the New Zealand Digital Library Project at the
5 * University of Waikato, New Zealand.
6 *
7 * Author: Michael Dewsnip, NZDL Project, University of Waikato, NZ
8 *
9 * Copyright (C) 2010 Greenstone Digital Library Project
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24 *############################################################################
25 */
26
27package org.greenstone.gatherer.metadata;
28
29import java.io.File;
30import java.net.*;
31import java.nio.charset.*;
32import java.util.*;
33import org.greenstone.gatherer.collection.CollectionManager;
34import org.greenstone.gatherer.DebugStream;
35
36import java.util.regex.Matcher;
37import java.util.regex.Pattern;
38
39
40
41/** Static access class that contains many of the methods used to work with filename encodings.
42* Works closely with classes FileNode, CollectionTreeNode, MetadataXMLFile, MetadataXMLFileManager
43* to maintain a map of URLEncodedFilenames to their filename encodings.
44* The process of filename encoding further affects the CollectionManager which refreshes its CollectionTree,
45* FileManager (move, delete, rename actions), MetadataValueTableModel, EnrichPane. */
46
47public class FilenameEncoding {
48 /** Display of filenames in the trees are in URL encoding, if debugging */
49 public static boolean DEBUGGING = false;
50
51 /** Set to false by Gatherer if the locale is UTF-8, as Java's handling is
52 * such that non-UTF8 filename encodings on a UTF-8 locale are destructively
53 * converted so that the bytecodes in the filename are not preserved. */
54 public static boolean MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = false;
55
56 /** Also set by Gatherer.
57 * If the OS supports multiple filename encodings, we will be working with URL strings
58 * and the applicable separators are always the forward slash ("/") not File.separator.
59 * If multiple filename encodings are not supported, we're dealing with File.separator. */
60 public static String URL_FILE_SEPARATOR = File.separator;
61
62
63 /** gs.filenameEncoding is a special sort of metadata that is not merely to be stored along
64 * with a file, but is to be applied in real-time on the file's name in the CollectionTree
65 * display. Since FileNodes are constantly destroyed and reconstructed by that Tree when
66 * its nodes are expanded and contracted, storing the filename encodings of each file along
67 * with the file in a FileNode doesn't help because it doesn't last. Instead of rediscovering
68 * the encoding at every stage by querying the metadataXML file, we store the encodings for
69 * fast access: in a map of (URLEncodedFilePath, filename-encoding) pairs.
70 * The current design of the map is to only store any active filename metadata assigned
71 * directly at that file/folder's level, and if there is none discovered at that level, then
72 * storing the empty string for it. Therefore, if the hashmap contains no entry for
73 * a file, it means this still needs to be retrieved. */
74 public static Map map = new HashMap();
75
76//*********************** BUSY REFRESHING / REQUIRING REFRESH *********************
77
78 /** Set to true if filename encoding metadata was changed. Called by the enter keyPress
79 * event in gui.EnrichPane and when the gs.FilenameEncoding field loses focus. */
80 private static boolean refreshRequired = false;
81
82 synchronized public static boolean isRefreshRequired() {
83 return refreshRequired;
84 }
85
86 synchronized public static void setRefreshRequired(boolean state) {
87 if(MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
88 refreshRequired = state;
89 } else {
90 refreshRequired = false;
91 }
92 }
93
94//************************** MAP RETRIEVAL METHODS ******************************
95
96 /** Returns the cumulative gs.filenameEncoding metadata
97 * assigned to a file inside the collection. */
98 public static String findFilenameEncoding(
99 File file, String urlEncodedFilePath, boolean bruteForceLookup)
100 {
101 //if(bruteForceLookup) {
102 // return findFilenameEncodingBruteForce(file, urlEncodedFilePath, bruteForceLookup);
103 //}
104
105 String encoding = "";
106
107 // Check any assigned encoding at this level, starting with the map first
108 // and else retrieving the filename encoding from the metadata file
109 if(!map.containsKey(urlEncodedFilePath)) {
110
111 // Check for filename encoding metadata *directly* associated with the file
112 // Now don't need to get any inherited encoding metadata here, because of
113 // the way we're storing and retrieving encoding information from the map.
114 ArrayList list = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(file, true); // true: gets gs.filenameEncoding only
115 if(!list.isEmpty()) {
116 MetadataValue metavalue = (MetadataValue)list.get(0); // get(list.size()-1);
117 encoding = metavalue.getValue();
118 } // else no filename encoding set yet at this level
119
120 // Now we've done a lookup at this level cache the result in the map,
121 // including empty strings, to indicate that we've done a full lookup
122 map.put(urlEncodedFilePath, encoding);
123 }
124 else { // an entry exists in the map, get it from there
125 encoding = (String)map.get(urlEncodedFilePath);
126 }
127
128 // if no meta was specified at at the file level, look for any inherited metadata
129 if(encoding.equals("")) {
130 encoding = getInheritedFilenameEncoding(urlEncodedFilePath, file);
131 }
132
133 //System.err.println("\n@@@@Looked for: " + urlEncodedFilePath + " | found: " + encoding);
134 return encoding; // found something in map, may still be "", but it's what was stored
135 }
136
137 /** Checks the file-to-encoding map for all the superfolders of the given
138 * filename in sequence for an applicable encoding. Note that the file/folder
139 * at the level of urlFoldername (and dir) has already been inspected. */
140 static public String getInheritedFilenameEncoding(String urlFoldername, File dir)
141 {
142 String encoding = "";
143 boolean done = false;
144
145 // don't want to search past import folder which is as
146 // far as we need to go to determine inherited encodings
147 File importDir = new File(CollectionManager.getLoadedCollectionImportDirectoryPath());
148 if(dir.equals(importDir)) { // if the top-level dir was already checked, we're done
149 done = true;
150 }
151
152 // For directories, first remove trailing file separator in order to start checking from higher level folders
153 int lastIndex = urlFoldername.length()-1;
154 char urlFileSeparatorChar = URL_FILE_SEPARATOR.charAt(0);
155 if(urlFoldername.charAt(lastIndex) == urlFileSeparatorChar) {
156 urlFoldername = urlFoldername.substring(0, lastIndex);
157 }
158
159 while(!done) {
160 // get the folder that's one level up
161 dir = dir.getParentFile();
162
163 int index = urlFoldername.lastIndexOf(URL_FILE_SEPARATOR);
164 if(index == -1) { // no more slashes
165 done = true;
166 } else {
167 urlFoldername = urlFoldername.substring(0, index);
168 }
169
170 // now look in the map to see whether there's an encoding for this folder
171 String folder = urlFoldername + URL_FILE_SEPARATOR;
172 if(map.containsKey(folder)) {
173 encoding = (String)map.get(folder); // may be ""
174 } else { // no entry in map, so look in the metadata.xml at this folder level
175 ArrayList list = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(
176 dir, true); // true: gets gs.filenameEncoding only
177 if(!list.isEmpty()) {
178 MetadataValue metavalue = (MetadataValue)list.get(0); // get(list.size()-1);
179 encoding = metavalue.getValue();
180 }
181 map.put(folder, encoding); // may be ""
182 }
183
184 if(!encoding.equals("")){
185 done = true;
186 } // else if "", loop to check next folder up
187 else if(dir.equals(importDir)) { // don't iterate past the import folder, which we've now checked
188 done = true;
189 }
190 }
191
192 return encoding;
193 }
194
195 /** Called by GUIManager when a collection is closed. This then empties the
196 * file-to-encoding map which is applicable only on a per-collection basis */
197 static public void closeCollection() {
198 //printFilenameMap("Closing collection. Clearing file-to-encoding map of entries:");
199 map.clear();
200 }
201
202 // Useful for debugging: prints contents of file-to-encoding map
203 static public void printFilenameMap(String heading) {
204 System.err.println("\n********************************************");
205 System.err.println(heading.toUpperCase());
206 Iterator entries = map.entrySet().iterator();
207 while(entries.hasNext()) {
208 Map.Entry entry = (Map.Entry)entries.next();
209 System.err.println("+ " + (String)entry.getKey() + ": " + (String)entry.getValue());
210 }
211 System.err.println("********************************************\n");
212 }
213
214 // UNUSED at present. Brute force version of the findFilenameEncoding() method
215 // Doesn't use the map, but gets *all* the metadata assigned to a file/folder to
216 // work out the encoding applicable to a file/folder.
217 public static String findFilenameEncodingBruteForce(File file, String urlEncodedFilename,
218 boolean bruteForceLookup)
219 {
220 System.err.println("\n***** BRUTE FORCE getFilenameEncoding() called\n");
221
222
223 String encoding = "";
224
225 // Check for filename encoding metadata *directly* associated with the file
226 // Now don't need to get any inherited encoding metadata here, because of
227 // the way we're storing and retrieving encoding information from the map.
228
229 ArrayList list = MetadataXMLFileManager.getMetadataAssignedToFile(file, true); // true: gets gs.filenameEncoding only
230 if(!list.isEmpty()) {
231 // try to get the filename encoding meta that was assigned last to this
232 // file, even though it makes no sense to have multiple values for it
233 MetadataValue metavalue = (MetadataValue)list.get(list.size()-1);
234 encoding = metavalue.getValue();
235
236 if(encoding == null) { // unlikely ???
237 System.err.println("**** ERROR: encoding for "
238 + urlEncodedFilename + " is NULL!");
239 encoding = "";
240 }
241 } // else no filename encoding set yet, perhaps
242 //System.err.println("**** Found encoding for " + urlEncodedFilename + " " + encoding);
243 return encoding;
244 }
245
246//****************************** APPLYING ENCODINGS TO FILENAMES *****************************
247
248 /** URL encoded version of the byte codes of the given file's name */
249 public static String calcURLEncodedFilePath(File file) {
250 if(!MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
251 return file.getAbsolutePath();
252 }
253 else {
254 String filename = fileToURLEncoding(file);
255 return filename;
256 }
257 }
258
259 /** URL encoded version of the byte codes of this file's name */
260 public static String calcURLEncodedFileName(String urlfilepath) {
261 String filename = urlfilepath;
262 if(filename.endsWith(URL_FILE_SEPARATOR)) { // directory, remove trailing slash
263 filename = filename.substring(0, filename.length() - 1);
264 }
265
266 // remove the directory prefix (if any) to get the filename
267 int index = filename.lastIndexOf(URL_FILE_SEPARATOR);
268 if(index != -1) {
269 filename = filename.substring(index+1); // skip separator
270 }
271
272 return filename;
273 }
274
275 /** Given a string representing an alias to an official encoding (and unofficial ones
276 * starting with "Latin-"), attempts to work out what the canonical encoding for that is.
277 * If the given encoding is unrecognised, it is returned as is. */
278 public static String canonicalEncodingName(String encoding) {
279 String canonicalEncoding = encoding;
280 try {
281 // Latin-1 -> ISO-8859-1
282 String alias = canonicalEncoding.toLowerCase();
283 if(alias.startsWith("latin")){
284 canonicalEncoding = "ISO-8859" + alias.substring("latin".length());
285 }
286
287 // canonical encoding for official aliases
288 canonicalEncoding = Charset.forName(canonicalEncoding).name();
289 return canonicalEncoding;
290 } catch (Exception e) {
291 System.err.println("(Could not recognise encoding (alias): "
292 + encoding + ".)");
293 return encoding; // no alias could be found, return the original parameter
294 }
295 }
296
297//************************* GETTING THE URL ENCODING OF FILENAMES *********************************
298
299 /**
300 * Given a String containing hexentities, will convert back into the unicode version of the String.
301 * e.g. A string like "02 Tēnā Koutou\.mp3" will be returned as "02 Tena Koutou\.mp3" with macrons on e and a
302 * I've tested this in a separate file that imports java.util.regex.Matcher and java.util.regex.Pattern
303 * and contains a copy of Utility.debugUnicodeString(String) with the following main function:
304 public static void main(String args[]) {
305 String str = "02 Tēnā Koutou\\.mp3"; // or more basic case: String str = "mmmmānnnnēpppp\\.txt";
306 System.err.println("About to decode hex string: " + str);
307 String result = decodeStringContainingHexEntities(str);
308 System.err.println("Decoded hex string: " + result + " - debug unicode form: " + debugUnicodeString(result));
309 }
310 */
311 public static String decodeStringContainingHexEntities(String str) {
312 String result = "";
313 boolean done = false;
314 Pattern hexPattern = Pattern.compile("(&#x[0-9a-zA-Z]{1,4}+;)");
315 Matcher matcher = hexPattern.matcher(str);
316
317 int searchFromIndex = 0;
318 int endMatchIndex = -1;
319
320 while(matcher.find(searchFromIndex)) {
321 String hexPart = matcher.group();
322 //System.err.println("Found hexpart match: " + hexPart);
323
324 int startMatchIndex = matcher.start();
325 endMatchIndex = matcher.end();
326 result += str.substring(searchFromIndex, startMatchIndex);
327
328 String hexNumberStr = hexPart.substring(3, hexPart.length()-1); // lose the "&#x" prefix and the ";" suffix to get just the hex number portion of the match
329 // https://stackoverflow.com/questions/16625865/java-unicode-to-hex-string
330 // https://stackoverflow.com/questions/11194513/convert-hex-string-to-int
331
332 //System.err.println("hexNumberStr so far: " + hexNumberStr);
333 int tmpDigit = Integer.parseInt(hexNumberStr);
334 //System.err.println("As digit: " + tmpDigit);
335 hexNumberStr = String.format("%04d", tmpDigit);
336 //System.err.println("2 hexNumberStr so far: " + hexNumberStr);
337 hexNumberStr = "0x" + hexNumberStr; // e.g "0xDDDD"
338 //int hexNumber = Integer.parseInt(hexNumberStr);
339 int hexNumber = Integer.decode(hexNumberStr);
340 String hexNumberAsChar = Character.toString((char) hexNumber);
341 result += hexNumberAsChar;
342
343 searchFromIndex = endMatchIndex;
344
345 }
346
347 if(endMatchIndex != -1) {
348 result += str.substring(endMatchIndex);
349 //System.err.println("suffix: " + str.substring(endMatchIndex));
350 }
351
352 return result;
353 }
354
355 /** Attempting to produce the equivalent method fileToURLEncoding() above, but taking a String as input parameter */
356 public static String fileNameToHex(String filename) {
357 /*String filename_url_encoded = "";
358 try {
359 URI filename_uri = new URI(filename);
360 String filename_ascii = filename_uri.toASCIIString();
361 String filename_raw_bytes = URLDecoder.decode(filename_ascii,"ISO-8859-1");
362 filename_url_encoded = iso_8859_1_filename_to_url_encoded(filename_raw_bytes);
363 return filename_url_encoded;
364 } catch (Exception e) {
365 e.printStackTrace();
366 // Give up trying to convert
367 filename_url_encoded = filename;
368 }
369 return filename_url_encoded;
370 */
371
372 String hexFilename = "";
373 for(int i = 0; i < filename.length(); i++) {
374 int charCode = filename.codePointAt(i); // unicode codepoint / ASCII code
375
376 // ASCII table: https://cdn.sparkfun.com/assets/home_page_posts/2/1/2/1/ascii_table_black.png
377 // If the unicode character code pt is less than the ASCII code for space and greater than for tilda, let's display the char in hex (x0000 format)
378 if((charCode >= 20 && charCode <= 126) || charCode == 9 || charCode == 10 || charCode == 13) { // space, tilda, TAB, LF, CR are printable, leave them in for XML element printing
379 hexFilename += filename.charAt(i);
380 } else {
381 hexFilename += "&#x" + String.format("%x", charCode).toUpperCase() + ";"; // looks like: "&#x[up-to-4-hexdigits-in-UPPERCASE];"
382 }
383 }
384
385 return hexFilename;
386 }
387
388 // Dr Bainbridge's methods
389 /* On Linux machines that are set to using an ISO-8859 (Latin) type encoding,
390 * we can work with URL-encoded filenames in Java. Java works with whatever
391 * encoding the filesystem uses. Unlike systems working with UTF-8, where Java
392 * interprets filenames as UTF-8 (a destructive process since characters invalid
393 * for UTF-8 are replaced with the invalid character, which means the original
394 * character's byte codes can not be regained), working with an ISO-8859-1
395 * system means the original byte codes of the characters are preserved,
396 * regardless of whether the characters represent ISO-8859-1 or not. Such byte
397 * codes are converted by the following method to the correct URL versions of
398 * the strings that the filenames represent (that is, the correct URL representations
399 * of the filenames in their original encodings). This is useful for interactions with
400 * Perl as Java and Perl can use URL-encoded filenames to talk about the same files
401 * on the file system, instead of having to work out what encoding they are in. */
402
403 public static String fileToURLEncoding(File file) {
404 if(!MULTIPLE_FILENAME_ENCODINGS_SUPPORTED) {
405 return file.getAbsolutePath();
406 }
407
408 String filename_url_encoded = "";
409
410 // The following test for whether the file exists or not is a problem
411 // when a File object--whose actual file is in the process of being moved
412 // and therefore temporarily does not 'exist' on the actual system--can't
413 // be URL encoded: the following would return "" when a file doesn't exist.
414 // So commenting out the test.
415 /*
416 if(!file.getName().equals("recycle")) {
417 if(!file.isFile() && !file.isDirectory()) {
418 System.err.println("*** ERROR. Java can't see file: " + file.getAbsolutePath());
419 return "";
420 }
421
422 if(!file.exists()) {
423 System.err.println("*** NOTE: File doesn't exist: " + file.getAbsolutePath());
424 return ""; //file.getName();
425 }
426 }
427 */
428
429 URI filename_uri = file.toURI();
430 try {
431 // The trick:
432 // 1. toASCIIString() will %xx encode values > 127
433 // 2. Decode the result to "ISO-8859-1"
434 // 3. URL encode the bytes to string
435
436 // Step 2 forces the string to be 8-bit values. It
437 // doesn't matter if the starting raw filename was *not*
438 // in the ISO-8859-1 encoding, the effect is to ensure
439 // we have an 8-bit byte string that (numerically)
440 // captures the right value. These numerical values are
441 // then used to determine how to URL encode it
442
443 String filename_ascii = filename_uri.toASCIIString();
444 String filename_raw_bytes = URLDecoder.decode(filename_ascii,"ISO-8859-1");
445 filename_url_encoded = iso_8859_1_filename_to_url_encoded(filename_raw_bytes);
446
447 }
448 catch (Exception e) {
449 e.printStackTrace();
450 // Give up trying to convert
451 filename_url_encoded = file.getAbsolutePath();
452 }
453 return filename_url_encoded;
454 }
455
456 // For unicode codepoints see:
457 // http://unicode.org/Public/MAPPINGS/ISO8859/8859-1.TXT for ISO8859-1 (Latin-1)
458 // where 0xE2 maps to codepoint 0x00E2 and is defined as "Latin small letter a with circumflex"
459 // http://unicode.org/Public/MAPPINGS/ISO8859/8859-7.TXT for ISO8859-7 (Greek)
460 // where 0xE2 maps to codepoint 0x03B2 and is defined as "Greek small letter beta"
461 public static String iso_8859_1_filename_to_url_encoded(String raw_bytes_filename)
462 throws Exception
463 {
464 String urlEncoded = "";
465
466 try {
467 // By this point we have a UTF-8 encoded string that captures
468 // what the ISO-8859-1 (Latin-1) character is that corresponded to the
469 // 8-bit numeric value for that character in the filename
470 // on the file system
471
472 // For example:
473 // File system char: <lower-case beta char in Latin-7> = %E2
474 // Equivalent Latin 1 char: <lower-case a with circumflex> = %E2
475 // Mapped to UTF-8: <lower-case a with circumflex> = <C3><A2>
476
477 // Our task is to take the string the contains <C3><A2> and ensure that
478 // we "see" it as <E2>
479
480 byte [] raw_bytes = raw_bytes_filename.getBytes("ISO-8859-1");
481 String unicode_filename = new String(raw_bytes,"UTF-8");
482
483 for(int i = 0; i < unicode_filename.length(); i++) {
484 char charVal = unicode_filename.charAt(i);
485 if ((int)charVal > 255) {
486 urlEncoded += String.format("&#x%02X;", (int)charVal);
487 }
488 else if((int)charVal > 127) {
489 urlEncoded += String.format("%%%02X", (int)charVal);
490 } else {
491 urlEncoded += String.format("%c", (char)charVal);
492 }
493 }
494 }
495 catch (Exception e) {
496 //e.printStackTrace();
497 throw(e);
498 }
499
500 return urlEncoded;
501 }
502
503 // unused for now
504 public static String raw_filename_to_url_encoded(String fileName)
505 throws Exception
506 {
507 String urlEncoded = "";
508 try {
509 byte[] bytes = fileName.getBytes();
510
511 for(int i = 0; i < bytes.length; i++) {
512 // mask each byte (by applying & 0xFF) to make the signed
513 // byte (in the range -128 to 127) unsigned (in the range
514 // 0 to 255).
515
516 int byteVal = (int)(bytes[i] & 0xFF);
517
518 if(byteVal > 127) {
519 urlEncoded += String.format("%%%02X", (int)byteVal);
520 } else {
521 urlEncoded += String.format("%c",(char)byteVal);
522 }
523 }
524 }
525 catch (Exception e) {
526 //e.printStackTrace();
527 throw(e);
528 }
529
530 return urlEncoded;
531 }
532
533}
Note: See TracBrowser for help on using the repository browser.