sonarqube and the scm plugin

I tried enabling the SCM integration from our CodeRanch Sonar install to our SVN repository. It didn’t quite work out the way I was hoping. But I learned stuff so it makes for a good blog entry.

Wait? The SCM Activity Plugin is deprecated?

If you search for Sonar SCM plugin, you get to the plugin documentation page. Which appears to be useful. Except that it says the plugin has been deprecated since Sonar 5.0. I stopped reading at that point and puzzled over it. If you continue to read, it says “This plugin is deprecated since SonarQube 5.0 which has built-in support for SCM information and which relies on independent plugins to cover SCM providers.” Ah. So it is deprecated because it is now a core feature. That explains why the functionality shows up on our Sonar.

Time to enable

I generated another SVN user and set the username/password in Sonar. I then set “disable the SCM sensor” to false and ran our Jenkins build. And I got this. Well, it went on for pages, but you get the idea:

[sonar:sonar] Sensor SCM Sensor
[sonar:sonar] SCM provider for this project is: svn
[sonar:sonar] 1404 files to be analyzed
[sonar:sonar] 0/1404 files analyzed
[sonar:sonar] Missing blame information for the following files:
[sonar:sonar] * /data/vhost1/ciengine.javaranch.com/data/jobs/JForum/workspace/src/main/java/net/jforum/util/bbcode/BBCodeHandler.java
[sonar:sonar] * /data/vhost1/ciengine.javaranch.com/data/jobs/JForum/workspace/src/main/java/net/jforum/util/concurrent/Executor.java

Ok. So the credentials are good as Sonar can connect to see how many files there are.  Google and SVN plugin page both suggested the problem was not being on Subversion 1.5 or lower. I looked around and that didn’t appear to be the case.  svnadmin —version returned 1.7.8 and the repository was on 1.6. So I thought maybe the mismatch was the problem and tried to run svnadmin upgrade svn (svn is the name of the folder containing our repository on disk). Then this happened and I had to recover from it.

Then it worked

After I upgraded the Subversion repository (or whatever the hell I did by accident), I was able to commit and trigger another Jenkins/Sonar build. I saw the person who committed lines of code in each file. And what’s really cool is that it WENT BACK IN TIME. I see the committers that aren’t active on CodeRanch anymore and haven’t contributed code in years. That is really cool.

What I was hoping it would do vs what it actually does

I wanted to see the “scm blame” output when reviewing files in Sonar. That works and I see it as being useful. I can see if something came from me (or another current developer.) Or if it came from someone no longer involved in the code base.

I wanted to see coverage on new code. This works depending on how you define new code. If you define it as “touched” code, it works well. You also have to remember to use a differential view so Sonar knows how far back to consider “new” code. This makes sense. I typically use the last 7 days as my view so “new” means “touched in the last 7 days”. Unfortunately, we don’t currently run our integration tests in Jenkins so this isn’t useful for the back end layer. I had started dealing with that. Maybe I should finish :).

I had read about the “Developer Cockpit” and seeing a view of your own contributions. I hadn’t realized that was a paid enterprise feature so not helpful to us at CodeRanch.  Without the cockpit, you can still go to “My account” to see a table with “leaks” (issues you created in the past week) and a link to all of your issues. Unfortunately Sonar thinks half the issues are mine and the others aren’t assigned:

  1. I was the one who committed the initial JForum fork we made which means every single open source issue is treated as mine.  (I don’t remember if I did or not, but I led that project so I wouldn’t be surprised.)
  2. Most of the developers don’t have accounts on Sonar. Only the admins do. Everyone else has been using it anonymously. (It’s behind an Apache password wall so not public.)

On the bright side, you *can* filter issues by author now. Which is the last committer. But still helpful. If the code is new, the last committer is someone who might look at it. If the committer is someone who isn’t around anymore, that’s informative.

 

 

 

 

upgrading a svn repository and recovering

While trying to integrate Sonar with Subversion, I had a suspicion that it wasn’t working because of either the version of the repository or a failed upgrade in the past. So on a whim*, I ran the upgrade command:

