15
15
import javax .xml .validation .Validator ;
16
16
import java .io .IOException ;
17
17
import java .io .StringReader ;
18
- import java .nio .charset .StandardCharsets ;
19
18
import java .nio .file .Files ;
20
19
import java .nio .file .Path ;
21
- import java .nio .file .StandardCopyOption ;
22
- import java .nio .file .StandardOpenOption ;
23
20
import java .util .List ;
24
21
import java .util .UUID ;
25
- import java .util .concurrent .locks .Lock ;
26
- import java .util .concurrent .locks .ReentrantLock ;
27
22
28
23
/**
29
24
* Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
32
27
@ CheckAvailability
33
28
@ OperatingSystem (OperatingSystem .Value .LINUX )
34
29
@ Priority (90 )
35
- public class DolphinPlaces implements QuickAccessService {
30
+ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
36
31
37
- private static final int MAX_FILE_SIZE = 1 << 15 ; //xml is quite verbose
32
+ private static final int MAX_FILE_SIZE = 1 << 20 ; //1MiB, xml is quite verbose
38
33
private static final Path PLACES_FILE = Path .of (System .getProperty ("user.home" ), ".local/share/user-places.xbel" );
39
- private static final Path TMP_FILE = Path .of (System .getProperty ("java.io.tmpdir" ), "user-places.xbel.cryptomator.tmp" );
40
- private static final Lock MODIFY_LOCK = new ReentrantLock ();
41
34
private static final String ENTRY_TEMPLATE = """
42
35
<bookmark href=\" %s\" >
43
36
<title>%s</title>
@@ -51,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService {
51
44
</info>
52
45
</bookmark>""" ;
53
46
54
-
55
47
private static final Validator XML_VALIDATOR ;
56
48
57
49
static {
@@ -64,96 +56,82 @@ public class DolphinPlaces implements QuickAccessService {
64
56
}
65
57
}
66
58
59
+ //SPI constructor
60
+ public DolphinPlaces () {
61
+ super (PLACES_FILE , MAX_FILE_SIZE );
62
+ }
67
63
68
64
@ Override
69
- public QuickAccessService .QuickAccessEntry add (Path target , String displayName ) throws QuickAccessServiceException {
70
- String id = UUID .randomUUID ().toString ();
65
+ EntryAndConfig addEntryToConfig (String config , Path target , String displayName ) throws QuickAccessServiceException {
71
66
try {
72
- MODIFY_LOCK .lock ();
73
- if (Files .size (PLACES_FILE ) > MAX_FILE_SIZE ) {
74
- throw new IOException ("File %s exceeds size of %d bytes" .formatted (PLACES_FILE , MAX_FILE_SIZE ));
75
- }
76
- var placesContent = Files .readString (PLACES_FILE );
67
+ String id = UUID .randomUUID ().toString ();
77
68
//validate
78
- XML_VALIDATOR .validate (new StreamSource (new StringReader (placesContent )));
69
+ XML_VALIDATOR .validate (new StreamSource (new StringReader (config )));
79
70
// modify
80
- int insertIndex = placesContent .lastIndexOf ("</xbel" ); //cannot be -1 due to validation; we do not match the end tag, since betweent tag name and closing bracket can be whitespaces
81
- try (var writer = Files .newBufferedWriter (TMP_FILE , StandardCharsets .UTF_8 , StandardOpenOption .WRITE , StandardOpenOption .CREATE , StandardOpenOption .TRUNCATE_EXISTING )) {
82
- writer .write (placesContent , 0 , insertIndex );
83
- writer .newLine ();
84
- writer .write (ENTRY_TEMPLATE .formatted (target .toUri (), displayName , id ).indent (1 ));
85
- writer .newLine ();
86
- writer .write (placesContent , insertIndex , placesContent .length () - insertIndex );
87
- }
88
- // save
89
- Files .move (TMP_FILE , PLACES_FILE , StandardCopyOption .REPLACE_EXISTING , StandardCopyOption .ATOMIC_MOVE );
90
- return new DolphinPlacesEntry (id );
71
+ int insertIndex = config .lastIndexOf ("</xbel" ); //cannot be -1 due to validation; we do not match the whole end tag, since between tag name and closing bracket can be whitespaces
72
+ var adjustedConfig = config .substring (0 , insertIndex ) //
73
+ + "\n " //
74
+ + ENTRY_TEMPLATE .formatted (target .toUri (), escapeXML (displayName ), id ).indent (1 ) //
75
+ + "\n " //
76
+ + config .substring (insertIndex );
77
+ return new EntryAndConfig (new DolphinPlacesEntry (id ), adjustedConfig );
91
78
} catch (SAXException | IOException e ) {
92
79
throw new QuickAccessServiceException ("Adding entry to KDE places file failed." , e );
93
- } finally {
94
- MODIFY_LOCK .unlock ();
95
80
}
96
81
}
97
82
98
- private static class DolphinPlacesEntry implements QuickAccessEntry {
83
+ private String escapeXML (String s ) {
84
+ return s .replace ("&" ,"&" ) //
85
+ .replace ("<" ,"<" ) //
86
+ .replace (">" ,">" );
87
+ }
88
+
89
+ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
99
90
100
91
private final String id ;
101
- private volatile boolean isRemoved = false ;
102
92
103
93
DolphinPlacesEntry (String id ) {
104
94
this .id = id ;
105
95
}
106
96
107
97
@ Override
108
- public void remove ( ) throws QuickAccessServiceException {
98
+ public String removeEntryFromConfig ( String config ) throws QuickAccessServiceException {
109
99
try {
110
- MODIFY_LOCK .lock ();
111
- if (isRemoved ) {
112
- return ;
113
- }
114
- if (Files .size (PLACES_FILE ) > MAX_FILE_SIZE ) {
115
- throw new IOException ("File %s exceeds size of %d bytes" .formatted (PLACES_FILE , MAX_FILE_SIZE ));
116
- }
117
- var placesContent = Files .readString (PLACES_FILE );
118
- int idIndex = placesContent .lastIndexOf (id );
100
+ int idIndex = config .lastIndexOf (id );
119
101
if (idIndex == -1 ) {
120
- isRemoved = true ;
121
- return ; //we assume someone has removed our entry
102
+ return config ; //assume someone has removed our entry, nothing to do
122
103
}
123
104
//validate
124
- XML_VALIDATOR .validate (new StreamSource (new StringReader (placesContent )));
105
+ XML_VALIDATOR .validate (new StreamSource (new StringReader (config )));
125
106
//modify
126
- int openingTagIndex = indexOfEntryOpeningTag (placesContent , idIndex );
127
- var contentToWrite1 = placesContent .substring (0 , openingTagIndex ).stripTrailing ();
107
+ int openingTagIndex = indexOfEntryOpeningTag (config , idIndex );
108
+ var contentToWrite1 = config .substring (0 , openingTagIndex ).stripTrailing ();
128
109
129
- int closingTagEndIndex = placesContent .indexOf ('>' , placesContent .indexOf ("</bookmark" , idIndex ));
130
- var part2Tmp = placesContent .substring (closingTagEndIndex + 1 ).split ("\\ A\\ v+" , 2 ); //removing leading vertical whitespaces, but no indentation
110
+ int closingTagEndIndex = config .indexOf ('>' , config .indexOf ("</bookmark" , idIndex ));
111
+ var part2Tmp = config .substring (closingTagEndIndex + 1 ).split ("\\ A\\ v+" , 2 ); //removing leading vertical whitespaces, but no indentation
131
112
var contentToWrite2 = part2Tmp [part2Tmp .length - 1 ];
132
113
133
- try (var writer = Files .newBufferedWriter (TMP_FILE , StandardCharsets .UTF_8 , StandardOpenOption .WRITE , StandardOpenOption .CREATE , StandardOpenOption .TRUNCATE_EXISTING )) {
134
- writer .write (contentToWrite1 );
135
- writer .newLine ();
136
- writer .write (contentToWrite2 );
137
- }
138
- // save
139
- Files .move (TMP_FILE , PLACES_FILE , StandardCopyOption .REPLACE_EXISTING , StandardCopyOption .ATOMIC_MOVE );
140
- isRemoved = true ;
141
- } catch (IOException | SAXException e ) {
114
+ return contentToWrite1 + "\n " + contentToWrite2 ;
115
+ } catch (IOException | SAXException | IllegalStateException e ) {
142
116
throw new QuickAccessServiceException ("Removing entry from KDE places file failed." , e );
143
- } finally {
144
- MODIFY_LOCK .unlock ();
145
117
}
146
118
}
147
119
120
+ /**
121
+ * Returns the start index (inclusive) of the {@link DolphinPlaces#ENTRY_TEMPLATE} entry
122
+ * @param placesContent the content of the XBEL places file
123
+ * @param idIndex start index (inclusive) of the entrys id tag value
124
+ * @return start index of the first bookmark tag, searching backwards from idIndex
125
+ */
148
126
private int indexOfEntryOpeningTag (String placesContent , int idIndex ) {
149
127
var xmlWhitespaceChars = List .of (' ' , '\t' , '\n' );
150
128
for (char c : xmlWhitespaceChars ) {
151
- int idx = placesContent .lastIndexOf ("<bookmark" + c , idIndex );
129
+ int idx = placesContent .lastIndexOf ("<bookmark" + c , idIndex ); //with the whitespace we ensure, that no tags starting with "bookmark" (e.g. bookmarkz) are selected
152
130
if (idx != -1 ) {
153
131
return idx ;
154
132
}
155
133
}
156
- throw new IllegalStateException ("File " + PLACES_FILE + " is valid xbel file , but does not contain opening bookmark tag." );
134
+ throw new IllegalStateException ("Found entry id " + id + " in " + PLACES_FILE + " , but it is not a child of < bookmark> tag." );
157
135
}
158
136
}
159
137
0 commit comments