import org.sonar.server.db.DbClient;
import org.sonar.server.issue.index.IssueAuthorizationIndexer;
import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.source.index.SourceLineIndexer;
public class ComponentCleanerService implements ServerComponent {
private final PurgeDao purgeDao;
private final IssueAuthorizationIndexer issueAuthorizationIndexer;
private final IssueIndexer issueIndexer;
+ private final SourceLineIndexer sourceLineIndexer;
- public ComponentCleanerService(DbClient dbClient, PurgeDao purgeDao, IssueAuthorizationIndexer issueAuthorizationIndexer, IssueIndexer issueIndexer) {
+ public ComponentCleanerService(DbClient dbClient, PurgeDao purgeDao, IssueAuthorizationIndexer issueAuthorizationIndexer, IssueIndexer issueIndexer, SourceLineIndexer sourceLineIndexer) {
this.dbClient = dbClient;
this.purgeDao = purgeDao;
this.issueAuthorizationIndexer = issueAuthorizationIndexer;
this.issueIndexer = issueIndexer;
+ this.sourceLineIndexer = sourceLineIndexer;
}
public void delete(String projectKey) {
// optimization : index issues is refreshed once at the end
issueAuthorizationIndexer.deleteProject(projectUuid, false);
issueIndexer.deleteProject(projectUuid, true);
+ sourceLineIndexer.deleteByProject(projectUuid);
}
}
import org.sonar.core.purge.PurgeConfiguration;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.properties.ProjectSettingsFactory;
+import org.sonar.server.source.index.SourceLineIndexer;
+
+import java.util.List;
import static org.sonar.core.purge.PurgeConfiguration.newDefaultPurgeConfiguration;
public class DataCleanerStep implements ComputationStep {
private final ProjectPurgeTask purgeTask;
private final IssueIndex issueIndex;
+ private final SourceLineIndexer sourceLineIndexer;
private final ProjectSettingsFactory projectSettingsFactory;
- public DataCleanerStep(ProjectSettingsFactory projectSettingsFactory, ProjectPurgeTask purgeTask, IssueIndex issueIndex) {
+ public DataCleanerStep(ProjectSettingsFactory projectSettingsFactory, ProjectPurgeTask purgeTask, IssueIndex issueIndex, SourceLineIndexer sourceLineIndexer) {
this.projectSettingsFactory = projectSettingsFactory;
this.purgeTask = purgeTask;
this.issueIndex = issueIndex;
+ this.sourceLineIndexer = sourceLineIndexer;
}
@Override
Settings settings = projectSettingsFactory.newProjectSettings(session, projectId);
PurgeConfiguration purgeConfiguration = newDefaultPurgeConfiguration(settings, projectId);
+ List<String> fileUuidsToDisable = purgeTask.findUuidsToDisable(session, projectId);
purgeTask.purge(session, purgeConfiguration, settings);
if (purgeConfiguration.maxLiveDateOfClosedIssues() != null) {
issueIndex.deleteClosedIssuesOfProjectBefore(project.uuid(), purgeConfiguration.maxLiveDateOfClosedIssues());
+ sourceLineIndexer.deleteByFiles(fileUuidsToDisable);
}
}
*/
package org.sonar.server.source.index;
-import com.google.common.annotations.VisibleForTesting;
import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.QueryBuilders;
import org.sonar.core.persistence.DbSession;
import org.sonar.server.db.DbClient;
import java.sql.Connection;
import java.util.Iterator;
+import java.util.List;
+
+import static org.sonar.server.source.index.SourceLineIndexDefinition.FIELD_FILE_UUID;
+import static org.sonar.server.source.index.SourceLineIndexDefinition.FIELD_PROJECT_UUID;
public class SourceLineIndexer extends BaseIndexer {
esClient.prepareDeleteByQuery(SourceLineIndexDefinition.INDEX)
.setTypes(SourceLineIndexDefinition.TYPE)
.setQuery(QueryBuilders.boolQuery()
- .must(QueryBuilders.termQuery(SourceLineIndexDefinition.FIELD_FILE_UUID, fileUuid))
+ .must(QueryBuilders.termQuery(FIELD_FILE_UUID, fileUuid))
.must(QueryBuilders.rangeQuery(SourceLineIndexDefinition.FIELD_LINE).gt(lastLine))
).get();
}
+
+ public void deleteByFiles(List<String> uuids) {
+ esClient.prepareDeleteByQuery(SourceLineIndexDefinition.INDEX)
+ .setTypes(SourceLineIndexDefinition.TYPE)
+ .setQuery(QueryBuilders.filteredQuery(QueryBuilders.matchAllQuery(), FilterBuilders.termsFilter(FIELD_FILE_UUID, uuids).cache(false)))
+ .get();
+ }
+
+ public void deleteByProject(String projectUuid) {
+ esClient.prepareDeleteByQuery(SourceLineIndexDefinition.INDEX)
+ .setTypes(SourceLineIndexDefinition.TYPE)
+ .setQuery(QueryBuilders.filteredQuery(QueryBuilders.matchAllQuery(), FilterBuilders.termFilter(FIELD_PROJECT_UUID, projectUuid).cache(false)))
+ .get();
+ }
}
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.properties.ProjectSettingsFactory;
import org.sonar.server.search.IndexClient;
+import org.sonar.server.source.index.SourceLineIndexer;
import org.sonar.server.tester.ServerTester;
import java.util.Date;
private DbClient dbClient;
private DbSession dbSession;
private IndexClient indexClient;
+ private SourceLineIndexer sourceLineIndexer;
private ProjectSettingsFactory projectSettingsFactory;
private ProjectPurgeTask purgeTask;
this.indexClient = tester.get(IndexClient.class);
this.projectSettingsFactory = tester.get(ProjectSettingsFactory.class);
this.purgeTask = tester.get(ProjectPurgeTask.class);
+ this.sourceLineIndexer = tester.get(SourceLineIndexer.class);
- this.sut = new DataCleanerStep(projectSettingsFactory, purgeTask, indexClient.get(IssueIndex.class));
+ this.sut = new DataCleanerStep(projectSettingsFactory, purgeTask, indexClient.get(IssueIndex.class), sourceLineIndexer);
}
@After
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.properties.ProjectSettings;
import org.sonar.server.properties.ProjectSettingsFactory;
+import org.sonar.server.source.index.SourceLineIndexer;
import java.util.Date;
private DataCleanerStep sut;
private ProjectPurgeTask purgeTask;
private IssueIndex issueIndex;
+ private SourceLineIndexer sourceLineIndexer;
private Settings settings;
private ProjectSettingsFactory projectSettingsFactory;
public void before() {
this.purgeTask = mock(ProjectPurgeTask.class);
this.issueIndex = mock(IssueIndex.class);
+ this.sourceLineIndexer = mock(SourceLineIndexer.class);
this.settings = mock(ProjectSettings.class);
this.projectSettingsFactory = mock(ProjectSettingsFactory.class);
when(projectSettingsFactory.newProjectSettings(any(DbSession.class), anyLong())).thenReturn(settings);
when(settings.getInt(any(String.class))).thenReturn(123);
- this.sut = new DataCleanerStep(projectSettingsFactory, purgeTask, issueIndex);
+ this.sut = new DataCleanerStep(projectSettingsFactory, purgeTask, issueIndex, sourceLineIndexer);
}
@Test
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.SearchHit;
import org.fest.assertions.MapAssert;
import org.junit.Before;
import org.junit.Rule;
import java.util.Map;
import static org.fest.assertions.Assertions.assertThat;
+import static org.sonar.server.source.index.SourceLineIndexDefinition.*;
public class SourceLineIndexerTest {
@Test
public void update_already_indexed_lines() throws Exception {
- es.client().prepareIndex(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE)
+ prepareIndex()
.setSource(IOUtils.toString(new FileInputStream(TestUtils.getResource(this.getClass(), "line2.json"))))
.get();
- es.client().prepareIndex(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE)
+ prepareIndex()
.setSource(IOUtils.toString(new FileInputStream(TestUtils.getResource(this.getClass(), "line2_other_file.json"))))
.setRefresh(true)
.get();
List<Integer> duplications = ImmutableList.of(1, 2, 3);
SourceLineDoc line1 = new SourceLineDoc(ImmutableMap.<String, Object>builder()
- .put(SourceLineIndexDefinition.FIELD_PROJECT_UUID, "abcd")
- .put(SourceLineIndexDefinition.FIELD_FILE_UUID, "efgh")
- .put(SourceLineIndexDefinition.FIELD_LINE, 1)
- .put(SourceLineIndexDefinition.FIELD_SCM_REVISION, "cafebabe")
- .put(SourceLineIndexDefinition.FIELD_SCM_DATE, DateUtils.parseDateTime("2014-01-01T12:34:56+0100"))
- .put(SourceLineIndexDefinition.FIELD_SCM_AUTHOR, "polop")
- .put(SourceLineIndexDefinition.FIELD_SOURCE, "package org.sonar.server.source;")
- .put(SourceLineIndexDefinition.FIELD_DUPLICATIONS, duplications)
+ .put(FIELD_PROJECT_UUID, "abcd")
+ .put(FIELD_FILE_UUID, "efgh")
+ .put(FIELD_LINE, 1)
+ .put(FIELD_SCM_REVISION, "cafebabe")
+ .put(FIELD_SCM_DATE, DateUtils.parseDateTime("2014-01-01T12:34:56+0100"))
+ .put(FIELD_SCM_AUTHOR, "polop")
+ .put(FIELD_SOURCE, "package org.sonar.server.source;")
+ .put(FIELD_DUPLICATIONS, duplications)
.put(BaseNormalizer.UPDATED_AT_FIELD, new Date())
.build());
SourceLineResultSetIterator.SourceFile file = new SourceLineResultSetIterator.SourceFile("efgh", System.currentTimeMillis());
assertThat(countDocuments()).isEqualTo(2L);
- SearchResponse fileSearch = es.client().prepareSearch(SourceLineIndexDefinition.INDEX)
- .setTypes(SourceLineIndexDefinition.TYPE)
- .setQuery(QueryBuilders.termQuery(SourceLineIndexDefinition.FIELD_FILE_UUID, "efgh"))
+ SearchResponse fileSearch = prepareSearch()
+ .setQuery(QueryBuilders.termQuery(FIELD_FILE_UUID, "efgh"))
.get();
assertThat(fileSearch.getHits().getTotalHits()).isEqualTo(1L);
Map<String, Object> fields = fileSearch.getHits().getHits()[0].sourceAsMap();
assertThat(fields).hasSize(9);
assertThat(fields).includes(
- MapAssert.entry(SourceLineIndexDefinition.FIELD_PROJECT_UUID, "abcd"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_FILE_UUID, "efgh"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_LINE, 1),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_SCM_REVISION, "cafebabe"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_SCM_DATE, "2014-01-01T11:34:56.000Z"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_SCM_AUTHOR, "polop"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_SOURCE, "package org.sonar.server.source;"),
- MapAssert.entry(SourceLineIndexDefinition.FIELD_DUPLICATIONS, duplications)
+ MapAssert.entry(FIELD_PROJECT_UUID, "abcd"),
+ MapAssert.entry(FIELD_FILE_UUID, "efgh"),
+ MapAssert.entry(FIELD_LINE, 1),
+ MapAssert.entry(FIELD_SCM_REVISION, "cafebabe"),
+ MapAssert.entry(FIELD_SCM_DATE, "2014-01-01T11:34:56.000Z"),
+ MapAssert.entry(FIELD_SCM_AUTHOR, "polop"),
+ MapAssert.entry(FIELD_SOURCE, "package org.sonar.server.source;"),
+ MapAssert.entry(FIELD_DUPLICATIONS, duplications)
);
}
+ @Test
+ public void delete_file_uuids() throws Exception {
+ addSource("line2.json");
+ addSource("line3.json");
+ addSource("line2_other_file.json");
+
+ indexer.deleteByFiles(Lists.newArrayList("efgh"));
+
+ List<SearchHit> hits = getDocuments();
+ Map<String, Object> document = hits.get(0).getSource();
+ assertThat(hits).hasSize(1);
+ assertThat(document.get(FIELD_LINE)).isEqualTo(2);
+ assertThat(document.get(FIELD_FILE_UUID)).isEqualTo("fdsq");
+ }
+
+ @Test
+ public void delete_by_project_uuid() throws Exception {
+ addSource("line2.json");
+ addSource("line3.json");
+ addSource("line2_other_file.json");
+ addSource("line3_other_project.json");
+
+ indexer.deleteByProject("abcd");
+
+ List<SearchHit> hits = getDocuments();
+ Map<String, Object> document = hits.get(0).getSource();
+ assertThat(hits).hasSize(1);
+ assertThat(document.get(FIELD_PROJECT_UUID)).isEqualTo("plmn");
+ }
+
+ private void addSource(String fileName) throws Exception {
+ prepareIndex()
+ .setSource(IOUtils.toString(new FileInputStream(TestUtils.getResource(this.getClass(), fileName))))
+ .get();
+ }
+
+ private SearchRequestBuilder prepareSearch() {
+ return es.client().prepareSearch(INDEX)
+ .setTypes(TYPE);
+ }
+
+ private IndexRequestBuilder prepareIndex() {
+ return es.client().prepareIndex(INDEX, TYPE);
+ }
+
+ private List<SearchHit> getDocuments() {
+ return es.getDocuments(INDEX, TYPE);
+ }
+
private long countDocuments() {
- return es.countDocuments(SourceLineIndexDefinition.INDEX, SourceLineIndexDefinition.TYPE);
+ return es.countDocuments(INDEX, TYPE);
}
}
--- /dev/null
+{
+ "projectUuid": "abcd",
+ "fileUuid": "efgh",
+ "line": 3,
+ "scmAuthor": "polop",
+ "scmDate": "2014-01-01T12:34:56.7+01:00",
+ "scmRevision": "cafebabe",
+ "source": "// Empty",
+ "updatedAt": "2014-01-01T23:45:01.8+01:00",
+ "utLineHits": 0,
+ "utConditions": 0,
+ "utCoveredConditions": 0,
+ "itLineHits": 0,
+ "itConditions": 0,
+ "itCoveredConditions": 0,
+ "overallLineHits": 0,
+ "overallConditions": 0,
+ "overallCoveredConditions": 0
+}
\ No newline at end of file
--- /dev/null
+{
+ "projectUuid": "plmn",
+ "fileUuid": "efgh",
+ "line": 3,
+ "scmAuthor": "polop",
+ "scmDate": "2014-01-01T12:34:56.7+01:00",
+ "scmRevision": "cafebabe",
+ "source": "// Empty",
+ "updatedAt": "2014-01-01T23:45:01.8+01:00",
+ "utLineHits": 0,
+ "utConditions": 0,
+ "utCoveredConditions": 0,
+ "itLineHits": 0,
+ "itConditions": 0,
+ "itCoveredConditions": 0,
+ "overallLineHits": 0,
+ "overallConditions": 0,
+ "overallCoveredConditions": 0
+}
\ No newline at end of file
import org.sonar.core.purge.PurgeDao;
import org.sonar.core.purge.PurgeProfiler;
+import java.util.List;
+
public class ProjectPurgeTask implements ServerComponent {
private static final Logger LOG = LoggerFactory.getLogger(ProjectPurgeTask.class);
private final PurgeProfiler profiler;
LOG.error("Fail to purge data [id=" + configuration.rootProjectId() + "]", e);
}
}
+
+ public List<String> findUuidsToDisable(DbSession session, Long projectId) {
+ return purgeDao.selectPurgeableFiles(session, projectId);
+ }
}
private final MyBatis mybatis;
private final ResourceDao resourceDao;
private final System2 system2;
- private PurgeProfiler profiler;
+ private final PurgeProfiler profiler;
public PurgeDao(MyBatis mybatis, ResourceDao resourceDao, PurgeProfiler profiler, System2 system2) {
this.mybatis = mybatis;
return projects;
}
+ public List<String> selectPurgeableFiles(DbSession dbSession, Long projectId) {
+ PurgeMapper mapper = dbSession.getMapper(PurgeMapper.class);
+ return mapper.selectPurgeableFileUuids(projectId);
+ }
}
void deleteFileSourcesByProjectUuid(String rootProjectUuid);
void deleteFileSourcesByUuid(String fileUuid);
+
+ List<String> selectPurgeableFileUuids(Long projectId);
}
and not exists(select s.project_id from snapshots s where s.islast=${_true} and s.project_id=p.id)
</select>
+ <select id="selectPurgeableFileUuids" resultType="string" parameterType="long">
+ select p.uuid from projects p
+ where (p.id=#{id} or p.root_id=#{id}) and p.enabled=${_true} and p.scope='FIL'
+ and not exists(select s.project_id from snapshots s where s.islast=${_true} and s.project_id=p.id)
+ </select>
+
<select id="selectMetricIdsWithoutHistoricalData" resultType="long">
select id from metrics where delete_historical_data=${_true}
</select>
*/
package org.sonar.core.purge;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.resources.Scopes;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
import org.sonar.core.persistence.AbstractDaoTestCase;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
import org.sonar.core.resource.ResourceDao;
import java.util.List;
public class PurgeDaoTest extends AbstractDaoTestCase {
- System2 system2;
-
- PurgeDao dao;
+ private PurgeDao sut;
+ private System2 system2;
+ private DbSession dbSession;
private static PurgeableSnapshotDto getById(List<PurgeableSnapshotDto> snapshots, long id) {
for (PurgeableSnapshotDto snapshot : snapshots) {
}
@Before
- public void createDao() {
+ public void before() {
system2 = mock(System2.class);
when(system2.now()).thenReturn(DateUtils.parseDate("2014-04-09").getTime());
+ dbSession = getMyBatis().openSession(false);
+
+ sut = new PurgeDao(getMyBatis(), new ResourceDao(getMyBatis(), system2), new PurgeProfiler(), system2);
+ }
- dao = new PurgeDao(getMyBatis(), new ResourceDao(getMyBatis(), system2), new PurgeProfiler(), system2);
+ @After
+ public void after() {
+ MyBatis.closeQuietly(dbSession);
}
@Test
public void shouldDeleteAbortedBuilds() {
setupData("shouldDeleteAbortedBuilds");
- dao.purge(new PurgeConfiguration(1L, new String[0], 30));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 30));
checkTables("shouldDeleteAbortedBuilds", "snapshots");
}
@Test
public void should_purge_project() {
setupData("shouldPurgeProject");
- dao.purge(new PurgeConfiguration(1L, new String[0], 30));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 30));
checkTables("shouldPurgeProject", "projects", "snapshots");
}
@Test
public void delete_file_sources_of_disabled_resources() {
setupData("delete_file_sources_of_disabled_resources");
- dao.purge(new PurgeConfiguration(1L, new String[0], 30, system2));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 30, system2));
checkTables("delete_file_sources_of_disabled_resources", "file_sources");
}
@Test
public void shouldDeleteHistoricalDataOfDirectoriesAndFiles() {
setupData("shouldDeleteHistoricalDataOfDirectoriesAndFiles");
- dao.purge(new PurgeConfiguration(1L, new String[] {Scopes.DIRECTORY, Scopes.FILE}, 30));
+ sut.purge(new PurgeConfiguration(1L, new String[] {Scopes.DIRECTORY, Scopes.FILE}, 30));
checkTables("shouldDeleteHistoricalDataOfDirectoriesAndFiles", "projects", "snapshots");
}
@Test
public void disable_resources_without_last_snapshot() {
setupData("disable_resources_without_last_snapshot");
- dao.purge(new PurgeConfiguration(1L, new String[0], 30, system2));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 30, system2));
checkTables("disable_resources_without_last_snapshot", "projects", "snapshots", "issues");
}
@Test
public void shouldDeleteSnapshots() {
setupData("shouldDeleteSnapshots");
- dao.deleteSnapshots(PurgeSnapshotQuery.create().setIslast(false).setResourceId(1L));
+ sut.deleteSnapshots(PurgeSnapshotQuery.create().setIslast(false).setResourceId(1L));
checkTables("shouldDeleteSnapshots", "snapshots");
}
@Test
public void shouldSelectPurgeableSnapshots() {
setupData("shouldSelectPurgeableSnapshots");
- List<PurgeableSnapshotDto> snapshots = dao.selectPurgeableSnapshots(1L);
+ List<PurgeableSnapshotDto> snapshots = sut.selectPurgeableSnapshots(1L);
assertThat(snapshots).hasSize(3);
assertThat(getById(snapshots, 1L).isLast()).isTrue();
@Test
public void should_delete_project_and_associated_data() {
setupData("shouldDeleteProject");
- dao.deleteResourceTree(new IdUuidPair(1L, "A"));
+ sut.deleteResourceTree(new IdUuidPair(1L, "A"));
assertEmptyTables("projects", "snapshots", "action_plans", "issues", "issue_changes", "file_sources");
}
@Test
public void should_delete_old_closed_issues() {
setupData("should_delete_old_closed_issues");
- dao.purge(new PurgeConfiguration(1L, new String[0], 30));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 30));
checkTables("should_delete_old_closed_issues", "issues", "issue_changes");
}
@Test
public void should_delete_all_closed_issues() {
setupData("should_delete_all_closed_issues");
- dao.purge(new PurgeConfiguration(1L, new String[0], 0));
+ sut.purge(new PurgeConfiguration(1L, new String[0], 0));
checkTables("should_delete_all_closed_issues", "issues", "issue_changes");
}
+
+ @Test
+ public void select_purgeable_file_uuids_and_only_them() {
+ setupData("select_purgeable_file_uuids");
+
+ List<String> uuids = sut.selectPurgeableFiles(dbSession, 1L);
+
+ assertThat(uuids).containsOnly("GHIJ");
+ }
}
--- /dev/null
+<dataset>
+
+ <!-- the project -->
+ <projects id="1" enabled="[true]" root_id="[null]" uuid="ABCD" project_uuid="ABCD" module_uuid="[null]"
+ module_uuid_path="[null]" created_at="[null]"
+ long_name="[null]" scope="PRJ" qualifier="TRK" kee="project" name="project"
+ description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" path="[null]"
+ deprecated_kee="[null]" authorization_updated_at="[null]"/>
+
+ <!-- the directory -->
+ <projects id="2" enabled="[true]" root_id="1" uuid="EFGH" project_uuid="ABCD" module_uuid="ABCD"
+ module_uuid_path="[null]" created_at="[null]"
+ long_name="[null]" scope="DIR" qualifier="DIR" kee="project:my/dir" name="my/dir"
+ description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" path="[null]"
+ deprecated_kee="[null]" authorization_updated_at="[null]"/>
+
+ <!-- the files -->
+ <projects id="3" enabled="[true]" root_id="1" uuid="GHIJ" project_uuid="ABCD" module_uuid="ABCD"
+ module_uuid_path="ABCD" created_at="[null]"
+ long_name="[null]" scope="FIL" qualifier="FIL" kee="project:my/dir/File.java" name="my/dir/File.java"
+ description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" path="[null]"
+ deprecated_kee="[null]" authorization_updated_at="[null]"/>
+ <projects id="4" enabled="[true]" root_id="1" uuid="KLMN" project_uuid="ABCD" module_uuid="ABCD"
+ module_uuid_path="ABCD" created_at="[null]"
+ long_name="[null]" scope="FIL" qualifier="FIL" kee="project:my/dir/File.java"
+ name="my/dir/File.java"
+ description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" path="[null]"
+ deprecated_kee="[null]" authorization_updated_at="[null]"/>
+ <!-- the file has already been disabled. It should not be selected -->
+ <projects id="5" enabled="[false]" root_id="1" uuid="OPQR" project_uuid="ABCD" module_uuid="ABCD"
+ module_uuid_path="ABCD" created_at="[null]"
+ long_name="[null]" scope="FIL" qualifier="FIL" kee="project:my/dir/File.java"
+ name="my/dir/File.java"
+ description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" path="[null]"
+ deprecated_kee="[null]" authorization_updated_at="[null]"/>
+
+ <snapshots id="1"
+ project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]"
+ status="P" islast="[true]" purge_status="[null]"
+ period1_mode="[null]" period1_param="[null]" period1_date="[null]"
+ period2_mode="[null]" period2_param="[null]" period2_date="[null]"
+ period3_mode="[null]" period3_param="[null]" period3_date="[null]"
+ period4_mode="[null]" period4_param="[null]" period4_date="[null]"
+ period5_mode="[null]" period5_param="[null]" period5_date="[null]"
+ depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00"
+ build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/>
+
+ <snapshots id="2"
+ project_id="2" parent_snapshot_id="1" root_project_id="1" root_snapshot_id="1"
+ status="P" islast="[true]" purge_status="[null]"
+ period1_mode="[null]" period1_param="[null]" period1_date="[null]"
+ period2_mode="[null]" period2_param="[null]" period2_date="[null]"
+ period3_mode="[null]" period3_param="[null]" period3_date="[null]"
+ period4_mode="[null]" period4_param="[null]" period4_date="[null]"
+ period5_mode="[null]" period5_param="[null]" period5_date="[null]"
+ depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00"
+ build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/>
+
+ <!-- isLast is false -->
+ <snapshots id="3"
+ project_id="3" parent_snapshot_id="2" root_project_id="1" root_snapshot_id="1"
+ status="P" islast="[false]" purge_status="[null]"
+ period1_mode="[null]" period1_param="[null]" period1_date="[null]"
+ period2_mode="[null]" period2_param="[null]" period2_date="[null]"
+ period3_mode="[null]" period3_param="[null]" period3_date="[null]"
+ period4_mode="[null]" period4_param="[null]" period4_date="[null]"
+ period5_mode="[null]" period5_param="[null]" period5_date="[null]"
+ depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00"
+ build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/>
+
+ <snapshots id="4"
+ project_id="4" parent_snapshot_id="2" root_project_id="1" root_snapshot_id="1"
+ status="P" islast="[true]" purge_status="[null]"
+ period1_mode="[null]" period1_param="[null]" period1_date="[null]"
+ period2_mode="[null]" period2_param="[null]" period2_date="[null]"
+ period3_mode="[null]" period3_param="[null]" period3_date="[null]"
+ period4_mode="[null]" period4_param="[null]" period4_date="[null]"
+ period5_mode="[null]" period5_param="[null]" period5_date="[null]"
+ depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00"
+ build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/>
+
+ <file_sources id="1" project_uuid="ABCD" file_uuid="GHIJ" data="[null]" line_hashes="[null]" data_hash="321654987"
+ created_at="123456789" updated_at="123456789"/>
+ <file_sources id="2" project_uuid="ABCD" file_uuid="KLMN" data="[null]" line_hashes="[null]" data_hash="321654988"
+ created_at="123456789" updated_at="123456789"/>
+</dataset>