-bash-4.1$ svnadmin upgrade svn
Repository lock acquired.
Please wait; upgrading the repository may take some time…

Upgrade completed.

svn is the directory with our repository on CodeRanch. This happened almost instantaneously so I figured it was on the latest and nothing happened. Well, something happened. The permissions changed on a few files in the svn directory. I got this message:

Some of selected resources were not committed.
svn: E204900: Commit failed (details follow):
svn: E204900: Can’t open file ‘/home/vhost1/svn.javaranch.com/svn/db/txn-current-lock’: Permission denied
svn: E175002: MKACTIVITY of ‘/svn/!svn/act/bc61d1cc-5501-0010-ba7e-45a47496be2e’: 500 Internal Server Error (https://svn.javaranch.com)

Uh oh. I was able to commit before I started. Long story short, I fixed it by iterating through the errors and wound up just having to run:

chmod g+w txn-current*
 chmod g+w txn-protorevs

I tested by pulling, committing, tagging and deleting a tag. Everything seems to work.

Disclaimer

* If you work at a company, please don’t upgrade things on a whim nor allow your staff to do. I would never do that at my real job. CodeRanch operates more like the wild, wild west. Which is appropriate given the name and the fact that everything is volunteer driven and the culture is one that allows for some risk. I was able to fix this in about 15 minutes so no harm done. But if you are being paid to do something or can’t tolerate the risk, be careful. Do things on a development server. Plan. Tell people. Then these unpleasant moments where you realize you’ve made it so nobody can commit don’t happen.

refactoring legacy code for java 8

I asked a teammate at CodeRanch if I could use a method he wrote to show how it could be written more clearly/succinctly in Java 8. He said yes. This is the original method. It is 95 lines and has a cyclomatic complexity of 27.

public static Map<Category, List<Forum>> getBetaTopForums(BestTopicsDAO bestTopicDAO, int userId, List<Topic> bestThisYear, List<Topic> secondaryBestList){
        Map<Integer, Integer> ratings = bestTopicDAO.selectForumRatingsByUserId(userId);
        
        List<Integer> forumIds = new ArrayList<>();
        List<Integer> ratedForumIds = CollectionCommon.getKeysSortedByValues(ratings, true);
        final int defaultUserId = SystemGlobals.getIntValue(ConfigKeys.BEST_TOPICS_DEFAULT_USER_ID);
        
        // Let's first add the forums that are rated above 5/10
        for(Integer forumId: ratedForumIds){
            Integer r = ratings.get(forumId);
            if(r != null && r > 5){
                forumIds.add(forumId);
            }
        }
        
        // Now let's add the forums found in best topics.
        if(bestThisYear != null){
            for(Topic topic: bestThisYear){
                int forumId = topic.getForumId();
                if(!forumIds.contains(forumId)){
                    forumIds.add(forumId);
                }
            }
        }
        if(secondaryBestList != null){
            for(Topic topic: secondaryBestList){
                int forumId = topic.getForumId();
                if(!forumIds.contains(forumId)){
                    forumIds.add(forumId);
                }
            }
        }
        
        // If this user is not the default-rating-user, let's add the top forums
        // rated by the default-rating-user.
        if(userId != defaultUserId) {
            Collection<Integer> fids = getTopRatedForumIds(bestTopicDAO);
            for(Integer fid: fids) {
                if(fid != null && !forumIds.contains(fid)) {
                    forumIds.add(fid);
                }
            }
        }
        
        
        // Let's also add some forums found in hot topics.
        List<Topic> tmpTopics = TopicRepository.getHottestTopics();
        for(Topic topic: tmpTopics){
            int forumId = topic.getForumId();
            if(!forumIds.contains(forumId)){
                forumIds.add(forumId);
            }
        }
        
        // Finally, let's add the forums that were rated below 6/10
        for(Integer forumId: ratedForumIds){
            Integer r = ratings.get(forumId);
            if(r != null && r < 6 && !forumIds.contains(forumId)){
                forumIds.add(forumId);
            }
        }
        
        Map<Category, List<Forum>> forums = new LinkedHashMap<>();
        
        PermissionControl pc = SecurityRepository.get(userId);
        
        final int maxTopForums = SystemGlobals.getIntValue(ConfigKeys.BETA_VIEW_MAX_TOP_FORUMS_IN_HOME);
        int forumsToShow = 0;
        
        // We need to remove the forums that are not visible, or rated 0/10
        for(Integer forumId: forumIds){
            if(pc.canAccess(SecurityConstants.PERM_FORUM, String.valueOf(forumId))){
                Forum f = ForumRepository.getForum(forumId);
                Category c = f != null ? ForumRepository.getCategory(f.getCategoryId()) : null;
                if(f != null && c != null && f.isShowInForumList()){
                    
                    if(++forumsToShow > maxTopForums){
                        break;
                    }
                    
                    List<Forum> fs;
                    if(forums.containsKey(c)){
                        fs = forums.get(c);
                    }else{
                        fs = new ArrayList<>();
                        forums.put(c, fs);
                    }
                    fs.add(f);
                }
            }
        }
        return forums;
        
    }

First step – write unit tests

First, I wrote unit tests for the existing code to ensure I didn’t break anything. While doing that, I did some minor refactorings. (I didn’t do extract method to make the method shorter since I knew I’d be updating that later). What I did change:

  1. Removed BestTopicsDAO as a parameter. It comes from a factory and we have a mock framework setup for that factory already. So there was no reason it had to be a parameter
  2. Renamed bestThisYear to primaryBestList. (It doesn’t always represent the best for the year)

I couldn’t get 100% test coverage because a few of the null checks were unreachable due to early logic (in the helper methods.) I left them in to see the effect they’d have on conversion.

Refactoring to Java 8 – Getting started

There’s a lot of for loops here. And I suspect they are very similar. Let’s start with the first one:

for(Integer forumId: ratedForumIds){
  Integer r = ratings.get(forumId);
  if(r != null && r > 5){
    forumIds.add(forumId);
  }
}

I converted this to a helper method and a stream:

forumIds.addAll(ratedForumIds.stream().filter(id -> {
  Integer r = ratings.get(id);
  return r != null && r > 5;
}).collect(Collectors.toList()));

I’ll grant you that this code isn’t shorter than the original code. It is more similar in form to the code I expect to write next though.  Also, it’s not easier to read given I didn’t extract the two line lambda. I’m going to do that after I get rid of the similar for loops.

The next refactoring

The next one is interesting. It’s just a for loop and if statement which is easy to rewrite.

for(Topic topic: primaryBestList){
  int forumId = topic.getForumId();
  if(!forumIds.contains(forumId)){
    forumIds.add(forumId);
  }
}

which is equivalent to

forumIds.addAll(primaryBestList.stream()
  .map(Topic::getForumId)
  .filter(f -> ! forumIds.contains(f))
  .collect(Collectors.toList()));

However, it is interesting because it check if the forum is already in forumIds. This shouldn’t be necessary as it is something a data structure could do. We need a data structure that is both a list (preserves order) and a set (checks for uniqueness.) Enter LinkedHashSet. It’s a set and preserves the insertion order. The JavaDoc even specifies that if you add the same element again, the order doesn’t change. Perfect. Switching forumIds to a LinkedHashSet allows for getting rid of the filter intermediate operation. And the unit tests still work.  I used the same techniques for the next five for loops.

Time to use methods

I converted two methods that had to do with ratings and three that were straight conversions.  I could have one common method they all use with identities for the filter/map that don’t apply. Two methods would be clearer. Giving me:

private static void addForumIds(Collection<Integer> forumIds, List<Topic> candidates) {
  if (candidates != null) {
    List<Integer> toAdd = candidates.stream().map(Topic::getForumId).collect(Collectors.toList());
    forumIds.addAll(toAdd);
  }
}

private static void addForumIds(Collection<Integer> forumIds, Collection<Integer> candidates, Predicate<Integer> predicate) {
  if (candidates != null) {
    List<Integer> toAdd = candidates.stream().filter(predicate).collect(Collectors.toList());
    forumIds.addAll(toAdd);
  }
}

The last one

The last chunk of code needed more refactoring to take care of all the forum filtering logic first. It also needed a mutable object for the counter rather than a primitive so it could be updated inside a lambda.

Conclusion

I could refactor this more. There’s duplication in the two lambdas. And I could extract more code into separate methods. Interestingly, the total code base is about the same as before. (It’s actually three lines longer, but I have more than three lines of comments.) But the method with the main logic is shorter and far less complicated:

public static Map<Category, List<Forum>> getBetaTopForums(int userId, List<Topic> primaryBestList, List<Topic> secondaryBestList){
        BestTopicsDAO bestTopicDAO = DataAccessDriver.newBestTopicDAO();
        Map<Integer, Integer> ratings = bestTopicDAO.selectForumRatingsByUserId(userId);
        
        Collection<Integer> forumIds = new LinkedHashSet<>();
        List<Integer> ratedForumIds = CollectionCommon.getKeysSortedByValues(ratings, true);
        final int defaultUserId = SystemGlobals.getIntValue(ConfigKeys.BEST_TOPICS_DEFAULT_USER_ID);
        
        // Let's first add the forums that are rated above 5/10
         addForumIds(forumIds, ratedForumIds, id -> {
            Integer r = ratings.get(id);
            return r != null && r > 5;
        });
        
        // Now let's add the forums found in best topics.
        addForumIds(forumIds, primaryBestList);
        addForumIds(forumIds, secondaryBestList);
          
        // If this user is not the default-rating-user, let's add the top forums
        // rated by the default-rating-user.
        if(userId != defaultUserId) {
            Collection<Integer> fids = getTopRatedForumIds(bestTopicDAO);
            addForumIds(forumIds, fids, id -> id != null);
        }
        
        // Let's also add some forums found in hot topics.
        List<Topic> tmpTopics = TopicRepository.getHottestTopics();
        addForumIds(forumIds, tmpTopics);

        // Finally, let's add the forums that were rated below 6/10
       addForumIds(forumIds, ratedForumIds, id -> {
            Integer r = ratings.get(id);
            return r != null && r < 6;
        });
        
        Map<Category, List<Forum>> forums = new LinkedHashMap<>();
        
        PermissionControl pc = SecurityRepository.get(userId);
        
        final int maxTopForums = SystemGlobals.getIntValue(ConfigKeys.BETA_VIEW_MAX_TOP_FORUMS_IN_HOME);
        AtomicInteger countForumsToShow = new AtomicInteger(0);
        
        // We need to remove the forums that are not visible, or rated 0/10
        forumIds.stream()
             // skip if can't access forum
            .filter(id -> pc.canAccess(SecurityConstants.PERM_FORUM, String.valueOf(id)))
            // get forum
            .map(ForumRepository::getForum)
            // skip if forum not found or can't access
            .filter(f -> f != null && f.isShowInForumList())
            // get category
            .forEach(f -> addForumToCategory(forums, f, maxTopForums, countForumsToShow));
        
        return forums;
        
    }
    
    private static void addForumToCategory(Map<Category, List<Forum>> forums, Forum f, int maxTopForums,
            AtomicInteger countForumsToShow) {
        Category c = ForumRepository.getCategory(f.getCategoryId());
        if (c != null && countForumsToShow.incrementAndGet() <= maxTopForums) {
            List<Forum> fs;
            if (forums.containsKey(c)) {
                fs = forums.get(c);
            } else {
                fs = new ArrayList<>();
                forums.put(c, fs);
            }
            fs.add(f);
        }
    }

    private static void addForumIds(Collection<Integer> forumIds, List<Topic> candidates) {
        if (candidates != null) {
            List<Integer> toAdd = candidates.stream().map(Topic::getForumId).collect(Collectors.toList());
            forumIds.addAll(toAdd);
        }
    }
    
    private static void addForumIds(Collection<Integer> forumIds, Collection<Integer> candidates, Predicate<Integer> predicate) {
        if (candidates != null) {
            List<Integer> toAdd = candidates.stream().filter(predicate).collect(Collectors.toList());
            forumIds.addAll(toAdd);
        }    
    }