lemval
2012-01-31 1c30dad2115fc513791d8a5b292ad0f7d7b85749
commit | author | age
93f472 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.text.MessageFormat;
21 import java.util.ArrayList;
22 import java.util.Arrays;
d7905a 23 import java.util.Collections;
93f472 24 import java.util.HashSet;
JM 25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29
30 import org.eclipse.jgit.lib.StoredConfig;
31 import org.eclipse.jgit.storage.file.FileBasedConfig;
32 import org.eclipse.jgit.util.FS;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
fe24a0 36 import com.gitblit.models.TeamModel;
93f472 37 import com.gitblit.models.UserModel;
0db5c4 38 import com.gitblit.utils.ArrayUtils;
fe24a0 39 import com.gitblit.utils.DeepCopier;
93f472 40 import com.gitblit.utils.StringUtils;
JM 41
42 /**
43  * ConfigUserService is Gitblit's default user service implementation since
44  * version 0.8.0.
45  * 
46  * Users and their repository memberships are stored in a git-style config file
47  * which is cached and dynamically reloaded when modified. This file is
48  * plain-text, human-readable, and may be edited with a text editor.
49  * 
50  * Additionally, this format allows for expansion of the user model without
51  * bringing in the complexity of a database.
52  * 
53  * @author James Moger
54  * 
55  */
56 public class ConfigUserService implements IUserService {
57
fe24a0 58     private static final String TEAM = "team";
JM 59
60     private static final String USER = "user";
61
62     private static final String PASSWORD = "password";
63
64     private static final String REPOSITORY = "repository";
65
66     private static final String ROLE = "role";
d7905a 67
0b9119 68     private static final String MAILINGLIST = "mailingList";
d7905a 69
JM 70     private static final String PRERECEIVE = "preReceiveScript";
71
72     private static final String POSTRECEIVE = "postReceiveScript";
fe24a0 73
93f472 74     private final File realmFile;
JM 75
76     private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
77
78     private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>();
79
80     private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
81
fe24a0 82     private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
93f472 83
JM 84     private volatile long lastModified;
85
86     public ConfigUserService(File realmFile) {
87         this.realmFile = realmFile;
88     }
89
90     /**
91      * Setup the user service.
92      * 
93      * @param settings
fe24a0 94      * @since 0.7.0
93f472 95      */
JM 96     @Override
97     public void setup(IStoredSettings settings) {
98     }
99
100     /**
101      * Does the user service support cookie authentication?
102      * 
103      * @return true or false
104      */
105     @Override
106     public boolean supportsCookies() {
107         return true;
108     }
109
110     /**
111      * Returns the cookie value for the specified user.
112      * 
113      * @param model
114      * @return cookie value
115      */
116     @Override
117     public char[] getCookie(UserModel model) {
118         read();
119         UserModel storedModel = users.get(model.username.toLowerCase());
120         String cookie = StringUtils.getSHA1(model.username + storedModel.password);
121         return cookie.toCharArray();
122     }
123
124     /**
125      * Authenticate a user based on their cookie.
126      * 
127      * @param cookie
128      * @return a user object or null
129      */
130     @Override
131     public UserModel authenticate(char[] cookie) {
132         String hash = new String(cookie);
133         if (StringUtils.isEmpty(hash)) {
134             return null;
135         }
136         read();
137         UserModel model = null;
138         if (cookies.containsKey(hash)) {
139             model = cookies.get(hash);
140         }
141         return model;
142     }
143
144     /**
145      * Authenticate a user based on a username and password.
146      * 
147      * @param username
148      * @param password
149      * @return a user object or null
150      */
151     @Override
152     public UserModel authenticate(String username, char[] password) {
153         read();
154         UserModel returnedUser = null;
155         UserModel user = getUserModel(username);
156         if (user == null) {
157             return null;
158         }
159         if (user.password.startsWith(StringUtils.MD5_TYPE)) {
160             // password digest
161             String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
162             if (user.password.equalsIgnoreCase(md5)) {
163                 returnedUser = user;
164             }
165         } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
166             // username+password digest
167             String md5 = StringUtils.COMBINED_MD5_TYPE
168                     + StringUtils.getMD5(username.toLowerCase() + new String(password));
169             if (user.password.equalsIgnoreCase(md5)) {
170                 returnedUser = user;
171             }
172         } else if (user.password.equals(new String(password))) {
173             // plain-text password
174             returnedUser = user;
175         }
176         return returnedUser;
177     }
178
179     /**
180      * Retrieve the user object for the specified username.
181      * 
182      * @param username
183      * @return a user object or null
184      */
185     @Override
186     public UserModel getUserModel(String username) {
187         read();
188         UserModel model = users.get(username.toLowerCase());
fe24a0 189         if (model != null) {
JM 190             // clone the model, otherwise all changes to this object are
191             // live and unpersisted
192             model = DeepCopier.copy(model);
193         }
93f472 194         return model;
JM 195     }
196
197     /**
198      * Updates/writes a complete user object.
199      * 
200      * @param model
201      * @return true if update is successful
202      */
203     @Override
204     public boolean updateUserModel(UserModel model) {
205         return updateUserModel(model.username, model);
206     }
207
208     /**
209      * Updates/writes and replaces a complete user object keyed by username.
210      * This method allows for renaming a user.
211      * 
212      * @param username
213      *            the old username
214      * @param model
215      *            the user object to use for username
216      * @return true if update is successful
217      */
218     @Override
219     public boolean updateUserModel(String username, UserModel model) {
220         try {
221             read();
fe24a0 222             UserModel oldUser = users.remove(username.toLowerCase());
93f472 223             users.put(model.username.toLowerCase(), model);
fe24a0 224             // null check on "final" teams because JSON-sourced UserModel
JM 225             // can have a null teams object
226             if (model.teams != null) {
227                 for (TeamModel team : model.teams) {
228                     TeamModel t = teams.get(team.name.toLowerCase());
229                     if (t == null) {
230                         // new team
231                         team.addUser(username);
232                         teams.put(team.name.toLowerCase(), team);
233                     } else {
234                         // do not clobber existing team definition
235                         // maybe because this is a federated user
236                         t.removeUser(username);
237                         t.addUser(model.username);
238                     }
239                 }
240
241                 // check for implicit team removal
242                 if (oldUser != null) {
243                     for (TeamModel team : oldUser.teams) {
244                         if (!model.isTeamMember(team.name)) {
245                             team.removeUser(username);
246                         }
247                     }
248                 }
249             }
93f472 250             write();
JM 251             return true;
252         } catch (Throwable t) {
253             logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
254                     t);
255         }
256         return false;
257     }
258
259     /**
260      * Deletes the user object from the user service.
261      * 
262      * @param model
263      * @return true if successful
264      */
265     @Override
266     public boolean deleteUserModel(UserModel model) {
267         return deleteUser(model.username);
268     }
269
270     /**
271      * Delete the user object with the specified username
272      * 
273      * @param username
274      * @return true if successful
275      */
276     @Override
277     public boolean deleteUser(String username) {
278         try {
279             // Read realm file
280             read();
fe24a0 281             UserModel model = users.remove(username.toLowerCase());
JM 282             // remove user from team
283             for (TeamModel team : model.teams) {
284                 TeamModel t = teams.get(team.name);
285                 if (t == null) {
286                     // new team
287                     team.removeUser(username);
288                     teams.put(team.name.toLowerCase(), team);
289                 } else {
290                     // existing team
291                     t.removeUser(username);
292                 }
293             }
93f472 294             write();
JM 295             return true;
296         } catch (Throwable t) {
297             logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
fe24a0 298         }
JM 299         return false;
300     }
301
302     /**
303      * Returns the list of all teams available to the login service.
304      * 
305      * @return list of all teams
306      * @since 0.8.0
307      */
308     @Override
309     public List<String> getAllTeamNames() {
310         read();
311         List<String> list = new ArrayList<String>(teams.keySet());
d7905a 312         Collections.sort(list);
fe24a0 313         return list;
JM 314     }
0b9119 315
fe24a0 316     /**
abeaaf 317      * Returns the list of all teams available to the login service.
JM 318      * 
319      * @return list of all teams
320      * @since 0.8.0
321      */
322     @Override
323     public List<TeamModel> getAllTeams() {
324         read();
325         List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
326         list = DeepCopier.copy(list);
327         Collections.sort(list);
328         return list;
329     }
330
331     /**
fe24a0 332      * Returns the list of all users who are allowed to bypass the access
JM 333      * restriction placed on the specified repository.
334      * 
335      * @param role
336      *            the repository name
337      * @return list of all usernames that can bypass the access restriction
338      */
339     @Override
340     public List<String> getTeamnamesForRepositoryRole(String role) {
341         List<String> list = new ArrayList<String>();
342         try {
343             read();
344             for (Map.Entry<String, TeamModel> entry : teams.entrySet()) {
345                 TeamModel model = entry.getValue();
346                 if (model.hasRepository(role)) {
347                     list.add(model.name);
348                 }
349             }
350         } catch (Throwable t) {
351             logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
352         }
d7905a 353         Collections.sort(list);
fe24a0 354         return list;
JM 355     }
356
357     /**
358      * Sets the list of all teams who are allowed to bypass the access
359      * restriction placed on the specified repository.
360      * 
361      * @param role
362      *            the repository name
363      * @param teamnames
364      * @return true if successful
365      */
366     @Override
367     public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
368         try {
369             Set<String> specifiedTeams = new HashSet<String>();
370             for (String teamname : teamnames) {
371                 specifiedTeams.add(teamname.toLowerCase());
372             }
373
374             read();
375
376             // identify teams which require add or remove role
377             for (TeamModel team : teams.values()) {
378                 // team has role, check against revised team list
379                 if (specifiedTeams.contains(team.name.toLowerCase())) {
380                     team.addRepository(role);
381                 } else {
382                     // remove role from team
383                     team.removeRepository(role);
384                 }
385             }
386
387             // persist changes
388             write();
389             return true;
390         } catch (Throwable t) {
391             logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t);
392         }
393         return false;
394     }
395
396     /**
397      * Retrieve the team object for the specified team name.
398      * 
399      * @param teamname
400      * @return a team object or null
401      * @since 0.8.0
402      */
403     @Override
404     public TeamModel getTeamModel(String teamname) {
405         read();
406         TeamModel model = teams.get(teamname.toLowerCase());
407         if (model != null) {
408             // clone the model, otherwise all changes to this object are
409             // live and unpersisted
410             model = DeepCopier.copy(model);
411         }
412         return model;
413     }
414
415     /**
416      * Updates/writes a complete team object.
417      * 
418      * @param model
419      * @return true if update is successful
420      * @since 0.8.0
421      */
422     @Override
423     public boolean updateTeamModel(TeamModel model) {
424         return updateTeamModel(model.name, model);
425     }
426
427     /**
428      * Updates/writes and replaces a complete team object keyed by teamname.
429      * This method allows for renaming a team.
430      * 
431      * @param teamname
432      *            the old teamname
433      * @param model
434      *            the team object to use for teamname
435      * @return true if update is successful
436      * @since 0.8.0
437      */
438     @Override
439     public boolean updateTeamModel(String teamname, TeamModel model) {
440         try {
441             read();
442             teams.remove(teamname.toLowerCase());
443             teams.put(model.name.toLowerCase(), model);
444             write();
445             return true;
446         } catch (Throwable t) {
447             logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
448         }
449         return false;
450     }
451
452     /**
453      * Deletes the team object from the user service.
454      * 
455      * @param model
456      * @return true if successful
457      * @since 0.8.0
458      */
459     @Override
460     public boolean deleteTeamModel(TeamModel model) {
461         return deleteTeam(model.name);
462     }
463
464     /**
465      * Delete the team object with the specified teamname
466      * 
467      * @param teamname
468      * @return true if successful
469      * @since 0.8.0
470      */
471     @Override
472     public boolean deleteTeam(String teamname) {
473         try {
474             // Read realm file
475             read();
476             teams.remove(teamname.toLowerCase());
477             write();
478             return true;
479         } catch (Throwable t) {
480             logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
93f472 481         }
JM 482         return false;
483     }
484
485     /**
486      * Returns the list of all users available to the login service.
487      * 
488      * @return list of all usernames
489      */
490     @Override
491     public List<String> getAllUsernames() {
492         read();
493         List<String> list = new ArrayList<String>(users.keySet());
d7905a 494         Collections.sort(list);
93f472 495         return list;
JM 496     }
abeaaf 497     
JM 498     /**
499      * Returns the list of all users available to the login service.
500      * 
501      * @return list of all usernames
502      */
503     @Override
504     public List<UserModel> getAllUsers() {
505         read();
506         List<UserModel> list = new ArrayList<UserModel>(users.values());
507         list = DeepCopier.copy(list);
508         Collections.sort(list);
509         return list;
510     }    
93f472 511
JM 512     /**
513      * Returns the list of all users who are allowed to bypass the access
514      * restriction placed on the specified repository.
515      * 
516      * @param role
517      *            the repository name
518      * @return list of all usernames that can bypass the access restriction
519      */
520     @Override
521     public List<String> getUsernamesForRepositoryRole(String role) {
522         List<String> list = new ArrayList<String>();
523         try {
524             read();
525             for (Map.Entry<String, UserModel> entry : users.entrySet()) {
526                 UserModel model = entry.getValue();
527                 if (model.hasRepository(role)) {
528                     list.add(model.username);
529                 }
530             }
531         } catch (Throwable t) {
532             logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
533         }
d7905a 534         Collections.sort(list);
93f472 535         return list;
JM 536     }
537
538     /**
539      * Sets the list of all uses who are allowed to bypass the access
540      * restriction placed on the specified repository.
541      * 
542      * @param role
543      *            the repository name
544      * @param usernames
545      * @return true if successful
546      */
547     @Override
548     public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
549         try {
550             Set<String> specifiedUsers = new HashSet<String>();
551             for (String username : usernames) {
552                 specifiedUsers.add(username.toLowerCase());
553             }
554
555             read();
556
557             // identify users which require add or remove role
558             for (UserModel user : users.values()) {
559                 // user has role, check against revised user list
560                 if (specifiedUsers.contains(user.username.toLowerCase())) {
561                     user.addRepository(role);
562                 } else {
563                     // remove role from user
564                     user.removeRepository(role);
565                 }
566             }
567
568             // persist changes
569             write();
570             return true;
571         } catch (Throwable t) {
572             logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
573         }
574         return false;
575     }
576
577     /**
578      * Renames a repository role.
579      * 
580      * @param oldRole
581      * @param newRole
582      * @return true if successful
583      */
584     @Override
585     public boolean renameRepositoryRole(String oldRole, String newRole) {
586         try {
587             read();
588             // identify users which require role rename
589             for (UserModel model : users.values()) {
590                 if (model.hasRepository(oldRole)) {
591                     model.removeRepository(oldRole);
592                     model.addRepository(newRole);
593                 }
594             }
595
fe24a0 596             // identify teams which require role rename
JM 597             for (TeamModel model : teams.values()) {
598                 if (model.hasRepository(oldRole)) {
599                     model.removeRepository(oldRole);
600                     model.addRepository(newRole);
601                 }
602             }
93f472 603             // persist changes
JM 604             write();
605             return true;
606         } catch (Throwable t) {
607             logger.error(
608                     MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
609         }
610         return false;
611     }
612
613     /**
614      * Removes a repository role from all users.
615      * 
616      * @param role
617      * @return true if successful
618      */
619     @Override
620     public boolean deleteRepositoryRole(String role) {
621         try {
622             read();
623
624             // identify users which require role rename
625             for (UserModel user : users.values()) {
626                 user.removeRepository(role);
627             }
628
fe24a0 629             // identify teams which require role rename
JM 630             for (TeamModel team : teams.values()) {
631                 team.removeRepository(role);
632             }
633
93f472 634             // persist changes
JM 635             write();
636             return true;
637         } catch (Throwable t) {
638             logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
639         }
640         return false;
641     }
642
643     /**
644      * Writes the properties file.
645      * 
646      * @param properties
647      * @throws IOException
648      */
649     private synchronized void write() throws IOException {
650         // Write a temporary copy of the users file
651         File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
652
653         StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
fe24a0 654
JM 655         // write users
93f472 656         for (UserModel model : users.values()) {
fe24a0 657             config.setString(USER, model.username, PASSWORD, model.password);
93f472 658
JM 659             // user roles
660             List<String> roles = new ArrayList<String>();
661             if (model.canAdmin) {
662                 roles.add(Constants.ADMIN_ROLE);
663             }
664             if (model.excludeFromFederation) {
665                 roles.add(Constants.NOT_FEDERATED_ROLE);
666             }
fe24a0 667             config.setStringList(USER, model.username, ROLE, roles);
93f472 668
JM 669             // repository memberships
fe24a0 670             // null check on "final" repositories because JSON-sourced UserModel
JM 671             // can have a null repositories object
0db5c4 672             if (!ArrayUtils.isEmpty(model.repositories)) {
fe24a0 673                 config.setStringList(USER, model.username, REPOSITORY, new ArrayList<String>(
JM 674                         model.repositories));
675             }
93f472 676         }
fe24a0 677
JM 678         // write teams
679         for (TeamModel model : teams.values()) {
680             // null check on "final" repositories because JSON-sourced TeamModel
681             // can have a null repositories object
0db5c4 682             if (!ArrayUtils.isEmpty(model.repositories)) {
fe24a0 683                 config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
JM 684                         model.repositories));
685             }
686
687             // null check on "final" users because JSON-sourced TeamModel
688             // can have a null users object
0db5c4 689             if (!ArrayUtils.isEmpty(model.users)) {
fe24a0 690                 config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
JM 691             }
0b9119 692
JM 693             // null check on "final" mailing lists because JSON-sourced
d7905a 694             // TeamModel can have a null users object
0db5c4 695             if (!ArrayUtils.isEmpty(model.mailingLists)) {
0b9119 696                 config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>(
JM 697                         model.mailingLists));
d7905a 698             }
JM 699
700             // null check on "final" preReceiveScripts because JSON-sourced
701             // TeamModel can have a null preReceiveScripts object
0db5c4 702             if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
d7905a 703                 config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts);
JM 704             }
705
706             // null check on "final" postReceiveScripts because JSON-sourced
707             // TeamModel can have a null postReceiveScripts object
0db5c4 708             if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
d7905a 709                 config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts);
0b9119 710             }
fe24a0 711         }
JM 712
93f472 713         config.save();
JM 714
715         // If the write is successful, delete the current file and rename
716         // the temporary copy to the original filename.
717         if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
718             if (realmFile.exists()) {
719                 if (!realmFile.delete()) {
720                     throw new IOException(MessageFormat.format("Failed to delete {0}!",
721                             realmFile.getAbsolutePath()));
722                 }
723             }
724             if (!realmFileCopy.renameTo(realmFile)) {
725                 throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
726                         realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath()));
727             }
728         } else {
729             throw new IOException(MessageFormat.format("Failed to save {0}!",
730                     realmFileCopy.getAbsolutePath()));
731         }
732     }
733
734     /**
735      * Reads the realm file and rebuilds the in-memory lookup tables.
736      */
737     protected synchronized void read() {
738         if (realmFile.exists() && (realmFile.lastModified() > lastModified)) {
739             lastModified = realmFile.lastModified();
740             users.clear();
741             cookies.clear();
fe24a0 742             teams.clear();
JM 743
93f472 744             try {
JM 745                 StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
746                 config.load();
fe24a0 747                 Set<String> usernames = config.getSubsections(USER);
93f472 748                 for (String username : usernames) {
ae0b13 749                     UserModel user = new UserModel(username.toLowerCase());
fe24a0 750                     user.password = config.getString(USER, username, PASSWORD);
93f472 751
JM 752                     // user roles
753                     Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
fe24a0 754                             USER, username, ROLE)));
93f472 755                     user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
JM 756                     user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
757
758                     // repository memberships
759                     Set<String> repositories = new HashSet<String>(Arrays.asList(config
fe24a0 760                             .getStringList(USER, username, REPOSITORY)));
93f472 761                     for (String repository : repositories) {
JM 762                         user.addRepository(repository);
763                     }
764
765                     // update cache
ae0b13 766                     users.put(user.username, user);
JM 767                     cookies.put(StringUtils.getSHA1(user.username + user.password), user);
93f472 768                 }
fe24a0 769
JM 770                 // load the teams
771                 Set<String> teamnames = config.getSubsections(TEAM);
772                 for (String teamname : teamnames) {
773                     TeamModel team = new TeamModel(teamname);
774                     team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname,
775                             REPOSITORY)));
776                     team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
d7905a 777                     team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname,
JM 778                             MAILINGLIST)));
779                     team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
780                             teamname, PRERECEIVE)));
781                     team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
782                             teamname, POSTRECEIVE)));
fe24a0 783
JM 784                     teams.put(team.name.toLowerCase(), team);
785
786                     // set the teams on the users
787                     for (String user : team.users) {
788                         UserModel model = users.get(user);
789                         if (model != null) {
790                             model.teams.add(team);
791                         }
792                     }
793                 }
93f472 794             } catch (Exception e) {
JM 795                 logger.error(MessageFormat.format("Failed to read {0}", realmFile), e);
796             }
797         }
798     }
799
800     protected long lastModified() {
801         return lastModified;
802     }
803
804     @Override
805     public String toString() {
806         return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")";
807     }
808 }