001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2012 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * Sonar 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     * Sonar 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
017     * License along with Sonar; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019     */
020    package org.sonar.core.resource;
021    
022    import org.apache.commons.lang.ArrayUtils;
023    import org.apache.commons.lang.StringUtils;
024    import org.apache.ibatis.session.ResultContext;
025    import org.apache.ibatis.session.ResultHandler;
026    import org.apache.ibatis.session.SqlSession;
027    import org.sonar.api.resources.Qualifiers;
028    import org.sonar.api.resources.Scopes;
029    import org.sonar.core.persistence.MyBatis;
030    
031    public class ResourceIndexerDao {
032    
033      public static final int MINIMUM_KEY_SIZE = 3;
034    
035      // The scopes and qualifiers that are not in the following constants are not indexed at all.
036      // Directories and packages are explicitly excluded.
037      private static final String[] RENAMABLE_QUALIFIERS = {Qualifiers.PROJECT, Qualifiers.MODULE, Qualifiers.VIEW, Qualifiers.SUBVIEW};
038      private static final String[] RENAMABLE_SCOPES = {Scopes.PROJECT};
039      private static final String[] NOT_RENAMABLE_QUALIFIERS = {Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE, Qualifiers.CLASS};
040      private static final String[] NOT_RENAMABLE_SCOPES = {Scopes.FILE};
041    
042      private final MyBatis mybatis;
043    
044      public ResourceIndexerDao(MyBatis mybatis) {
045        this.mybatis = mybatis;
046      }
047    
048      /**
049       * This method is reentrant. It can be executed even if the project is already indexed.
050       */
051      public ResourceIndexerDao indexProject(final int rootProjectId) {
052        SqlSession session = mybatis.openBatchSession();
053        try {
054          ResourceIndexerMapper mapper = session.getMapper(ResourceIndexerMapper.class);
055          doIndexProject(rootProjectId, session, mapper);
056          session.commit();
057          return this;
058    
059        } finally {
060          MyBatis.closeQuietly(session);
061        }
062      }
063    
064      /**
065       * This method is reentrant. It can be executed even if some projects are already indexed.
066       */
067      public ResourceIndexerDao indexProjects() {
068        final SqlSession session = mybatis.openBatchSession();
069        try {
070          final ResourceIndexerMapper mapper = session.getMapper(ResourceIndexerMapper.class);
071          session.select("selectRootProjectIds", /* workaround to get booleans */ResourceIndexerQuery.create(), new ResultHandler() {
072            public void handleResult(ResultContext context) {
073              Integer rootProjectId = (Integer) context.getResultObject();
074              doIndexProject(rootProjectId, session, mapper);
075              session.commit();
076            }
077          });
078          return this;
079    
080        } finally {
081          MyBatis.closeQuietly(session);
082        }
083      }
084    
085      private void doIndexProject(int rootProjectId, SqlSession session, final ResourceIndexerMapper mapper) {
086        // non indexed resources
087        ResourceIndexerQuery query = ResourceIndexerQuery.create()
088          .setNonIndexedOnly(true)
089          .setQualifiers(NOT_RENAMABLE_QUALIFIERS)
090          .setScopes(NOT_RENAMABLE_SCOPES)
091          .setRootProjectId(rootProjectId);
092    
093        session.select("selectResources", query, new ResultHandler() {
094          public void handleResult(ResultContext context) {
095            ResourceDto resource = (ResourceDto) context.getResultObject();
096            doIndex(resource, mapper);
097          }
098        });
099    
100        // some resources can be renamed, so index must be regenerated
101        // -> delete existing rows and create them again
102        query = ResourceIndexerQuery.create()
103          .setNonIndexedOnly(false)
104          .setQualifiers(RENAMABLE_QUALIFIERS)
105          .setScopes(RENAMABLE_SCOPES)
106          .setRootProjectId(rootProjectId);
107    
108        session.select("selectResources", query, new ResultHandler() {
109          public void handleResult(ResultContext context) {
110            ResourceDto resource = (ResourceDto) context.getResultObject();
111    
112            mapper.deleteByResourceId(resource.getId());
113            doIndex(resource, mapper);
114          }
115        });
116      }
117    
118    
119      void doIndex(ResourceDto resource, ResourceIndexerMapper mapper) {
120        String key = nameToKey(resource.getName());
121        if (key.length() >= MINIMUM_KEY_SIZE) {
122          ResourceIndexDto dto = new ResourceIndexDto()
123            .setResourceId(resource.getId())
124            .setQualifier(resource.getQualifier())
125            .setRootProjectId(resource.getRootId())
126            .setNameSize(resource.getName().length());
127    
128          for (int position = 0; position <= key.length() - MINIMUM_KEY_SIZE; position++) {
129            dto.setPosition(position);
130            dto.setKey(StringUtils.substring(key, position));
131            mapper.insert(dto);
132          }
133        }
134      }
135    
136      public boolean indexResource(int id, String name, String qualifier, int rootProjectId) {
137        boolean indexed = false;
138        if (isIndexableQualifier(qualifier)) {
139          SqlSession session = mybatis.openSession();
140          try {
141            String key = nameToKey(name);
142            if (key.length() >= MINIMUM_KEY_SIZE) {
143              indexed = true;
144              ResourceIndexerMapper mapper = session.getMapper(ResourceIndexerMapper.class);
145              boolean toBeIndexed = sanitizeIndex(id, key, mapper);
146              if (toBeIndexed) {
147                ResourceIndexDto dto = new ResourceIndexDto()
148                  .setResourceId(id)
149                  .setQualifier(qualifier)
150                  .setRootProjectId(rootProjectId)
151                  .setNameSize(name.length());
152    
153                for (int position = 0; position <= key.length() - MINIMUM_KEY_SIZE; position++) {
154                  dto.setPosition(position);
155                  dto.setKey(StringUtils.substring(key, position));
156                  mapper.insert(dto);
157                }
158                session.commit();
159              }
160            }
161          } finally {
162            MyBatis.closeQuietly(session);
163          }
164        }
165        return indexed;
166      }
167    
168    
169      /**
170       * Return true if the resource must be indexed, false if the resource is already indexed.
171       * If the resource is indexed with a different key, then this index is dropped and the
172       * resource must be indexed again.
173       */
174      private boolean sanitizeIndex(int resourceId, String key, ResourceIndexerMapper mapper) {
175        ResourceIndexDto masterIndex = mapper.selectMasterIndexByResourceId(resourceId);
176        if (masterIndex != null && !StringUtils.equals(key, masterIndex.getKey())) {
177          // resource has been renamed -> drop existing indexes
178          mapper.deleteByResourceId(resourceId);
179          masterIndex = null;
180        }
181        return masterIndex == null;
182      }
183    
184      static String nameToKey(String input) {
185        return StringUtils.lowerCase(StringUtils.trimToEmpty(input));
186      }
187    
188      static boolean isIndexableQualifier(String qualifier) {
189        return ArrayUtils.contains(RENAMABLE_QUALIFIERS, qualifier) || ArrayUtils.contains(NOT_RENAMABLE_QUALIFIERS, qualifier);
190      }
191    }