James Moger
2011-12-23 31bcbea4c35e29d3b5147d33a41544cb125cf694
commit | author | age
f13c4c 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  */
fc948c 16 package com.gitblit;
JM 17
b75734 18 import java.io.BufferedReader;
fc948c 19 import java.io.File;
831469 20 import java.io.FileFilter;
f97bf0 21 import java.io.IOException;
b75734 22 import java.io.InputStream;
JM 23 import java.io.InputStreamReader;
dd6f08 24 import java.lang.reflect.Field;
166e6a 25 import java.text.MessageFormat;
fc948c 26 import java.util.ArrayList;
8f73a7 27 import java.util.Arrays;
0b9119 28 import java.util.Collection;
00afd7 29 import java.util.Collections;
8c9a20 30 import java.util.HashMap;
6cc1d4 31 import java.util.HashSet;
fc948c 32 import java.util.List;
8c9a20 33 import java.util.Map;
JM 34 import java.util.Map.Entry;
6cc1d4 35 import java.util.Set;
831469 36 import java.util.concurrent.ConcurrentHashMap;
JM 37 import java.util.concurrent.Executors;
38 import java.util.concurrent.ScheduledExecutorService;
39 import java.util.concurrent.TimeUnit;
dd6f08 40 import java.util.concurrent.atomic.AtomicInteger;
fc948c 41
831469 42 import javax.mail.Message;
JM 43 import javax.mail.MessagingException;
b75734 44 import javax.servlet.ServletContext;
87cc1e 45 import javax.servlet.ServletContextEvent;
JM 46 import javax.servlet.ServletContextListener;
85c2e6 47 import javax.servlet.http.Cookie;
fc948c 48
85c2e6 49 import org.apache.wicket.protocol.http.WebResponse;
fc948c 50 import org.eclipse.jgit.errors.RepositoryNotFoundException;
JM 51 import org.eclipse.jgit.lib.Repository;
5c2841 52 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
f97bf0 53 import org.eclipse.jgit.lib.StoredConfig;
f5d0ad 54 import org.eclipse.jgit.transport.resolver.FileResolver;
892570 55 import org.eclipse.jgit.transport.resolver.RepositoryResolver;
JM 56 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
f5d0ad 57 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
5c2841 58 import org.eclipse.jgit.util.FS;
8a2e9c 59 import org.eclipse.jgit.util.FileUtils;
fc948c 60 import org.slf4j.Logger;
JM 61 import org.slf4j.LoggerFactory;
62
dfb889 63 import com.gitblit.Constants.AccessRestrictionType;
831469 64 import com.gitblit.Constants.FederationRequest;
JM 65 import com.gitblit.Constants.FederationStrategy;
66 import com.gitblit.Constants.FederationToken;
67 import com.gitblit.models.FederationModel;
68 import com.gitblit.models.FederationProposal;
31abc2 69 import com.gitblit.models.FederationSet;
4d44cf 70 import com.gitblit.models.Metric;
1f9dae 71 import com.gitblit.models.RepositoryModel;
d03aff 72 import com.gitblit.models.ServerSettings;
b75734 73 import com.gitblit.models.ServerStatus;
JM 74 import com.gitblit.models.SettingModel;
fe24a0 75 import com.gitblit.models.TeamModel;
1f9dae 76 import com.gitblit.models.UserModel;
72633d 77 import com.gitblit.utils.ByteFormat;
f6740d 78 import com.gitblit.utils.FederationUtils;
fc948c 79 import com.gitblit.utils.JGitUtils;
93f0b1 80 import com.gitblit.utils.JsonUtils;
4d44cf 81 import com.gitblit.utils.MetricUtils;
9275bd 82 import com.gitblit.utils.ObjectCache;
00afd7 83 import com.gitblit.utils.StringUtils;
fc948c 84
892570 85 /**
JM 86  * GitBlit is the servlet context listener singleton that acts as the core for
87  * the web ui and the servlets. This class is either directly instantiated by
88  * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the
89  * definition in the web.xml file (Gitblit WAR).
90  * 
91  * This class is the central logic processor for Gitblit. All settings, user
92  * object, and repository object operations pass through this class.
93  * 
94  * Repository Resolution. There are two pathways for finding repositories. One
95  * pathway, for web ui display and repository authentication & authorization, is
96  * within this class. The other pathway is through the standard GitServlet.
97  * 
98  * @author James Moger
99  * 
100  */
87cc1e 101 public class GitBlit implements ServletContextListener {
fc948c 102
5450d0 103     private static GitBlit gitblit;
fc948c 104
JM 105     private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
106
831469 107     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
JM 108
109     private final List<FederationModel> federationRegistrations = Collections
110             .synchronizedList(new ArrayList<FederationModel>());
111
112     private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
4d44cf 113
JM 114     private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
115
116     private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
831469 117
892570 118     private RepositoryResolver<Void> repositoryResolver;
fc948c 119
b75734 120     private ServletContext servletContext;
JM 121
166e6a 122     private File repositoriesFolder;
fc948c 123
5450d0 124     private boolean exportAll = true;
fc948c 125
85c2e6 126     private IUserService userService;
fc948c 127
892570 128     private IStoredSettings settings;
b75734 129
84c1d5 130     private ServerSettings settingsModel;
b75734 131
JM 132     private ServerStatus serverStatus;
831469 133
JM 134     private MailExecutor mailExecutor;
87cc1e 135
5450d0 136     public GitBlit() {
JM 137         if (gitblit == null) {
892570 138             // set the static singleton reference
5450d0 139             gitblit = this;
JM 140         }
87cc1e 141     }
JM 142
892570 143     /**
JM 144      * Returns the Gitblit singleton.
145      * 
146      * @return gitblit singleton
147      */
2a7306 148     public static GitBlit self() {
5450d0 149         if (gitblit == null) {
892570 150             new GitBlit();
5450d0 151         }
JM 152         return gitblit;
831469 153     }
JM 154
155     /**
156      * Determine if this is the GO variant of Gitblit.
157      * 
158      * @return true if this is the GO variant of Gitblit.
159      */
160     public static boolean isGO() {
161         return self().settings instanceof FileSettings;
2a7306 162     }
JM 163
892570 164     /**
JM 165      * Returns the boolean value for the specified key. If the key does not
166      * exist or the value for the key can not be interpreted as a boolean, the
167      * defaultValue is returned.
168      * 
169      * @see IStoredSettings.getBoolean(String, boolean)
170      * @param key
171      * @param defaultValue
172      * @return key value or defaultValue
173      */
2a7306 174     public static boolean getBoolean(String key, boolean defaultValue) {
892570 175         return self().settings.getBoolean(key, defaultValue);
2a7306 176     }
JM 177
892570 178     /**
JM 179      * Returns the integer value for the specified key. If the key does not
180      * exist or the value for the key can not be interpreted as an integer, the
181      * defaultValue is returned.
182      * 
183      * @see IStoredSettings.getInteger(String key, int defaultValue)
184      * @param key
185      * @param defaultValue
186      * @return key value or defaultValue
187      */
2a7306 188     public static int getInteger(String key, int defaultValue) {
892570 189         return self().settings.getInteger(key, defaultValue);
2a7306 190     }
JM 191
892570 192     /**
7e5ee5 193      * Returns the char value for the specified key. If the key does not exist
JM 194      * or the value for the key can not be interpreted as a character, the
195      * defaultValue is returned.
196      * 
197      * @see IStoredSettings.getChar(String key, char defaultValue)
198      * @param key
199      * @param defaultValue
200      * @return key value or defaultValue
201      */
202     public static char getChar(String key, char defaultValue) {
203         return self().settings.getChar(key, defaultValue);
204     }
dd6f08 205
7e5ee5 206     /**
892570 207      * Returns the string value for the specified key. If the key does not exist
JM 208      * or the value for the key can not be interpreted as a string, the
209      * defaultValue is returned.
210      * 
211      * @see IStoredSettings.getString(String key, String defaultValue)
212      * @param key
213      * @param defaultValue
214      * @return key value or defaultValue
215      */
2a7306 216     public static String getString(String key, String defaultValue) {
892570 217         return self().settings.getString(key, defaultValue);
2a7306 218     }
JM 219
892570 220     /**
JM 221      * Returns a list of space-separated strings from the specified key.
222      * 
223      * @see IStoredSettings.getStrings(String key)
224      * @param name
225      * @return list of strings
226      */
2a7306 227     public static List<String> getStrings(String key) {
892570 228         return self().settings.getStrings(key);
2a7306 229     }
892570 230
JM 231     /**
232      * Returns the list of keys whose name starts with the specified prefix. If
233      * the prefix is null or empty, all key names are returned.
234      * 
235      * @see IStoredSettings.getAllKeys(String key)
236      * @param startingWith
237      * @return list of keys
238      */
2a7306 239
JM 240     public static List<String> getAllKeys(String startingWith) {
892570 241         return self().settings.getAllKeys(startingWith);
fc948c 242     }
155bf7 243
892570 244     /**
JM 245      * Is Gitblit running in debug mode?
246      * 
247      * @return true if Gitblit is running in debug mode
248      */
8c9a20 249     public static boolean isDebugMode() {
892570 250         return self().settings.getBoolean(Keys.web.debugMode, false);
d03aff 251     }
JM 252
253     /**
b774de 254      * Returns the file object for the specified configuration key.
JM 255      * 
256      * @return the file
257      */
258     public static File getFileOrFolder(String key, String defaultFileOrFolder) {
259         String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
260         return getFileOrFolder(fileOrFolder);
261     }
262
263     /**
264      * Returns the file object which may have it's base-path determined by
265      * environment variables for running on a cloud hosting service. All Gitblit
266      * file or folder retrievals are (at least initially) funneled through this
267      * method so it is the correct point to globally override/alter filesystem
268      * access based on environment or some other indicator.
269      * 
270      * @return the file
271      */
272     public static File getFileOrFolder(String fileOrFolder) {
273         String openShift = System.getenv("OPENSHIFT_DATA_DIR");
274         if (!StringUtils.isEmpty(openShift)) {
275             // running on RedHat OpenShift
276             return new File(openShift, fileOrFolder);
277         }
278         return new File(fileOrFolder);
279     }
280
281     /**
282      * Returns the path of the repositories folder. This method checks to see if
283      * Gitblit is running on a cloud service and may return an adjusted path.
284      * 
285      * @return the repositories folder path
286      */
287     public static File getRepositoriesFolder() {
288         return getFileOrFolder(Keys.git.repositoriesFolder, "git");
289     }
290
291     /**
292      * Returns the path of the proposals folder. This method checks to see if
293      * Gitblit is running on a cloud service and may return an adjusted path.
294      * 
295      * @return the proposals folder path
296      */
297     public static File getProposalsFolder() {
298         return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
6cc1d4 299     }
JM 300
301     /**
302      * Returns the path of the Groovy folder. This method checks to see if
303      * Gitblit is running on a cloud service and may return an adjusted path.
304      * 
305      * @return the Groovy scripts folder path
306      */
307     public static File getGroovyScriptsFolder() {
308         return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
b774de 309     }
JM 310
311     /**
d03aff 312      * Updates the list of server settings.
JM 313      * 
314      * @param settings
315      * @return true if the update succeeded
316      */
97a20e 317     public boolean updateSettings(Map<String, String> updatedSettings) {
486ee1 318         return settings.saveSettings(updatedSettings);
b75734 319     }
JM 320
321     public ServerStatus getStatus() {
322         // update heap memory status
323         serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
324         serverStatus.heapFree = Runtime.getRuntime().freeMemory();
325         return serverStatus;
87cc1e 326     }
JM 327
892570 328     /**
JM 329      * Returns the list of non-Gitblit clone urls. This allows Gitblit to
330      * advertise alternative urls for Git client repository access.
331      * 
332      * @param repositoryName
333      * @return list of non-gitblit clone urls
334      */
8a2e9c 335     public List<String> getOtherCloneUrls(String repositoryName) {
JM 336         List<String> cloneUrls = new ArrayList<String>();
892570 337         for (String url : settings.getStrings(Keys.web.otherUrls)) {
8a2e9c 338             cloneUrls.add(MessageFormat.format(url, repositoryName));
JM 339         }
340         return cloneUrls;
fc948c 341     }
JM 342
892570 343     /**
JM 344      * Set the user service. The user service authenticates all users and is
345      * responsible for managing user permissions.
346      * 
347      * @param userService
348      */
85c2e6 349     public void setUserService(IUserService userService) {
JM 350         logger.info("Setting up user service " + userService.toString());
351         this.userService = userService;
63ee41 352         this.userService.setup(settings);
fc948c 353     }
JM 354
892570 355     /**
JM 356      * Authenticate a user based on a username and password.
357      * 
358      * @see IUserService.authenticate(String, char[])
359      * @param username
360      * @param password
361      * @return a user object or null
362      */
511554 363     public UserModel authenticate(String username, char[] password) {
831469 364         if (StringUtils.isEmpty(username)) {
JM 365             // can not authenticate empty username
366             return null;
367         }
368         String pw = new String(password);
369         if (StringUtils.isEmpty(pw)) {
370             // can not authenticate empty password
371             return null;
372         }
373
374         // check to see if this is the federation user
375         if (canFederate()) {
376             if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
377                 List<String> tokens = getFederationTokens();
378                 if (tokens.contains(pw)) {
379                     // the federation user is an administrator
380                     UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
381                     federationUser.canAdmin = true;
382                     return federationUser;
383                 }
384             }
385         }
386
387         // delegate authentication to the user service
85c2e6 388         if (userService == null) {
fc948c 389             return null;
JM 390         }
85c2e6 391         return userService.authenticate(username, password);
JM 392     }
393
892570 394     /**
JM 395      * Authenticate a user based on their cookie.
396      * 
397      * @param cookies
398      * @return a user object or null
399      */
85c2e6 400     public UserModel authenticate(Cookie[] cookies) {
JM 401         if (userService == null) {
402             return null;
403         }
404         if (userService.supportsCookies()) {
405             if (cookies != null && cookies.length > 0) {
406                 for (Cookie cookie : cookies) {
407                     if (cookie.getName().equals(Constants.NAME)) {
408                         String value = cookie.getValue();
409                         return userService.authenticate(value.toCharArray());
410                     }
411                 }
412             }
413         }
414         return null;
415     }
416
892570 417     /**
JM 418      * Sets a cookie for the specified user.
419      * 
420      * @param response
421      * @param user
422      */
85c2e6 423     public void setCookie(WebResponse response, UserModel user) {
JM 424         if (userService == null) {
425             return;
426         }
427         if (userService.supportsCookies()) {
428             Cookie userCookie;
429             if (user == null) {
430                 // clear cookie for logout
431                 userCookie = new Cookie(Constants.NAME, "");
432             } else {
433                 // set cookie for login
434                 char[] cookie = userService.getCookie(user);
435                 userCookie = new Cookie(Constants.NAME, new String(cookie));
436                 userCookie.setMaxAge(Integer.MAX_VALUE);
437             }
438             userCookie.setPath("/");
439             response.addCookie(userCookie);
440         }
fc948c 441     }
JM 442
892570 443     /**
JM 444      * Returns the list of all users available to the login service.
445      * 
446      * @see IUserService.getAllUsernames()
447      * @return list of all usernames
448      */
f98825 449     public List<String> getAllUsernames() {
85c2e6 450         List<String> names = new ArrayList<String>(userService.getAllUsernames());
00afd7 451         Collections.sort(names);
JM 452         return names;
8a2e9c 453     }
JM 454
892570 455     /**
JM 456      * Delete the user object with the specified username
457      * 
458      * @see IUserService.deleteUser(String)
459      * @param username
460      * @return true if successful
461      */
8a2e9c 462     public boolean deleteUser(String username) {
85c2e6 463         return userService.deleteUser(username);
f98825 464     }
dfb889 465
892570 466     /**
JM 467      * Retrieve the user object for the specified username.
468      * 
469      * @see IUserService.getUserModel(String)
470      * @param username
471      * @return a user object or null
472      */
f98825 473     public UserModel getUserModel(String username) {
85c2e6 474         UserModel user = userService.getUserModel(username);
dfb889 475         return user;
f98825 476     }
8a2e9c 477
892570 478     /**
JM 479      * Returns the list of all users who are allowed to bypass the access
480      * restriction placed on the specified repository.
481      * 
482      * @see IUserService.getUsernamesForRepositoryRole(String)
483      * @param repository
484      * @return list of all usernames that can bypass the access restriction
485      */
f98825 486     public List<String> getRepositoryUsers(RepositoryModel repository) {
892570 487         return userService.getUsernamesForRepositoryRole(repository.name);
f98825 488     }
8a2e9c 489
892570 490     /**
JM 491      * Sets the list of all uses who are allowed to bypass the access
492      * restriction placed on the specified repository.
493      * 
494      * @see IUserService.setUsernamesForRepositoryRole(String, List<String>)
495      * @param repository
496      * @param usernames
497      * @return true if successful
498      */
f98825 499     public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
892570 500         return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers);
dfb889 501     }
JM 502
892570 503     /**
JM 504      * Adds/updates a complete user object keyed by username. This method allows
505      * for renaming a user.
506      * 
507      * @see IUserService.updateUserModel(String, UserModel)
508      * @param username
509      * @param user
510      * @param isCreate
511      * @throws GitBlitException
512      */
513     public void updateUserModel(String username, UserModel user, boolean isCreate)
2a7306 514             throws GitBlitException {
16038c 515         if (!username.equalsIgnoreCase(user.username)) {
JM 516             if (userService.getUserModel(user.username) != null) {
d03aff 517                 throw new GitBlitException(MessageFormat.format(
JM 518                         "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
519                         user.username));
16038c 520             }
JM 521         }
85c2e6 522         if (!userService.updateUserModel(username, user)) {
dfb889 523             throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
JM 524         }
fe24a0 525     }
JM 526
527     /**
528      * Returns the list of available teams that a user or repository may be
529      * assigned to.
530      * 
531      * @return the list of teams
532      */
533     public List<String> getAllTeamnames() {
534         List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
535         Collections.sort(teams);
536         return teams;
537     }
538
539     /**
540      * Returns the TeamModel object for the specified name.
541      * 
542      * @param teamname
543      * @return a TeamModel object or null
544      */
545     public TeamModel getTeamModel(String teamname) {
546         return userService.getTeamModel(teamname);
547     }
548
549     /**
550      * Returns the list of all teams who are allowed to bypass the access
551      * restriction placed on the specified repository.
552      * 
553      * @see IUserService.getTeamnamesForRepositoryRole(String)
554      * @param repository
555      * @return list of all teamnames that can bypass the access restriction
556      */
557     public List<String> getRepositoryTeams(RepositoryModel repository) {
558         return userService.getTeamnamesForRepositoryRole(repository.name);
559     }
560
561     /**
562      * Sets the list of all uses who are allowed to bypass the access
563      * restriction placed on the specified repository.
564      * 
565      * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
566      * @param repository
567      * @param teamnames
568      * @return true if successful
569      */
570     public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
571         return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
572     }
fa54be 573
fe24a0 574     /**
JM 575      * Updates the TeamModel object for the specified name.
576      * 
577      * @param teamname
578      * @param team
579      * @param isCreate
580      */
fa54be 581     public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
JM 582             throws GitBlitException {
fe24a0 583         if (!teamname.equalsIgnoreCase(team.name)) {
JM 584             if (userService.getTeamModel(team.name) != null) {
585                 throw new GitBlitException(MessageFormat.format(
586                         "Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
587                         team.name));
588             }
589         }
590         if (!userService.updateTeamModel(teamname, team)) {
591             throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
592         }
593     }
fa54be 594
fe24a0 595     /**
JM 596      * Delete the team object with the specified teamname
597      * 
598      * @see IUserService.deleteTeam(String)
599      * @param teamname
600      * @return true if successful
601      */
602     public boolean deleteTeam(String teamname) {
603         return userService.deleteTeam(teamname);
dfb889 604     }
JM 605
892570 606     /**
4d44cf 607      * Clears all the cached data for the specified repository.
JM 608      * 
609      * @param repositoryName
610      */
611     public void clearRepositoryCache(String repositoryName) {
612         repositorySizeCache.remove(repositoryName);
613         repositoryMetricsCache.remove(repositoryName);
614     }
615
616     /**
892570 617      * Returns the list of all repositories available to Gitblit. This method
JM 618      * does not consider user access permissions.
619      * 
620      * @return list of all repositories
621      */
166e6a 622     public List<String> getRepositoryList() {
2a7306 623         return JGitUtils.getRepositoryList(repositoriesFolder, exportAll,
892570 624                 settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
166e6a 625     }
155bf7 626
892570 627     /**
JM 628      * Returns the JGit repository for the specified name.
629      * 
630      * @param repositoryName
631      * @return repository or null
632      */
166e6a 633     public Repository getRepository(String repositoryName) {
JM 634         Repository r = null;
635         try {
636             r = repositoryResolver.open(null, repositoryName);
637         } catch (RepositoryNotFoundException e) {
638             r = null;
1f9dae 639             logger.error("GitBlit.getRepository(String) failed to find "
JM 640                     + new File(repositoriesFolder, repositoryName).getAbsolutePath());
892570 641         } catch (ServiceNotAuthorizedException e) {
JM 642             r = null;
643             logger.error("GitBlit.getRepository(String) failed to find "
644                     + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
166e6a 645         } catch (ServiceNotEnabledException e) {
JM 646             r = null;
892570 647             logger.error("GitBlit.getRepository(String) failed to find "
JM 648                     + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
166e6a 649         }
JM 650         return r;
651     }
dfb889 652
892570 653     /**
JM 654      * Returns the list of repository models that are accessible to the user.
655      * 
656      * @param user
657      * @return list of repository models accessible to user
658      */
511554 659     public List<RepositoryModel> getRepositoryModels(UserModel user) {
166e6a 660         List<String> list = getRepositoryList();
JM 661         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
662         for (String repo : list) {
dfb889 663             RepositoryModel model = getRepositoryModel(user, repo);
JM 664             if (model != null) {
665                 repositories.add(model);
666             }
166e6a 667         }
3d293a 668         if (getBoolean(Keys.web.showRepositorySizes, true)) {
JM 669             int repoCount = 0;
670             long startTime = System.currentTimeMillis();
671             ByteFormat byteFormat = new ByteFormat();
672             for (RepositoryModel model : repositories) {
673                 if (!model.skipSizeCalculation) {
674                     repoCount++;
675                     model.size = byteFormat.format(calculateSize(model));
676                 }
677             }
678             long duration = System.currentTimeMillis() - startTime;
679             logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
680                     repoCount, duration));
681         }
166e6a 682         return repositories;
JM 683     }
8a2e9c 684
892570 685     /**
JM 686      * Returns a repository model if the repository exists and the user may
687      * access the repository.
688      * 
689      * @param user
690      * @param repositoryName
691      * @return repository model or null
692      */
511554 693     public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
dfb889 694         RepositoryModel model = getRepositoryModel(repositoryName);
85c2e6 695         if (model == null) {
JM 696             return null;
697         }
dfb889 698         if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
efe8ec 699             if (user != null && user.canAccessRepository(model)) {
dfb889 700                 return model;
JM 701             }
702             return null;
703         } else {
704             return model;
705         }
706     }
707
892570 708     /**
JM 709      * Returns the repository model for the specified repository. This method
710      * does not consider user access permissions.
711      * 
712      * @param repositoryName
713      * @return repository model or null
714      */
166e6a 715     public RepositoryModel getRepositoryModel(String repositoryName) {
JM 716         Repository r = getRepository(repositoryName);
1f9dae 717         if (r == null) {
JM 718             return null;
719         }
166e6a 720         RepositoryModel model = new RepositoryModel();
JM 721         model.name = repositoryName;
bc9d4a 722         model.hasCommits = JGitUtils.hasCommits(r);
ed21d2 723         model.lastChange = JGitUtils.getLastChange(r, null);
166e6a 724         StoredConfig config = JGitUtils.readConfig(r);
JM 725         if (config != null) {
00afd7 726             model.description = getConfig(config, "description", "");
JM 727             model.owner = getConfig(config, "owner", "");
728             model.useTickets = getConfig(config, "useTickets", false);
729             model.useDocs = getConfig(config, "useDocs", false);
2a7306 730             model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
JM 731                     "accessRestriction", null));
00afd7 732             model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
JM 733             model.isFrozen = getConfig(config, "isFrozen", false);
a1ea87 734             model.showReadme = getConfig(config, "showReadme", false);
3d293a 735             model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
fe3262 736             model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
831469 737             model.federationStrategy = FederationStrategy.fromName(getConfig(config,
JM 738                     "federationStrategy", null));
8f73a7 739             model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 740                     "gitblit", null, "federationSets")));
831469 741             model.isFederated = getConfig(config, "isFederated", false);
JM 742             model.origin = config.getString("remote", "origin", "url");
fa54be 743             model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 744                     "gitblit", null, "preReceiveScript")));
745             model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
746                     "gitblit", null, "postReceiveScript")));
eb96ea 747             model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 748                     "gitblit", null, "mailingList")));
166e6a 749         }
JM 750         r.close();
751         return model;
00afd7 752     }
8a2e9c 753
892570 754     /**
4d44cf 755      * Returns the size in bytes of the repository. Gitblit caches the
JM 756      * repository sizes to reduce the performance penalty of recursive
757      * calculation. The cache is updated if the repository has been changed
758      * since the last calculation.
5c2841 759      * 
JM 760      * @param model
761      * @return size in bytes
762      */
763     public long calculateSize(RepositoryModel model) {
4d44cf 764         if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
JM 765             return repositorySizeCache.getObject(model.name);
766         }
5c2841 767         File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
4d44cf 768         long size = com.gitblit.utils.FileUtils.folderSize(gitDir);
JM 769         repositorySizeCache.updateObject(model.name, model.lastChange, size);
770         return size;
5c2841 771     }
JM 772
773     /**
dd6f08 774      * Ensure that a cached repository is completely closed and its resources
JM 775      * are properly released.
776      * 
777      * @param repositoryName
778      */
779     private void closeRepository(String repositoryName) {
780         Repository repository = getRepository(repositoryName);
781         // assume 2 uses in case reflection fails
782         int uses = 2;
783         try {
784             // The FileResolver caches repositories which is very useful
785             // for performance until you want to delete a repository.
786             // I have to use reflection to call close() the correct
787             // number of times to ensure that the object and ref databases
788             // are properly closed before I can delete the repository from
789             // the filesystem.
790             Field useCnt = Repository.class.getDeclaredField("useCnt");
791             useCnt.setAccessible(true);
792             uses = ((AtomicInteger) useCnt.get(repository)).get();
793         } catch (Exception e) {
794             logger.warn(MessageFormat
795                     .format("Failed to reflectively determine use count for repository {0}",
796                             repositoryName), e);
797         }
798         if (uses > 0) {
799             logger.info(MessageFormat
800                     .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
801                             repositoryName, uses, uses));
802             for (int i = 0; i < uses; i++) {
803                 repository.close();
804             }
805         }
806     }
807
808     /**
4d44cf 809      * Returns the metrics for the default branch of the specified repository.
JM 810      * This method builds a metrics cache. The cache is updated if the
811      * repository is updated. A new copy of the metrics list is returned on each
812      * call so that modifications to the list are non-destructive.
813      * 
814      * @param model
815      * @param repository
816      * @return a new array list of metrics
817      */
818     public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
819         if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
820             return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
821         }
822         List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null);
823         repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
824         return new ArrayList<Metric>(metrics);
825     }
826
827     /**
828      * Returns the gitblit string value for the specified key. If key is not
892570 829      * set, returns defaultValue.
JM 830      * 
831      * @param config
832      * @param field
833      * @param defaultValue
834      * @return field value or defaultValue
835      */
00afd7 836     private String getConfig(StoredConfig config, String field, String defaultValue) {
JM 837         String value = config.getString("gitblit", null, field);
838         if (StringUtils.isEmpty(value)) {
839             return defaultValue;
840         }
841         return value;
842     }
8a2e9c 843
892570 844     /**
JM 845      * Returns the gitblit boolean vlaue for the specified key. If key is not
846      * set, returns defaultValue.
847      * 
848      * @param config
849      * @param field
850      * @param defaultValue
851      * @return field value or defaultValue
852      */
00afd7 853     private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
JM 854         return config.getBoolean("gitblit", field, defaultValue);
166e6a 855     }
JM 856
892570 857     /**
JM 858      * Creates/updates the repository model keyed by reopsitoryName. Saves all
859      * repository settings in .git/config. This method allows for renaming
860      * repositories and will update user access permissions accordingly.
861      * 
862      * All repositories created by this method are bare and automatically have
863      * .git appended to their names, which is the standard convention for bare
864      * repositories.
865      * 
866      * @param repositoryName
867      * @param repository
868      * @param isCreate
869      * @throws GitBlitException
870      */
871     public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
2a7306 872             boolean isCreate) throws GitBlitException {
f5d0ad 873         Repository r = null;
JM 874         if (isCreate) {
a3bde6 875             // ensure created repository name ends with .git
168566 876             if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
a3bde6 877                 repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
JM 878             }
166e6a 879             if (new File(repositoriesFolder, repository.name).exists()) {
2a7306 880                 throw new GitBlitException(MessageFormat.format(
JM 881                         "Can not create repository ''{0}'' because it already exists.",
882                         repository.name));
166e6a 883             }
dfb889 884             // create repository
f5d0ad 885             logger.info("create repository " + repository.name);
168566 886             r = JGitUtils.createRepository(repositoriesFolder, repository.name);
f5d0ad 887         } else {
8a2e9c 888             // rename repository
JM 889             if (!repositoryName.equalsIgnoreCase(repository.name)) {
16038c 890                 if (!repository.name.toLowerCase().endsWith(
JM 891                         org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
892                     repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
893                 }
894                 if (new File(repositoriesFolder, repository.name).exists()) {
d03aff 895                     throw new GitBlitException(MessageFormat.format(
JM 896                             "Failed to rename ''{0}'' because ''{1}'' already exists.",
897                             repositoryName, repository.name));
16038c 898                 }
dd6f08 899                 closeRepository(repositoryName);
8a2e9c 900                 File folder = new File(repositoriesFolder, repositoryName);
JM 901                 File destFolder = new File(repositoriesFolder, repository.name);
902                 if (destFolder.exists()) {
2a7306 903                     throw new GitBlitException(
JM 904                             MessageFormat
905                                     .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
906                                             repositoryName, repository.name));
8a2e9c 907                 }
JM 908                 if (!folder.renameTo(destFolder)) {
2a7306 909                     throw new GitBlitException(MessageFormat.format(
JM 910                             "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
911                             repository.name));
8a2e9c 912                 }
JM 913                 // rename the roles
85c2e6 914                 if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
2a7306 915                     throw new GitBlitException(MessageFormat.format(
JM 916                             "Failed to rename repository permissions ''{0}'' to ''{1}''.",
917                             repositoryName, repository.name));
8a2e9c 918                 }
4d44cf 919
JM 920                 // clear the cache
921                 clearRepositoryCache(repositoryName);
8a2e9c 922             }
JM 923
f5d0ad 924             // load repository
JM 925             logger.info("edit repository " + repository.name);
926             try {
927                 r = repositoryResolver.open(null, repository.name);
928             } catch (RepositoryNotFoundException e) {
929                 logger.error("Repository not found", e);
892570 930             } catch (ServiceNotAuthorizedException e) {
JM 931                 logger.error("Service not authorized", e);
f5d0ad 932             } catch (ServiceNotEnabledException e) {
JM 933                 logger.error("Service not enabled", e);
934             }
f97bf0 935         }
JM 936
f5d0ad 937         // update settings
2a7306 938         if (r != null) {
831469 939             updateConfiguration(r, repository);
2a7306 940             r.close();
831469 941         }
JM 942     }
943
944     /**
945      * Updates the Gitblit configuration for the specified repository.
946      * 
947      * @param r
948      *            the Git repository
949      * @param repository
950      *            the Gitblit repository model
951      */
952     public void updateConfiguration(Repository r, RepositoryModel repository) {
953         StoredConfig config = JGitUtils.readConfig(r);
954         config.setString("gitblit", null, "description", repository.description);
955         config.setString("gitblit", null, "owner", repository.owner);
956         config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
957         config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
958         config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
959         config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
960         config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
961         config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
3d293a 962         config.setBoolean("gitblit", null, "skipSizeCalculation", repository.skipSizeCalculation);
fe3262 963         config.setBoolean("gitblit", null, "skipSummaryMetrics", repository.skipSummaryMetrics);
8f73a7 964         config.setStringList("gitblit", null, "federationSets", repository.federationSets);
831469 965         config.setString("gitblit", null, "federationStrategy",
JM 966                 repository.federationStrategy.name());
967         config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
3a2c57 968         if (repository.preReceiveScripts != null) {
JM 969             config.setStringList("gitblit", null, "preReceiveScript", repository.preReceiveScripts);
970         }
971         if (repository.postReceiveScripts != null) {
972             config.setStringList("gitblit", null, "postReceiveScript",
973                     repository.postReceiveScripts);
974         }
eb96ea 975         if (repository.mailingLists != null) {
JM 976             config.setStringList("gitblit", null, "mailingList", repository.mailingLists);
3a2c57 977         }
831469 978         try {
JM 979             config.save();
980         } catch (IOException e) {
981             logger.error("Failed to save repository config!", e);
f97bf0 982         }
f5d0ad 983     }
JM 984
892570 985     /**
JM 986      * Deletes the repository from the file system and removes the repository
987      * permission from all repository users.
988      * 
989      * @param model
990      * @return true if successful
991      */
8a2e9c 992     public boolean deleteRepositoryModel(RepositoryModel model) {
JM 993         return deleteRepository(model.name);
994     }
995
892570 996     /**
JM 997      * Deletes the repository from the file system and removes the repository
998      * permission from all repository users.
999      * 
1000      * @param repositoryName
1001      * @return true if successful
1002      */
8a2e9c 1003     public boolean deleteRepository(String repositoryName) {
JM 1004         try {
dd6f08 1005             closeRepository(repositoryName);
8a2e9c 1006             File folder = new File(repositoriesFolder, repositoryName);
JM 1007             if (folder.exists() && folder.isDirectory()) {
892570 1008                 FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
85c2e6 1009                 if (userService.deleteRepositoryRole(repositoryName)) {
8a2e9c 1010                     return true;
JM 1011                 }
1012             }
4d44cf 1013
JM 1014             // clear the repository cache
1015             clearRepositoryCache(repositoryName);
8a2e9c 1016         } catch (Throwable t) {
JM 1017             logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
1018         }
1019         return false;
1020     }
1021
892570 1022     /**
JM 1023      * Returns an html version of the commit message with any global or
1024      * repository-specific regular expression substitution applied.
1025      * 
1026      * @param repositoryName
1027      * @param text
1028      * @return html version of the commit message
1029      */
8c9a20 1030     public String processCommitMessage(String repositoryName, String text) {
JM 1031         String html = StringUtils.breakLinesForHtml(text);
1032         Map<String, String> map = new HashMap<String, String>();
1033         // global regex keys
892570 1034         if (settings.getBoolean(Keys.regex.global, false)) {
JM 1035             for (String key : settings.getAllKeys(Keys.regex.global)) {
8c9a20 1036                 if (!key.equals(Keys.regex.global)) {
JM 1037                     String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1038                     map.put(subKey, settings.getString(key, ""));
8c9a20 1039                 }
JM 1040             }
1041         }
1042
1043         // repository-specific regex keys
892570 1044         List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "."
8c9a20 1045                 + repositoryName.toLowerCase());
JM 1046         for (String key : keys) {
1047             String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1048             map.put(subKey, settings.getString(key, ""));
8c9a20 1049         }
JM 1050
1051         for (Entry<String, String> entry : map.entrySet()) {
1052             String definition = entry.getValue().trim();
1053             String[] chunks = definition.split("!!!");
1054             if (chunks.length == 2) {
1055                 html = html.replaceAll(chunks[0], chunks[1]);
1056             } else {
1057                 logger.warn(entry.getKey()
1058                         + " improperly formatted.  Use !!! to separate match from replacement: "
1059                         + definition);
1060             }
1061         }
1062         return html;
1063     }
1064
892570 1065     /**
831469 1066      * Returns Gitblit's scheduled executor service for scheduling tasks.
JM 1067      * 
1068      * @return scheduledExecutor
1069      */
1070     public ScheduledExecutorService executor() {
1071         return scheduledExecutor;
1072     }
1073
1074     public static boolean canFederate() {
2c32fd 1075         String passphrase = getString(Keys.federation.passphrase, "");
JM 1076         return !StringUtils.isEmpty(passphrase);
831469 1077     }
JM 1078
1079     /**
1080      * Configures this Gitblit instance to pull any registered federated gitblit
1081      * instances.
1082      */
1083     private void configureFederation() {
2c32fd 1084         boolean validPassphrase = true;
JM 1085         String passphrase = settings.getString(Keys.federation.passphrase, "");
1086         if (StringUtils.isEmpty(passphrase)) {
1087             logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
1088             validPassphrase = false;
831469 1089         }
2c32fd 1090         if (validPassphrase) {
8f73a7 1091             // standard tokens
831469 1092             for (FederationToken tokenType : FederationToken.values()) {
JM 1093                 logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
1094                         getFederationToken(tokenType)));
8f73a7 1095             }
JM 1096
1097             // federation set tokens
1098             for (String set : settings.getStrings(Keys.federation.sets)) {
1099                 logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
1100                         getFederationToken(set)));
831469 1101             }
JM 1102         }
1103
1104         // Schedule the federation executor
1105         List<FederationModel> registrations = getFederationRegistrations();
1106         if (registrations.size() > 0) {
f6740d 1107             FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
JM 1108             scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
831469 1109         }
JM 1110     }
1111
1112     /**
1113      * Returns the list of federated gitblit instances that this instance will
1114      * try to pull.
1115      * 
1116      * @return list of registered gitblit instances
1117      */
1118     public List<FederationModel> getFederationRegistrations() {
1119         if (federationRegistrations.isEmpty()) {
f6740d 1120             federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
831469 1121         }
JM 1122         return federationRegistrations;
1123     }
1124
1125     /**
1126      * Retrieve the specified federation registration.
1127      * 
1128      * @param name
1129      *            the name of the registration
1130      * @return a federation registration
1131      */
1132     public FederationModel getFederationRegistration(String url, String name) {
1133         // check registrations
1134         for (FederationModel r : getFederationRegistrations()) {
1135             if (r.name.equals(name) && r.url.equals(url)) {
1136                 return r;
1137             }
1138         }
1139
1140         // check the results
1141         for (FederationModel r : getFederationResultRegistrations()) {
1142             if (r.name.equals(name) && r.url.equals(url)) {
1143                 return r;
1144             }
1145         }
1146         return null;
1147     }
1148
1149     /**
31abc2 1150      * Returns the list of federation sets.
JM 1151      * 
1152      * @return list of federation sets
1153      */
1154     public List<FederationSet> getFederationSets(String gitblitUrl) {
1155         List<FederationSet> list = new ArrayList<FederationSet>();
1156         // generate standard tokens
1157         for (FederationToken type : FederationToken.values()) {
1158             FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
1159             fset.repositories = getRepositories(gitblitUrl, fset.token);
1160             list.add(fset);
1161         }
1162         // generate tokens for federation sets
1163         for (String set : settings.getStrings(Keys.federation.sets)) {
1164             FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
1165                     getFederationToken(set));
1166             fset.repositories = getRepositories(gitblitUrl, fset.token);
1167             list.add(fset);
1168         }
1169         return list;
1170     }
1171
1172     /**
831469 1173      * Returns the list of possible federation tokens for this Gitblit instance.
JM 1174      * 
1175      * @return list of federation tokens
1176      */
1177     public List<String> getFederationTokens() {
1178         List<String> tokens = new ArrayList<String>();
8f73a7 1179         // generate standard tokens
831469 1180         for (FederationToken type : FederationToken.values()) {
JM 1181             tokens.add(getFederationToken(type));
8f73a7 1182         }
JM 1183         // generate tokens for federation sets
1184         for (String set : settings.getStrings(Keys.federation.sets)) {
1185             tokens.add(getFederationToken(set));
831469 1186         }
JM 1187         return tokens;
1188     }
1189
1190     /**
1191      * Returns the specified federation token for this Gitblit instance.
1192      * 
1193      * @param type
1194      * @return a federation token
1195      */
1196     public String getFederationToken(FederationToken type) {
8f73a7 1197         return getFederationToken(type.name());
JM 1198     }
1199
1200     /**
1201      * Returns the specified federation token for this Gitblit instance.
1202      * 
1203      * @param value
1204      * @return a federation token
1205      */
1206     public String getFederationToken(String value) {
2c32fd 1207         String passphrase = settings.getString(Keys.federation.passphrase, "");
8f73a7 1208         return StringUtils.getSHA1(passphrase + "-" + value);
831469 1209     }
JM 1210
1211     /**
1212      * Compares the provided token with this Gitblit instance's tokens and
1213      * determines if the requested permission may be granted to the token.
1214      * 
1215      * @param req
1216      * @param token
1217      * @return true if the request can be executed
1218      */
1219     public boolean validateFederationRequest(FederationRequest req, String token) {
1220         String all = getFederationToken(FederationToken.ALL);
1221         String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
1222         String jur = getFederationToken(FederationToken.REPOSITORIES);
1223         switch (req) {
1224         case PULL_REPOSITORIES:
1225             return token.equals(all) || token.equals(unr) || token.equals(jur);
1226         case PULL_USERS:
fe24a0 1227         case PULL_TEAMS:
831469 1228             return token.equals(all) || token.equals(unr);
JM 1229         case PULL_SETTINGS:
1230             return token.equals(all);
1231         }
1232         return false;
1233     }
1234
1235     /**
1236      * Acknowledge and cache the status of a remote Gitblit instance.
1237      * 
1238      * @param identification
1239      *            the identification of the pulling Gitblit instance
1240      * @param registration
1241      *            the registration from the pulling Gitblit instance
1242      * @return true if acknowledged
1243      */
1244     public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
1245         // reset the url to the identification of the pulling Gitblit instance
1246         registration.url = identification;
1247         String id = identification;
1248         if (!StringUtils.isEmpty(registration.folder)) {
1249             id += "-" + registration.folder;
1250         }
1251         federationPullResults.put(id, registration);
1252         return true;
1253     }
1254
1255     /**
1256      * Returns the list of registration results.
1257      * 
1258      * @return the list of registration results
1259      */
1260     public List<FederationModel> getFederationResultRegistrations() {
1261         return new ArrayList<FederationModel>(federationPullResults.values());
1262     }
1263
1264     /**
1265      * Submit a federation proposal. The proposal is cached locally and the
1266      * Gitblit administrator(s) are notified via email.
1267      * 
1268      * @param proposal
1269      *            the proposal
1270      * @param gitblitUrl
4aafd4 1271      *            the url of your gitblit instance to send an email to
JM 1272      *            administrators
831469 1273      * @return true if the proposal was submitted
JM 1274      */
1275     public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
1276         // convert proposal to json
93f0b1 1277         String json = JsonUtils.toJsonString(proposal);
831469 1278
JM 1279         try {
1280             // make the proposals folder
b774de 1281             File proposalsFolder = getProposalsFolder();
831469 1282             proposalsFolder.mkdirs();
JM 1283
1284             // cache json to a file
1285             File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
1286             com.gitblit.utils.FileUtils.writeContent(file, json);
1287         } catch (Exception e) {
1288             logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
1289         }
1290
1291         // send an email, if possible
1292         try {
1293             Message message = mailExecutor.createMessageForAdministrators();
1294             if (message != null) {
1295                 message.setSubject("Federation proposal from " + proposal.url);
1296                 message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
1297                         + proposal.token);
1298                 mailExecutor.queue(message);
1299             }
1300         } catch (Throwable t) {
1301             logger.error("Failed to notify administrators of proposal", t);
1302         }
1303         return true;
1304     }
1305
1306     /**
1307      * Returns the list of pending federation proposals
1308      * 
1309      * @return list of federation proposals
1310      */
1311     public List<FederationProposal> getPendingFederationProposals() {
1312         List<FederationProposal> list = new ArrayList<FederationProposal>();
b774de 1313         File folder = getProposalsFolder();
831469 1314         if (folder.exists()) {
JM 1315             File[] files = folder.listFiles(new FileFilter() {
1316                 @Override
1317                 public boolean accept(File file) {
1318                     return file.isFile()
1319                             && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
1320                 }
1321             });
1322             for (File file : files) {
1323                 String json = com.gitblit.utils.FileUtils.readContent(file, null);
31abc2 1324                 FederationProposal proposal = JsonUtils.fromJsonString(json,
JM 1325                         FederationProposal.class);
831469 1326                 list.add(proposal);
JM 1327             }
1328         }
1329         return list;
1330     }
1331
1332     /**
dd9ae7 1333      * Get repositories for the specified token.
JM 1334      * 
1335      * @param gitblitUrl
1336      *            the base url of this gitblit instance
1337      * @param token
1338      *            the federation token
1339      * @return a map of <cloneurl, RepositoryModel>
1340      */
1341     public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
1342         Map<String, String> federationSets = new HashMap<String, String>();
1343         for (String set : getStrings(Keys.federation.sets)) {
1344             federationSets.put(getFederationToken(set), set);
1345         }
1346
1347         // Determine the Gitblit clone url
1348         StringBuilder sb = new StringBuilder();
1349         sb.append(gitblitUrl);
1350         sb.append(Constants.GIT_PATH);
1351         sb.append("{0}");
1352         String cloneUrl = sb.toString();
1353
1354         // Retrieve all available repositories
1355         UserModel user = new UserModel(Constants.FEDERATION_USER);
1356         user.canAdmin = true;
1357         List<RepositoryModel> list = getRepositoryModels(user);
1358
1359         // create the [cloneurl, repositoryModel] map
1360         Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
1361         for (RepositoryModel model : list) {
1362             // by default, setup the url for THIS repository
1363             String url = MessageFormat.format(cloneUrl, model.name);
1364             switch (model.federationStrategy) {
1365             case EXCLUDE:
1366                 // skip this repository
1367                 continue;
1368             case FEDERATE_ORIGIN:
1369                 // federate the origin, if it is defined
1370                 if (!StringUtils.isEmpty(model.origin)) {
1371                     url = model.origin;
1372                 }
1373                 break;
1374             }
1375
1376             if (federationSets.containsKey(token)) {
1377                 // include repositories only for federation set
1378                 String set = federationSets.get(token);
1379                 if (model.federationSets.contains(set)) {
1380                     repositories.put(url, model);
1381                 }
1382             } else {
1383                 // standard federation token for ALL
1384                 repositories.put(url, model);
1385             }
1386         }
1387         return repositories;
1388     }
1389
1390     /**
1391      * Creates a proposal from the token.
1392      * 
1393      * @param gitblitUrl
1394      *            the url of this Gitblit instance
1395      * @param token
1396      * @return a potential proposal
1397      */
1398     public FederationProposal createFederationProposal(String gitblitUrl, String token) {
1399         FederationToken tokenType = FederationToken.REPOSITORIES;
1400         for (FederationToken type : FederationToken.values()) {
1401             if (token.equals(getFederationToken(type))) {
1402                 tokenType = type;
1403                 break;
1404             }
1405         }
1406         Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
1407         FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
1408                 repositories);
1409         return proposal;
1410     }
1411
1412     /**
831469 1413      * Returns the proposal identified by the supplied token.
JM 1414      * 
1415      * @param token
1416      * @return the specified proposal or null
1417      */
1418     public FederationProposal getPendingFederationProposal(String token) {
1419         List<FederationProposal> list = getPendingFederationProposals();
1420         for (FederationProposal proposal : list) {
1421             if (proposal.token.equals(token)) {
1422                 return proposal;
1423             }
1424         }
1425         return null;
1426     }
1427
1428     /**
1429      * Deletes a pending federation proposal.
1430      * 
1431      * @param a
1432      *            proposal
1433      * @return true if the proposal was deleted
1434      */
1435     public boolean deletePendingFederationProposal(FederationProposal proposal) {
b774de 1436         File folder = getProposalsFolder();
831469 1437         File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
JM 1438         return file.delete();
1439     }
1440
1441     /**
6cc1d4 1442      * Returns the list of all available Groovy push hook scripts that are not
JM 1443      * already specified globally for all repositories. Script files must have
1444      * .groovy extension
1445      * 
1446      * @return list of available hook scripts
1447      */
1448     public List<String> getAvailableScripts() {
1449         File groovyFolder = getGroovyScriptsFolder();
1450         File[] files = groovyFolder.listFiles(new FileFilter() {
1451             @Override
1452             public boolean accept(File pathname) {
1453                 return pathname.isFile() && pathname.getName().endsWith(".groovy");
1454             }
1455         });
1456
1457         Set<String> globals = new HashSet<String>();
1458         String[] keys = { Keys.groovy.preReceiveScripts, Keys.groovy.postReceiveScripts };
1459         for (String key : keys) {
1460             for (String script : getStrings(key)) {
1461                 if (script.endsWith(".groovy")) {
1462                     globals.add(script.substring(0, script.lastIndexOf('.')));
1463                 } else {
1464                     globals.add(script);
1465                 }
1466             }
1467         }
1468
1469         // create list of available scripts by excluding scripts that are
1470         // globally specified
1471         List<String> scripts = new ArrayList<String>();
1472         if (files != null) {
1473             for (File file : files) {
1474                 String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
1475                 if (!globals.contains(script)) {
1476                     scripts.add(script);
1477                 }
1478             }
1479         }
1480         return scripts;
1481     }
31bcbe 1482     
0d013a 1483     public List<String> getInheritedPreReceiveScripts(RepositoryModel repository) {
JM 1484         Set<String> globals = new HashSet<String>();
1485         for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
1486             if (script.endsWith(".groovy")) {
1487                 globals.add(script.substring(0, script.lastIndexOf('.')));
1488             } else {
1489                 globals.add(script);
1490             }
1491         }
1492         return new ArrayList<String>(globals);
1493     }
1494     
1495     public List<String> getInheritedPostReceiveScripts(RepositoryModel repository) {
1496         Set<String> globals = new HashSet<String>();
1497         for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
1498             if (script.endsWith(".groovy")) {
1499                 globals.add(script.substring(0, script.lastIndexOf('.')));
1500             } else {
1501                 globals.add(script);
1502             }
1503         }
1504         return new ArrayList<String>(globals);
1505     }
1506
6cc1d4 1507     /**
831469 1508      * Notify the administrators by email.
JM 1509      * 
1510      * @param subject
1511      * @param message
1512      */
eb96ea 1513     public void sendMailToAdministrators(String subject, String message) {
831469 1514         try {
JM 1515             Message mail = mailExecutor.createMessageForAdministrators();
1516             if (mail != null) {
1517                 mail.setSubject(subject);
1518                 mail.setText(message);
1519                 mailExecutor.queue(mail);
1520             }
1521         } catch (MessagingException e) {
1522             logger.error("Messaging error", e);
1523         }
1524     }
1525
1526     /**
fa54be 1527      * Notify users by email of something.
JM 1528      * 
1529      * @param subject
1530      * @param message
1531      * @param toAddresses
1532      */
0b9119 1533     public void sendMail(String subject, String message, Collection<String> toAddresses) {
eb96ea 1534         this.sendMail(subject, message, toAddresses.toArray(new String[0]));
fa54be 1535     }
JM 1536
1537     /**
1538      * Notify users by email of something.
1539      * 
1540      * @param subject
1541      * @param message
1542      * @param toAddresses
1543      */
eb96ea 1544     public void sendMail(String subject, String message, String... toAddresses) {
fa54be 1545         try {
JM 1546             Message mail = mailExecutor.createMessage(toAddresses);
1547             if (mail != null) {
1548                 mail.setSubject(subject);
1549                 mail.setText(message);
1550                 mailExecutor.queue(mail);
1551             }
1552         } catch (MessagingException e) {
1553             logger.error("Messaging error", e);
1554         }
1555     }
1556
1557     /**
b75734 1558      * Returns the descriptions/comments of the Gitblit config settings.
JM 1559      * 
84c1d5 1560      * @return SettingsModel
b75734 1561      */
84c1d5 1562     public ServerSettings getSettingsModel() {
b75734 1563         // ensure that the current values are updated in the setting models
773bb6 1564         for (String key : settings.getAllKeys(null)) {
JM 1565             SettingModel setting = settingsModel.get(key);
1566             if (setting != null) {
1567                 setting.currentValue = settings.getString(key, "");
1568             }
1569         }
6cc1d4 1570         settingsModel.pushScripts = getAvailableScripts();
84c1d5 1571         return settingsModel;
b75734 1572     }
JM 1573
1574     /**
1575      * Parse the properties file and aggregate all the comments by the setting
1576      * key. A setting model tracks the current value, the default value, the
1577      * description of the setting and and directives about the setting.
1578      * 
1579      * @return Map<String, SettingModel>
1580      */
84c1d5 1581     private ServerSettings loadSettingModels() {
JM 1582         ServerSettings settingsModel = new ServerSettings();
b75734 1583         try {
JM 1584             // Read bundled Gitblit properties to extract setting descriptions.
1585             // This copy is pristine and only used for populating the setting
1586             // models map.
97a20e 1587             InputStream is = servletContext.getResourceAsStream("/WEB-INF/reference.properties");
b75734 1588             BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
JM 1589             StringBuilder description = new StringBuilder();
1590             SettingModel setting = new SettingModel();
1591             String line = null;
1592             while ((line = propertiesReader.readLine()) != null) {
1593                 if (line.length() == 0) {
1594                     description.setLength(0);
1595                     setting = new SettingModel();
1596                 } else {
1597                     if (line.charAt(0) == '#') {
1598                         if (line.length() > 1) {
1599                             String text = line.substring(1).trim();
1600                             if (SettingModel.CASE_SENSITIVE.equals(text)) {
1601                                 setting.caseSensitive = true;
1602                             } else if (SettingModel.RESTART_REQUIRED.equals(text)) {
1603                                 setting.restartRequired = true;
1604                             } else if (SettingModel.SPACE_DELIMITED.equals(text)) {
1605                                 setting.spaceDelimited = true;
1606                             } else if (text.startsWith(SettingModel.SINCE)) {
1607                                 try {
1608                                     setting.since = text.split(" ")[1];
1609                                 } catch (Exception e) {
1610                                     setting.since = text;
1611                                 }
1612                             } else {
1613                                 description.append(text);
1614                                 description.append('\n');
1615                             }
1616                         }
1617                     } else {
1618                         String[] kvp = line.split("=", 2);
1619                         String key = kvp[0].trim();
1620                         setting.name = key;
1621                         setting.defaultValue = kvp[1].trim();
1622                         setting.currentValue = setting.defaultValue;
1623                         setting.description = description.toString().trim();
84c1d5 1624                         settingsModel.add(setting);
b75734 1625                         description.setLength(0);
JM 1626                         setting = new SettingModel();
1627                     }
1628                 }
1629             }
1630             propertiesReader.close();
1631         } catch (NullPointerException e) {
1632             logger.error("Failed to find resource copy of gitblit.properties");
1633         } catch (IOException e) {
1634             logger.error("Failed to load resource copy of gitblit.properties");
1635         }
84c1d5 1636         return settingsModel;
b75734 1637     }
JM 1638
1639     /**
892570 1640      * Configure the Gitblit singleton with the specified settings source. This
JM 1641      * source may be file settings (Gitblit GO) or may be web.xml settings
1642      * (Gitblit WAR).
1643      * 
1644      * @param settings
1645      */
f6740d 1646     public void configureContext(IStoredSettings settings, boolean startFederation) {
1f9dae 1647         logger.info("Reading configuration from " + settings.toString());
892570 1648         this.settings = settings;
b774de 1649         repositoriesFolder = getRepositoriesFolder();
5450d0 1650         logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
166e6a 1651         repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
486ee1 1652         serverStatus = new ServerStatus(isGO());
85c2e6 1653         String realm = settings.getString(Keys.realm.userService, "users.properties");
JM 1654         IUserService loginService = null;
5450d0 1655         try {
892570 1656             // check to see if this "file" is a login service class
5450d0 1657             Class<?> realmClass = Class.forName(realm);
85c2e6 1658             if (IUserService.class.isAssignableFrom(realmClass)) {
JM 1659                 loginService = (IUserService) realmClass.newInstance();
5450d0 1660             }
JM 1661         } catch (Throwable t) {
eb96ea 1662             loginService = new GitblitUserService();
5450d0 1663         }
85c2e6 1664         setUserService(loginService);
831469 1665         mailExecutor = new MailExecutor(settings);
JM 1666         if (mailExecutor.isReady()) {
1667             scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
1668         } else {
1669             logger.warn("Mail server is not properly configured.  Mail services disabled.");
f6740d 1670         }
JM 1671         if (startFederation) {
1672             configureFederation();
831469 1673         }
87cc1e 1674     }
JM 1675
892570 1676     /**
JM 1677      * Configure Gitblit from the web.xml, if no configuration has already been
1678      * specified.
1679      * 
1680      * @see ServletContextListener.contextInitialize(ServletContextEvent)
1681      */
87cc1e 1682     @Override
JM 1683     public void contextInitialized(ServletContextEvent contextEvent) {
b75734 1684         servletContext = contextEvent.getServletContext();
773bb6 1685         settingsModel = loadSettingModels();
892570 1686         if (settings == null) {
JM 1687             // Gitblit WAR is running in a servlet container
b774de 1688             ServletContext context = contextEvent.getServletContext();
JM 1689             WebXmlSettings webxmlSettings = new WebXmlSettings(context);
1690
1691             // 0.7.0 web.properties in the deployed war folder
1692             File overrideFile = new File(context.getRealPath("/WEB-INF/web.properties"));
1693             if (overrideFile.exists()) {
1694                 webxmlSettings.applyOverrides(overrideFile);
1695             }
1696
1697             // 0.8.0 gitblit.properties file located outside the deployed war
1698             // folder lie, for example, on RedHat OpenShift.
1699             overrideFile = getFileOrFolder("gitblit.properties");
1700             if (!overrideFile.getPath().equals("gitblit.properties")) {
1701                 webxmlSettings.applyOverrides(overrideFile);
1702             }
f6740d 1703             configureContext(webxmlSettings, true);
87cc1e 1704         }
773bb6 1705
486ee1 1706         serverStatus.servletContainer = servletContext.getServerInfo();
87cc1e 1707     }
JM 1708
892570 1709     /**
JM 1710      * Gitblit is being shutdown either because the servlet container is
1711      * shutting down or because the servlet container is re-deploying Gitblit.
1712      */
87cc1e 1713     @Override
JM 1714     public void contextDestroyed(ServletContextEvent contextEvent) {
f339f5 1715         logger.info("Gitblit context destroyed by servlet container.");
831469 1716         scheduledExecutor.shutdownNow();
87cc1e 1717     }
fc948c 1718 }