001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2014 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * SonarQube is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * SonarQube is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public License
017     * along with this program; if not, write to the Free Software Foundation,
018     * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019     */
020    package org.sonar.batch.index;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import org.apache.commons.lang.StringUtils;
024    import org.slf4j.Logger;
025    import org.slf4j.LoggerFactory;
026    import org.sonar.api.BatchComponent;
027    import org.sonar.api.batch.fs.FileSystem;
028    import org.sonar.api.batch.fs.InputFile;
029    import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile;
030    import org.sonar.api.database.DatabaseSession;
031    import org.sonar.api.database.model.ResourceModel;
032    import org.sonar.api.resources.Directory;
033    import org.sonar.api.resources.File;
034    import org.sonar.api.resources.Project;
035    import org.sonar.api.resources.Qualifiers;
036    import org.sonar.api.resources.Resource;
037    import org.sonar.api.resources.Scopes;
038    import org.sonar.api.utils.PathUtils;
039    import org.sonar.batch.util.DeprecatedKeyUtils;
040    
041    import java.util.HashMap;
042    import java.util.List;
043    import java.util.Map;
044    
045    public class ResourceKeyMigration implements BatchComponent {
046    
047      private static final String UNABLE_TO_UPDATE_COMPONENT_NO_MATCH_WAS_FOUND = "Unable to update component {}. No match was found.";
048      private static final String COMPONENT_CHANGED_TO = "Component {} changed to {}";
049      private final Logger logger;
050      private final DatabaseSession session;
051    
052      private boolean migrationNeeded = false;
053    
054      public ResourceKeyMigration(DatabaseSession session) {
055        this(session, LoggerFactory.getLogger(ResourceKeyMigration.class));
056      }
057    
058      @VisibleForTesting
059      ResourceKeyMigration(DatabaseSession session, Logger logger) {
060        this.session = session;
061        this.logger = logger;
062      }
063    
064      public void checkIfMigrationNeeded(Project rootProject) {
065        ResourceModel model = session.getSingleResult(ResourceModel.class, "key", rootProject.getEffectiveKey());
066        if (model != null && StringUtils.isBlank(model.getDeprecatedKey())) {
067          this.migrationNeeded = true;
068        }
069      }
070    
071      public void migrateIfNeeded(Project module, FileSystem fs) {
072        if (migrationNeeded) {
073          migrateIfNeeded(module, fs.inputFiles(fs.predicates().all()));
074        }
075      }
076    
077      void migrateIfNeeded(Project module, Iterable<InputFile> inputFiles) {
078        logger.info("Update component keys");
079        Map<String, InputFile> deprecatedFileKeyMapper = new HashMap<String, InputFile>();
080        Map<String, InputFile> deprecatedTestKeyMapper = new HashMap<String, InputFile>();
081        Map<String, String> deprecatedDirectoryKeyMapper = new HashMap<String, String>();
082        for (InputFile inputFile : inputFiles) {
083          String deprecatedKey = ((DeprecatedDefaultInputFile) inputFile).deprecatedKey();
084          if (deprecatedKey != null) {
085            if (InputFile.Type.TEST == inputFile.type() && !deprecatedTestKeyMapper.containsKey(deprecatedKey)) {
086              deprecatedTestKeyMapper.put(deprecatedKey, inputFile);
087            } else if (InputFile.Type.MAIN == inputFile.type() && !deprecatedFileKeyMapper.containsKey(deprecatedKey)) {
088              deprecatedFileKeyMapper.put(deprecatedKey, inputFile);
089            }
090          }
091        }
092    
093        ResourceModel moduleModel = session.getSingleResult(ResourceModel.class, "key", module.getEffectiveKey());
094        int moduleId = moduleModel.getId();
095        migrateFiles(module, deprecatedFileKeyMapper, deprecatedTestKeyMapper, deprecatedDirectoryKeyMapper, moduleId);
096        migrateDirectories(deprecatedDirectoryKeyMapper, moduleId);
097        session.commit();
098      }
099    
100      private void migrateFiles(Project module, Map<String, InputFile> deprecatedFileKeyMapper, Map<String, InputFile> deprecatedTestKeyMapper,
101        Map<String, String> deprecatedDirectoryKeyMapper,
102        int moduleId) {
103        // Find all FIL or CLA resources for this module
104        StringBuilder hql = newResourceQuery()
105          .append(" and scope = '").append(Scopes.FILE).append("' order by qualifier, key");
106        Map<String, ResourceModel> disabledResourceByKey = loadDisabledResources(moduleId, hql);
107        List<ResourceModel> resources = loadEnabledResources(moduleId, hql);
108        for (ResourceModel resourceModel : resources) {
109          String oldEffectiveKey = resourceModel.getKey();
110          boolean isTest = Qualifiers.UNIT_TEST_FILE.equals(resourceModel.getQualifier());
111          InputFile matchedFile = findInputFile(deprecatedFileKeyMapper, deprecatedTestKeyMapper, oldEffectiveKey, isTest);
112          if (matchedFile != null) {
113            String newEffectiveKey = ((DeprecatedDefaultInputFile) matchedFile).key();
114            // Now compute migration of the parent dir
115            String oldKey = StringUtils.substringAfterLast(oldEffectiveKey, ":");
116            Resource sonarFile;
117            String parentOldKey;
118            if ("java".equals(resourceModel.getLanguageKey())) {
119              parentOldKey = String.format("%s:%s", module.getEffectiveKey(), DeprecatedKeyUtils.getJavaFileParentDeprecatedKey(oldKey));
120            } else {
121              sonarFile = new File(oldKey);
122              parentOldKey = String.format("%s:%s", module.getEffectiveKey(), sonarFile.getParent().getDeprecatedKey());
123            }
124            String parentNewKey = String.format("%s:%s", module.getEffectiveKey(), getParentKey(matchedFile));
125            if (!deprecatedDirectoryKeyMapper.containsKey(parentOldKey)) {
126              deprecatedDirectoryKeyMapper.put(parentOldKey, parentNewKey);
127            } else if (!parentNewKey.equals(deprecatedDirectoryKeyMapper.get(parentOldKey))) {
128              logger.warn("Directory with key " + parentOldKey + " matches both " + deprecatedDirectoryKeyMapper.get(parentOldKey) + " and "
129                + parentNewKey + ". First match is arbitrary chosen.");
130            }
131            updateKey(resourceModel, newEffectiveKey, disabledResourceByKey);
132            resourceModel.setDeprecatedKey(oldEffectiveKey);
133            logger.info(COMPONENT_CHANGED_TO, oldEffectiveKey, newEffectiveKey);
134          } else {
135            logger.warn(UNABLE_TO_UPDATE_COMPONENT_NO_MATCH_WAS_FOUND, oldEffectiveKey);
136          }
137        }
138      }
139    
140      private void updateKey(ResourceModel resourceModel, String newEffectiveKey, Map<String, ResourceModel> disabledResourceByKey) {
141        // Look for disabled resource with conflicting key
142        if (disabledResourceByKey.containsKey(newEffectiveKey)) {
143          ResourceModel duplicateDisabledResource = disabledResourceByKey.get(newEffectiveKey);
144          String disabledKey = newEffectiveKey + "_renamed_by_resource_key_migration";
145          duplicateDisabledResource.setKey(disabledKey);
146          logger.info(COMPONENT_CHANGED_TO, newEffectiveKey, disabledKey);
147        }
148        resourceModel.setKey(newEffectiveKey);
149      }
150    
151      private StringBuilder newResourceQuery() {
152        return new StringBuilder().append("from ")
153          .append(ResourceModel.class.getSimpleName())
154          .append(" where enabled = :enabled")
155          .append(" and rootId = :rootId ");
156      }
157    
158      private InputFile findInputFile(Map<String, InputFile> deprecatedFileKeyMapper, Map<String, InputFile> deprecatedTestKeyMapper, String oldEffectiveKey, boolean isTest) {
159        if (isTest) {
160          return deprecatedTestKeyMapper.get(oldEffectiveKey);
161        } else {
162          return deprecatedFileKeyMapper.get(oldEffectiveKey);
163        }
164      }
165    
166      private void migrateDirectories(Map<String, String> deprecatedDirectoryKeyMapper, int moduleId) {
167        // Find all DIR resources for this module
168        StringBuilder hql = newResourceQuery()
169          .append(" and qualifier = '").append(Qualifiers.DIRECTORY).append("'");
170        Map<String, ResourceModel> disabledResourceByKey = loadDisabledResources(moduleId, hql);
171        List<ResourceModel> resources = loadEnabledResources(moduleId, hql);
172        for (ResourceModel resourceModel : resources) {
173          String oldEffectiveKey = resourceModel.getKey();
174          if (deprecatedDirectoryKeyMapper.containsKey(oldEffectiveKey)) {
175            String newEffectiveKey = deprecatedDirectoryKeyMapper.get(oldEffectiveKey);
176            updateKey(resourceModel, newEffectiveKey, disabledResourceByKey);
177            resourceModel.setDeprecatedKey(oldEffectiveKey);
178            logger.info(COMPONENT_CHANGED_TO, oldEffectiveKey, newEffectiveKey);
179          } else {
180            logger.warn(UNABLE_TO_UPDATE_COMPONENT_NO_MATCH_WAS_FOUND, oldEffectiveKey);
181          }
182        }
183      }
184    
185      private List<ResourceModel> loadEnabledResources(int moduleId, StringBuilder hql) {
186        return session.createQuery(hql.toString())
187          .setParameter("rootId", moduleId)
188          .setParameter("enabled", true)
189          .getResultList();
190      }
191    
192      private Map<String, ResourceModel> loadDisabledResources(int moduleId, StringBuilder hql) {
193        List<ResourceModel> disabledResources = session.createQuery(hql.toString())
194          .setParameter("rootId", moduleId)
195          .setParameter("enabled", false)
196          .getResultList();
197        Map<String, ResourceModel> disabledResourceByKey = new HashMap<String, ResourceModel>();
198        for (ResourceModel disabledResourceModel : disabledResources) {
199          disabledResourceByKey.put(disabledResourceModel.getKey(), disabledResourceModel);
200        }
201        return disabledResourceByKey;
202      }
203    
204      private String getParentKey(InputFile matchedFile) {
205        String filePath = PathUtils.sanitize(matchedFile.relativePath());
206        String parentFolderPath;
207        if (filePath.contains(Directory.SEPARATOR)) {
208          parentFolderPath = StringUtils.substringBeforeLast(filePath, Directory.SEPARATOR);
209        } else {
210          parentFolderPath = Directory.SEPARATOR;
211        }
212        return parentFolderPath;
213      }
214    }