// Copyright 2010 Google Inc. All Rights Reseved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.testing.testify.risk.frontend.server.service.impl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.testing.testify.risk.frontend.model.AccElementType;
import com.google.testing.testify.risk.frontend.model.Bug;
import com.google.testing.testify.risk.frontend.model.Checkin;
import com.google.testing.testify.risk.frontend.model.DataRequest;
import com.google.testing.testify.risk.frontend.model.DataSource;
import com.google.testing.testify.risk.frontend.model.DatumType;
import com.google.testing.testify.risk.frontend.model.Filter;
import com.google.testing.testify.risk.frontend.model.Signoff;
import com.google.testing.testify.risk.frontend.model.TestCase;
import com.google.testing.testify.risk.frontend.model.UploadedDatum;
import com.google.testing.testify.risk.frontend.server.service.DataService;
import com.google.testing.testify.risk.frontend.server.service.UserService;
import com.google.testing.testify.risk.frontend.server.util.ServletUtils;
import com.google.testing.testify.risk.frontend.shared.util.StringUtil;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;
/**
* Implementation of the DataRequestService, for tracking project requests for external
* data. This data is also exposed via the DataRequest servlet.
*
* @author chrsmith@google.com (Chris Smith)
* @author jimr@google.com (Jim Reardon)
*/
@Singleton
public class DataServiceImpl implements DataService {
private static final Logger log = Logger.getLogger(DataServiceImpl.class.getName());
private final PersistenceManagerFactory pmf;
private final UserService userService;
/**
* Creates a new DataServiceImpl instance.
*/
@Inject
public DataServiceImpl(PersistenceManagerFactory pmf, UserService userService) {
this.pmf = pmf;
this.userService = userService;
}
@Override
public boolean isSignedOff(AccElementType type, Long elementId) {
Signoff signoff = getSignoff(type, elementId);
return signoff == null ? false : signoff.getSignedOff();
}
@Override
public void setSignedOff(long projectId, AccElementType type, long elementId,
boolean isSignedOff) {
ServletUtils.requireAccess(userService.hasEditAccess(projectId));
PersistenceManager pm = pmf.getPersistenceManager();
try {
Signoff signoff = getSignoff(type, elementId);
if (signoff == null) {
signoff = new Signoff();
signoff.setParentProjectId(projectId);
signoff.setElementType(type);
signoff.setElementId(elementId);
} else {
if (signoff.getParentProjectId() != projectId) {
ServletUtils.requireAccess(false);
}
}
signoff.setSignedOff(isSignedOff);
pm.makePersistent(signoff);
} finally {
pm.close();
}
}
@Override
@SuppressWarnings("unchecked")
public List<Signoff> getSignoffsByType(long projectId, AccElementType type) {
ServletUtils.requireAccess(userService.hasViewAccess(projectId));
PersistenceManager pm = pmf.getPersistenceManager();
try {
Query query = pm.newQuery(Signoff.class);
query.declareParameters("AccElementType elementTypeParam, Long projectIdParam");
query.setFilter("elementType == elementTypeParam && parentProjectId == projectIdParam");
List<Signoff> results = (List<Signoff>) query.execute(type, projectId);
return ServletUtils.makeGwtSafe(results, pm);
} finally {
pm.close();
}
}
@SuppressWarnings("unchecked")
private Signoff getSignoff(AccElementType type, Long elementId) {
PersistenceManager pm = pmf.getPersistenceManager();
try {
Query query = pm.newQuery(Signoff.class);
query.declareParameters("AccElementType elementTypeParam, Long elementIdParam");
query.setFilter("elementType == elementTypeParam && elementId == elementIdParam");
List<Signoff> results = (List<Signoff>) query.execute(type, elementId);
if (results.size() > 0) {
Signoff signoff = results.get(0);
ServletUtils.requireAccess(userService.hasViewAccess(signoff.getParentProjectId()));
return ServletUtils.makeGwtSafe(signoff, pm);
} else {
return null;
}
} finally {
pm.close();
}
}
@SuppressWarnings("unchecked")
@Override
public List<DataSource> getDataSources() {
boolean isInternal = userService.isInternalUser();
PersistenceManager pm = pmf.getPersistenceManager();
List<DataSource> results = null;
log.info("Retrieving data sources.");
try {
Query query = pm.newQuery(DataSource.class);
if (isInternal == false) {
query.setFilter("internalOnly == false");
log.info("Only retrieving external friendly sources, not an internal user.");
}
results = (List<DataSource>) query.execute();
results = ServletUtils.makeGwtSafe(results, pm);
} finally {
pm.close();
}
log.info("Returning results: " + results.size());
return results;
}
@SuppressWarnings("unchecked")
@Override
public List<DataRequest> getProjectRequests(long projectId) {
ServletUtils.requireAccess(userService.hasViewAccess(projectId));
log.info("Getting Data Requests for project: " + Long.toString(projectId));
PersistenceManager pm = pmf.getPersistenceManager();
Query jdoQuery = pm.newQuery(DataRequest.class);
jdoQuery.declareParameters("Long parentProjectParam");
jdoQuery.setFilter("parentProjectId == parentProjectParam");
List<DataRequest> results = null;
try {
List<DataRequest> returnedRequests = (List<DataRequest>) jdoQuery.execute(projectId);
results = ServletUtils.makeGwtSafe(returnedRequests, pm);
} finally {
pm.close();
}
return results;
}
@Override
public long addDataRequest(DataRequest request) {
ServletUtils.requireAccess(userService.hasEditAccess(request.getParentProjectId()));
log.info("Creating new Data Request for source: " + request.getDataSourceName());
request.setDataSourceName(request.getDataSourceName().trim());
PersistenceManager pm = pmf.getPersistenceManager();
try {
pm.makePersistent(request);
} finally {
pm.close();
}
return request.getRequestId();
}
@Override
public void updateDataRequest(DataRequest request) {
if (request.getRequestId() == null) {
throw new IllegalArgumentException(
"Request has not been saved. Please call addDataRequest first.");
}
ServletUtils.requireAccess(userService.hasEditAccess(request.getParentProjectId()));
log.info("Updating DataRequest: " + request.getRequestId().toString());
PersistenceManager pm = pmf.getPersistenceManager();
try {
pm.makePersistent(request);
} finally {
pm.close();
}
}
@Override
public void removeDataRequest(DataRequest request) {
if (request.getRequestId() == null) {
log.info("Attempting to delete unsaved DataRequest. Ignoring.");
return;
}
ServletUtils.requireAccess(userService.hasEditAccess(request.getParentProjectId()));
log.info("Removing DataRequest: " + request.getRequestId().toString());
PersistenceManager pm = pmf.getPersistenceManager();
try {
DataRequest requestToDelete = pm.getObjectById(DataRequest.class, request.getRequestId());
pm.deletePersistent(requestToDelete);
} finally {
pm.close();
}
}
@Override
public List<Filter> getFilters(long projectId) {
return getFiltersByType(projectId, null);
}
@SuppressWarnings("unchecked")
private List<Filter> getFiltersByType(long projectId, DatumType filterType) {
ServletUtils.requireAccess(userService.hasViewAccess(projectId));
log.info("Getting Filters for project: " + Long.toString(projectId));
PersistenceManager pm = pmf.getPersistenceManager();
Query jdoQuery = pm.newQuery(Filter.class);
List<Filter> jdoResults;
try {
if (filterType == null) {
jdoQuery.declareParameters("Long parentProjectParam");
jdoQuery.setFilter("parentProjectId == parentProjectParam");
jdoResults = (List<Filter>) jdoQuery.execute(projectId);
} else {
jdoQuery.declareParameters("Long parentProjectParam, DatumType filterTypeParam");
jdoQuery.setFilter(
"parentProjectId == parentProjectParam && filterType == filterTypeParam");
jdoResults = (List<Filter>) jdoQuery.execute(projectId, filterType);
}
return ServletUtils.makeGwtSafe(jdoResults, pm);
} finally {
pm.close();
}
}
@Override
public long addFilter(Filter filter) {
ServletUtils.requireAccess(userService.hasEditAccess(filter.getParentProjectId()));
log.info("Adding filter for project: " + filter.getParentProjectId());
PersistenceManager pm = pmf.getPersistenceManager();
try {
pm.makePersistent(filter);
} finally {
pm.close();
}
return filter.getId();
}
@Override
public void updateFilter(Filter filter) {
if (filter.getId() == null) {
throw new IllegalArgumentException(
"Filter has not been saved. Please call addFilter first.");
}
ServletUtils.requireAccess(userService.hasEditAccess(filter.getParentProjectId()));
log.info("Updating filter: " + filter.getId().toString());
PersistenceManager pm = pmf.getPersistenceManager();
try {
pm.makePersistent(filter);
} finally {
pm.close();
}
}
@Override
public void removeFilter(Filter filter) {
if (filter.getId() == null) {
log.info("Attempting to delete unsaved Filter. Ignoring.");
return;
}
ServletUtils.requireAccess(userService.hasEditAccess(filter.getParentProjectId()));
log.info("Deleting filter: " + filter.getId().toString());
PersistenceManager pm = pmf.getPersistenceManager();
try {
Filter filterToDelete = pm.getObjectById(Filter.class, filter.getId());
pm.deletePersistent(filterToDelete);
} finally {
pm.close();
}
}
@SuppressWarnings("unchecked")
@Override
public List<Bug> getProjectBugsById(long projectId) {
return getProjectData(Bug.class, projectId);
}
@SuppressWarnings("unchecked")
@Override
public void updateBugAssociations(long bugId, long attributeId, long componentId,
long capabilityId) {
updateAssociations(Bug.class, bugId, attributeId, componentId, capabilityId);
}
/**
* Try to upload a new bug into the GAE datastore. If a bug with the same Bug ID already exists,
* it will be updated. This is a friendly function; it will not throw if there's an error
* adding the bug.
*/
@Override
public void addBug(Bug bug) {
addBug(bug, userService.getEmail());
}
@SuppressWarnings("unchecked")
@Override
public void addBug(Bug bug, String asEmail) {
log.info("Trying to add Bug: " + bug.getTitle() + " for project " + bug.getParentProjectId());
ServletUtils.requireAccess(userService.hasEditAccess(bug.getParentProjectId(), asEmail));
// Trim long fields.
bug.setTitle(StringUtil.trimString(bug.getTitle()));
saveOrUpdateDatum(bug);
}
@SuppressWarnings("unchecked")
@Override
public List<Checkin> getProjectCheckinsById(long projectId) {
return getProjectData(Checkin.class, projectId);
}
@Override
public void addCheckin(Checkin checkin) {
addCheckin(checkin, userService.getEmail());
}
/**
* Try to upload a new Checkin into the GAE datastore. If a Checkin with the same Checkin ID
* already exists, it will be updated.
*/
@SuppressWarnings("unchecked")
@Override
public void addCheckin(Checkin checkin, String asEmail) {
log.info("Trying to add Checkin: " + checkin.getSummary());
ServletUtils.requireAccess(userService.hasEditAccess(checkin.getParentProjectId(), asEmail));
checkin.setSummary(StringUtil.trimString(checkin.getSummary()));
saveOrUpdateDatum(checkin);
}
@SuppressWarnings("unchecked")
@Override
public void updateCheckinAssociations(long checkinId, long attributeId, long componentId,
long capabilityId) {
updateAssociations(Checkin.class, checkinId, attributeId, componentId, capabilityId);
}
@SuppressWarnings("unchecked")
@Override
public List<TestCase> getProjectTestCasesById(long projectId) {
return getProjectData(TestCase.class, projectId);
}
@SuppressWarnings("unchecked")
@Override
public void updateTestAssociations(long testCaseId, long attributeId, long componentId,
long capabilityId) {
updateAssociations(TestCase.class, testCaseId, attributeId, componentId, capabilityId);
}
@Override
public void addTestCase(TestCase test) {
addTestCase(test, userService.getEmail());
}
/**
* Try to upload a new bug into the GAE datastore. If a test case with the same ID already
* exists, it will be updated.
*/
@SuppressWarnings("unchecked")
@Override
public void addTestCase(TestCase test, String asEmail) {
log.info("Trying to add Test: " + test.getTitle());
ServletUtils.requireAccess(userService.hasEditAccess(test.getParentProjectId(), asEmail));
// Trim long fields.
test.setTitle(StringUtil.trimString(test.getTitle()));
saveOrUpdateDatum(test);
}
@SuppressWarnings("unchecked")
private <T extends UploadedDatum> List<T> getProjectData(Class<T> clazz, long projectId) {
ServletUtils.requireAccess(userService.hasViewAccess(projectId));
log.info("Getting data for project: " + Long.toString(projectId));
PersistenceManager pm = pmf.getPersistenceManager();
Query jdoQuery = pm.newQuery(clazz);
jdoQuery.declareParameters("Long parentProjectParam");
jdoQuery.setFilter("parentProjectId == parentProjectParam");
jdoQuery.setOrdering("externalId asc");
List<T> projectData = null;
try {
List<T> results = (List<T>) jdoQuery.execute(projectId);
projectData = ServletUtils.makeGwtSafe(results, pm);
} finally {
pm.close();
}
return projectData;
}
/**
* Saves a new datum, or updates an existing datum in the database if it already exists.
*
* @param datum
*/
@SuppressWarnings("unchecked")
private <T extends UploadedDatum> void saveOrUpdateDatum(T datum) {
PersistenceManager pm = pmf.getPersistenceManager();
Query jdoQuery = pm.newQuery(datum.getClass());
jdoQuery.declareParameters("Long parentProjectParam, Long externalIdParam");
jdoQuery.setFilter("parentProjectId == parentProjectParam && externalId == externalIdParam");
try {
// There are two ways an existing item may exist: the same primary key, or the same
// parent project ID and bug ID.
T oldDatum = null;
if (datum.getInternalId() != null) {
// If it's the same primary key, make sure it makes sense to overwrite it. The existing
// bug should be non-null (we shouldn't have a pkey if it's unsaved already) and match
// projects.
oldDatum = (T) pm.getObjectById(datum.getClass(), datum.getInternalId());
ServletUtils.requireAccess(oldDatum != null);
ServletUtils.requireAccess(oldDatum.getParentProjectId() == datum.getParentProjectId());
} else {
// Try to load by a combination of project and external IDs.
List<T> results = (List<T>)
jdoQuery.execute(datum.getParentProjectId(), datum.getExternalId());
if (results.size() > 0) {
oldDatum = results.get(0);
}
}
if (oldDatum != null) {
datum.setInternalId(oldDatum.getInternalId());
transferAssignments(oldDatum, datum);
} else {
applyFilters(datum);
}
pm.makePersistent(datum);
} finally {
pm.close();
}
}
private <T extends UploadedDatum> void updateAssociations(Class<T> clazz, long internalId,
long attributeId, long componentId,
long capabilityId) {
PersistenceManager pm = pmf.getPersistenceManager();
try {
T datum = pm.getObjectById(clazz, internalId);
if (datum == null) {
log.info("No results when querying for datum ID: " + internalId);
return;
}
ServletUtils.requireAccess(userService.hasEditAccess(datum.getParentProjectId()));
boolean updated = false;
if (attributeId >= 0) {
datum.setTargetAttributeId(attributeId == 0 ? null : attributeId);
updated = true;
}
if (componentId >= 0) {
datum.setTargetComponentId(componentId == 0 ? null : componentId);
updated = true;
}
if (capabilityId >= 0) {
datum.setTargetCapabilityId(capabilityId == 0 ? null : capabilityId);
updated = true;
}
if (updated) {
pm.makePersistent(datum);
}
} finally {
pm.close();
}
}
/**
* Iff the old datum (from the database) has an assigned attribute, component, or capability
* AND the new datum doesn't have an assignment, we copy the old assignment over.
*
* @param oldDatum the old item.
* @param newDatum the new item; it will have the a/c/c set from the old item.
*/
private void transferAssignments(UploadedDatum oldDatum, UploadedDatum newDatum) {
if (positive(oldDatum.getTargetAttributeId()) && !positive(newDatum.getTargetAttributeId())) {
newDatum.setTargetAttributeId(oldDatum.getTargetAttributeId());
}
if (positive(oldDatum.getTargetComponentId()) && !positive(newDatum.getTargetComponentId())) {
newDatum.setTargetComponentId(oldDatum.getTargetComponentId());
}
if (positive(oldDatum.getTargetCapabilityId()) && !positive(newDatum.getTargetCapabilityId())) {
newDatum.setTargetCapabilityId(oldDatum.getTargetCapabilityId());
}
}
private void applyFilters(UploadedDatum item) {
// TODO(jimr): This is a poor way to filter items... it's a stop-gap solution until the recently
// announced Full Text Search is available for the AppEngine datastore.
List<Filter> filters = getFiltersByType(item.getParentProjectId(), item.getDatumType());
for (Filter filter : filters) {
filter.apply(item);
}
}
private boolean positive(Long value) {
return value != null && value > 0;
}
}