1515import javax .xml .validation .Validator ;
1616import java .io .IOException ;
1717import java .io .StringReader ;
18- import java .nio .charset .StandardCharsets ;
1918import java .nio .file .Files ;
2019import java .nio .file .Path ;
21- import java .nio .file .StandardCopyOption ;
22- import java .nio .file .StandardOpenOption ;
2320import java .util .List ;
2421import java .util .UUID ;
25- import java .util .concurrent .locks .Lock ;
26- import java .util .concurrent .locks .ReentrantLock ;
2722
2823/**
2924 * Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
3227@ CheckAvailability
3328@ OperatingSystem (OperatingSystem .Value .LINUX )
3429@ Priority (90 )
35- public class DolphinPlaces implements QuickAccessService {
30+ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
3631
37- private static final int MAX_FILE_SIZE = 1 << 20 ; //xml is quite verbose
32+ private static final int MAX_FILE_SIZE = 1 << 20 ; //1MiB, xml is quite verbose
3833 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 ();
4134 private static final String ENTRY_TEMPLATE = """
4235 <bookmark href=\" %s\" >
4336 <title>%s</title>
@@ -51,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService {
5144 </info>
5245 </bookmark>""" ;
5346
54-
5547 private static final Validator XML_VALIDATOR ;
5648
5749 static {
@@ -64,96 +56,82 @@ public class DolphinPlaces implements QuickAccessService {
6456 }
6557 }
6658
59+ //SPI constructor
60+ public DolphinPlaces () {
61+ super (PLACES_FILE , MAX_FILE_SIZE );
62+ }
6763
6864 @ 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 {
7166 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 ();
7768 //validate
78- XML_VALIDATOR .validate (new StreamSource (new StringReader (placesContent )));
69+ XML_VALIDATOR .validate (new StreamSource (new StringReader (config )));
7970 // 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 );
9178 } catch (SAXException | IOException e ) {
9279 throw new QuickAccessServiceException ("Adding entry to KDE places file failed." , e );
93- } finally {
94- MODIFY_LOCK .unlock ();
9580 }
9681 }
9782
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 {
9990
10091 private final String id ;
101- private volatile boolean isRemoved = false ;
10292
10393 DolphinPlacesEntry (String id ) {
10494 this .id = id ;
10595 }
10696
10797 @ Override
108- public void remove ( ) throws QuickAccessServiceException {
98+ public String removeEntryFromConfig ( String config ) throws QuickAccessServiceException {
10999 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 );
119101 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
122103 }
123104 //validate
124- XML_VALIDATOR .validate (new StreamSource (new StringReader (placesContent )));
105+ XML_VALIDATOR .validate (new StreamSource (new StringReader (config )));
125106 //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 ();
128109
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
131112 var contentToWrite2 = part2Tmp [part2Tmp .length - 1 ];
132113
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 ) {
142116 throw new QuickAccessServiceException ("Removing entry from KDE places file failed." , e );
143- } finally {
144- MODIFY_LOCK .unlock ();
145117 }
146118 }
147119
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+ */
148126 private int indexOfEntryOpeningTag (String placesContent , int idIndex ) {
149127 var xmlWhitespaceChars = List .of (' ' , '\t' , '\n' );
150128 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
152130 if (idx != -1 ) {
153131 return idx ;
154132 }
155133 }
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." );
157135 }
158136 }
159137
0 commit comments