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