/* Copyright (c) 2012-2014, terrestris GmbH & Co. KG
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* (This is the BSD 3-Clause, sometimes called 'BSD New' or 'BSD Simplified',
* see http://opensource.org/licenses/BSD-3-Clause)
*/
package de.terrestris.shogun.service;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import javax.persistence.ManyToMany;
import org.apache.log4j.Logger;
import org.hibernate.collection.internal.PersistentSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import de.terrestris.shogun.exception.ShogunDatabaseAccessException;
import de.terrestris.shogun.exception.ShogunServiceException;
import de.terrestris.shogun.hibernatecriteria.filter.HibernateFilter;
import de.terrestris.shogun.hibernatecriteria.paging.HibernatePagingObject;
import de.terrestris.shogun.hibernatecriteria.sort.HibernateSortObject;
import de.terrestris.shogun.jsonrequest.Paging;
import de.terrestris.shogun.jsonrequest.Request;
import de.terrestris.shogun.jsonrequest.Sort;
import de.terrestris.shogun.jsonrequest.association.Association;
import de.terrestris.shogun.model.BaseModel;
import de.terrestris.shogun.model.BaseModelInheritance;
import de.terrestris.shogun.model.Group;
import de.terrestris.shogun.model.MapConfig;
import de.terrestris.shogun.model.MapLayer;
import de.terrestris.shogun.model.Module;
import de.terrestris.shogun.model.User;
import de.terrestris.shogun.util.JsHelper;
/**
* A basic service class of SHOGun offering some business logic.
*
* @author terrestris GmbH & Co. KG
*
*/
@Service
public class ShogunService extends AbstractShogunService {
/**
* the logger instance
*/
private static Logger LOGGER = Logger.getLogger(ShogunService.class);
/**
* the list of packages where mapped DB model classes are located
*/
private ArrayList<String> dbEntitiesPackages;
/**
* Get Entities defined within a request Request defines filter, sorting and
* paging
*
* @return List<Object>
* @throws ShogunServiceException
*/
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPERADMIN')")
@Transactional(readOnly = true, propagation = Propagation.NOT_SUPPORTED)
public Map<String, Object> getEntities(Request request) throws ShogunServiceException {
HibernateSortObject hibernateSortObject = null;
HibernateFilter hibernateFilter = null;
HibernateFilter hibernateAdditionalFilter = null;
HibernatePagingObject hibernatePaging = null;
Set<String> fields = null;
Set<String> ignoreFields = null;
try {
// Detect the mapped model class for the given
// object type
String objectType = request.getObject_type();
Class clazz = this.getHibernateModelByObjectType(objectType);
if (clazz == null) {
throw new ShogunServiceException("No mapped class for object type " + objectType + " found.");
}
// get the criteria objects from client request
Sort sortObject = request.getSortObject();
de.terrestris.shogun.jsonrequest.Filter filter = request.getFilter();
de.terrestris.shogun.jsonrequest.Filter additionalFilter = request.getGlobalAndFilter();
Paging paging = request.getPagingObject();
hibernateSortObject = HibernateSortObject.create(clazz, sortObject);
hibernateFilter = HibernateFilter.create(clazz, filter);
fields = request.getFields();
ignoreFields = request.getIgnoreFields();
// needed to be able to have another global conjunction
// temporary solution
hibernateAdditionalFilter = HibernateFilter.create(clazz, additionalFilter);
hibernatePaging = HibernatePagingObject.create(clazz, paging);
// get total count
long total = this.getDatabaseDao().getTotal(hibernateFilter, hibernateAdditionalFilter);
// get the data from database
List<Object> dataList = null;
dataList = this.getDatabaseDao().getDataByFilter(
hibernateSortObject,
hibernateFilter,
fields,
ignoreFields,
hibernatePaging,
hibernateAdditionalFilter
);
//TODO introduce a Beans abstracting this
Map<String, Object> returnMap = new HashMap<String, Object>();
returnMap.put("total", new Long(total));
returnMap.put("data", dataList);
returnMap.put("success", true);
return returnMap;
} catch (Exception e) {
throw new ShogunServiceException("Error while requesting data " + e.getMessage(), e);
}
}
/**
* TODO return a valid number
* TODO exception handling
* TODO refer to constant for package
* TODO cleanup
*
* @param association
* @return
* @throws IntrospectionException
*/
@Transactional
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_SUPERADMIN')")
public Integer updateAssociation(Association association) throws IntrospectionException {
Class<?> leftClazz;
Class<?> rightClazz;
Integer leftEntityId;
List<Integer> assocications;
Map<String, String> associationProperties;
Method rightGetter;
try {
leftClazz = this.getHibernateModelByObjectType(association.getLeftEntity());
rightClazz = this.getHibernateModelByObjectType(association.getRightEntity());
leftEntityId = association.getLeftEntityId();
assocications = association.getAssociations();
// getter setter and property which is associated
associationProperties = this.detectAssociationProperties(rightClazz, leftClazz);
// Method object representing the getter-method of the right class, that returns the
// list of left-class-objects, e.g. User.getMapLayers()
LOGGER.debug("Create a method object for " + associationProperties.get("assocGetter"));
rightGetter = rightClazz.getMethod(associationProperties.get("assocGetter"));
BaseModelInheritance wmsMapLayerInstanceToAdd = (BaseModelInheritance)this.getDatabaseDao().getEntityById(leftEntityId, leftClazz);
// List<Object> allUsers = this.getDatabaseDao().getAllEntities(rightClazz);
List<? extends Object> allnewAssocedUsers = this.getDatabaseDao().getEntitiesByIds(assocications.toArray(), rightClazz);
for (Iterator<?> iterator = allnewAssocedUsers.iterator(); iterator.hasNext();) {
BaseModel currentAssocedUser = (BaseModel) iterator.next();
// dynamic invoking of the getter
// receive all left objects that are associated to the current right object
// e.g. all MapLayers of a User
PersistentSet currentAssocedUsersMapLayers = (PersistentSet) rightGetter.invoke(currentAssocedUser);
/*
* http://www.java-forums.org/new-java/20849-how-can-i-avoid-java-util-concurrentmodificationexception-exception.html
*/
if(!currentAssocedUsersMapLayers.contains(wmsMapLayerInstanceToAdd)) {
currentAssocedUsersMapLayers.add(wmsMapLayerInstanceToAdd);
}
if (currentAssocedUsersMapLayers.size() == 0) {
currentAssocedUsersMapLayers.add(wmsMapLayerInstanceToAdd);
}
}
List<? extends Object> allNotAssocedUsers = this.getDatabaseDao().getEntitiesByExcludingIds(assocications.toArray(), rightClazz);
for (Iterator<?> iterator = allNotAssocedUsers.iterator(); iterator.hasNext();) {
BaseModel currentNotAssocedUser = (BaseModel) iterator.next();
// dynamic invoking of the getter
// receive all left objects that are associated to the current right object
// e.g. all MapLayers of a User
PersistentSet currentNotAssocedUsersMapLayers = (PersistentSet)rightGetter.invoke(currentNotAssocedUser);
/*
* http://www.java-forums.org/new-java/20849-how-can-i-avoid-java-util-concurrentmodificationexception-exception.html
*/
if(currentNotAssocedUsersMapLayers.contains(wmsMapLayerInstanceToAdd )) {
currentNotAssocedUsersMapLayers.remove(wmsMapLayerInstanceToAdd);
}
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return null;
}
/**
* Determines the properties responsible to map the association between two
* entities, such as their getter and setter methods.
*
* TODO allow OneToMany as well, only ManyToMany supported ATM
*
* @param clazz
* @param targetClass
* @return
*
* @throws IntrospectionException
*/
private Map<String, String> detectAssociationProperties(Class<?> clazz, Class<?> targetClass) throws IntrospectionException {
Map<String, String> classProperties = new HashMap<String, String>();
PropertyDescriptor[] propertyDescriptors;
propertyDescriptors = Introspector.getBeanInfo(clazz).getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
Method readMethod = propertyDescriptor.getReadMethod();
Annotation[] annotationsProp = readMethod.getAnnotations();
for (Annotation annotation : annotationsProp) {
if (annotation instanceof ManyToMany) {
String dspNameProp = propertyDescriptor.getDisplayName();
Method writeMethod = propertyDescriptor.getWriteMethod();
Class<?> classCandidate = ((ManyToMany)annotation).targetEntity();
if (classCandidate.isAssignableFrom(targetClass)) {
classProperties.put("assocProperty", dspNameProp);
classProperties.put("assocGetter", readMethod.getName());
classProperties.put("assocSetter", writeMethod.getName());
}
}
}
}
return classProperties;
}
/**
* Builds an application context object, consisting of:
* <ul>
* <li>logged in user from session</li>
* <li>application object derived as a combination of user's permissions
* and the permissions of the user's groups</li>
* <ul>
*
* @return HashMap representing the application context as JSON object
* @throws ShogunDatabaseAccessException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Transactional
public Map<String, Object> getAppContextBySession() throws ShogunServiceException, ShogunDatabaseAccessException, IllegalAccessException, InvocationTargetException {
// get the authorization context, incl. user name
Authentication authResult = SecurityContextHolder.getContext().getAuthentication();
// get the user object from database
List<User> users = this.getDatabaseDao().getUserByName(authResult.getName());
// user-check
User user = null;
if (users.size() > 0) {
user = users.get(0);
} else {
throw new ShogunServiceException("No user found, who is logged in at the backend");
}
// fetch the modules within the transaction in order to prevent a LazyLoadingException
// while serializing the user object to JSON
user.getModules();
// get groups and layers of user
Set<Group> usergroups = user.getGroups();
Set<Module> userModules = user.getModules();
// container for app related objects
Set<MapLayer> appMapLayers = new HashSet<MapLayer>();
Set<Module> appModules = new HashSet<Module>();
// collect layers and modules owned by user's group
for (Group usergroup : usergroups) {
appMapLayers.addAll(usergroup.getMapLayers());
appModules.addAll(usergroup.getModules());
}
// derive the common modules of user an its groups
appModules.retainAll(userModules);
// add all owned layers to the set of map layers in the app
List<MapLayer> ownedLayers = this.getDatabaseDao().getOwnedMapLayers(user);
// TODO detect co-owned MapLayers as well
appMapLayers.addAll(ownedLayers);
MapConfig stdMapConfig = (MapConfig) this.getDatabaseDao()
.getEntityByStringField(MapConfig.class, "mapId", "stdmap");
stdMapConfig.restrictBy(user.getMapConfig());
// create an data object containing an JS object for app and user
// as a sub object of the return object
Map<String, Object> appDataMap = new HashMap<String, Object>();
appDataMap.put("mapConfig", stdMapConfig);
appDataMap.put("mapLayers", appMapLayers);
appDataMap.put("modules", appModules);
// the application context object
Map<String, Object> appContextMap = new HashMap<String, Object>(2);
appContextMap.put("app", appDataMap);
appContextMap.put("user", user);
return appContextMap;
}
/**
* Method returns the modules of a user specified by its user name
*
* The complete user object is fetched from the database and the modules are
* extracted and returned
*
* @param username
* @return the list of module objects
* @throws ShogunDatabaseAccessException
*/
@Transactional
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_SUPERADMIN')")
public java.util.Set<Module> getModulesByUser(String username) throws ShogunDatabaseAccessException {
List<User> users = this.getDatabaseDao().getUserByName(username);
User user = null;
if (users.size() > 0) {
user = users.get(0);
Set<Module> modules = user.getModules();
return modules;
} else {
return new HashSet<Module>();
}
}
/**
* Method returns all available {@link Module} objects
*
* <b>CAUTION: Only if the logged in user has the role ROLE_SUPERADMIN the
* function is accessible, otherwise access is denied.</b>
*
* @return the list of module objects
* @throws Exception
* Specifiy generic errors with an own error message
*/
@Transactional
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_SUPERADMIN')")
public List<Module> getAllModules() throws ShogunServiceException {
try {
// get Objects from database
List<Object> dedicatedModules = this.getDatabaseDao().getAllEntities(Module.class);
// Cast from Object to Module
List<Module> allModules = new ArrayList<Module>(dedicatedModules.size());
for (Iterator<Object> iterator = dedicatedModules.iterator(); iterator.hasNext();) {
Module module = (Module) iterator.next();
allModules.add(module);
}
return allModules;
} catch (Exception e) {
throw new ShogunServiceException(
"Error while fetching all Modules from database" + e.getMessage());
}
}
/**
* CAUTION! Not used at the moment, maybe useful for several future use-cases.<br>
* Method returns distinct field values of a specified column within an
* entity
*
* <b>CAUTION: Only if the logged in user has the role ROLE_USER or the role
* ROLE_SUPERADMIN the function is accessible, otherwise access is
* denied.</b>
*
* @return the list of distinct country phone codes as String
* @throws ShogunServiceException
*/
@Transactional
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
public List<Object> getDistictFieldValues(String entity, String field) throws ShogunServiceException {
Class<?> entityClass = null;
try {
entityClass = Class.forName(entity);
} catch (ClassNotFoundException cnfEx) {
cnfEx.printStackTrace();
throw new ShogunServiceException("Provided Class " + entity + " not found " + cnfEx.getMessage());
}
// TODO use param for entity MODEL_CLASS_MAP.get(objectType)
List<String> codes = this.getDatabaseDao().getDistinctEntitiesByField(
entityClass, field, true);
List returnList = new ArrayList();
for (Iterator<String> iterator = codes.iterator(); iterator.hasNext();) {
String code = iterator.next();
List<String> codeAsList = new ArrayList<String>();
codeAsList.add(code);
returnList.add(codeAsList);
}
return returnList;
}
/**
* Inserts a new module into the database.
* Also creates the needed stubs and config blocks within
* the JavaScript parts of SHOGun.
*
* @param moduleKey
* @param path
* @throws IOException
*/
@Transactional
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_SUPERADMIN')")
public void insertModule(Module module, String contextPath)
throws IOException {
// WRITE RIGHT KEYS IN FILE
java.io.File rightKeys = new java.io.File(contextPath + "src/main/webapp/client/configs/right-keys.js");
if (rightKeys.isFile()) {
// Note that FileReader is used, not File, since File is not closeable
Scanner scanner = new Scanner(new FileReader(rightKeys));
StringBuffer sb = new StringBuffer();
try {
//first use a Scanner to get each line
while ( scanner.hasNextLine() ){
sb.append(JsHelper.processLineRightKeys(scanner.nextLine(), module.getModule_name()));
}
}
finally {
// ensure the underlying stream is always closed
// this only has any effect if the item passed to the Scanner
// constructor implements closeable (which it does in this case).
scanner.close();
}
// use buffering
Writer output = new BufferedWriter(new FileWriter(rightKeys));
try {
// FileWriter always assumes default encoding is OK!
output.write( sb.toString() );
}
finally {
output.close();
}
} else {
throw new IOException("file " + contextPath + "src/main/webapp/client/configs/right-keys.js not found ...");
}
// CREATE TEMPLATE DUE TO NAME CONVENTION
java.io.File moduleTemplate = new java.io.File(contextPath + "src/main/webapp/client/javascript/module/TemplateModule.js");
java.io.File newClassFile = new java.io.File(contextPath + "src/main/webapp/client/javascript/module/" + module.getModule_name() + ".js");
if (moduleTemplate.isFile()) {
//Note that FileReader is used, not File, since File is not Closeable
Scanner scanner = new Scanner(new FileReader(moduleTemplate));
StringBuffer sb = new StringBuffer();
try {
//first use a Scanner to get each line
while ( scanner.hasNextLine() ){
sb.append(JsHelper.processLine(scanner.nextLine(), module.getModule_name()));
}
}
finally {
//ensure the underlying stream is always closed
//this only has any effect if the item passed to the Scanner
//constructor implements Closeable (which it does in this case).
scanner.close();
}
//use buffering
Writer output = new BufferedWriter(new FileWriter(newClassFile));
try {
//FileWriter always assumes default encoding is OK!
output.write( sb.toString() );
}
finally {
output.close();
}
} else {
throw new IOException("file " + contextPath + "src/main/webapp/client/javascript/module/TemplateModule.js not found ...");
}
// WRITE CONFIG FOR LOADER
java.io.File loaderConfigOld = new java.io.File(contextPath + "src/main/webapp/client/javascript/terrestris-suite.config.loader.js");
if (loaderConfigOld.isFile()) {
//Note that FileReader is used, not File, since File is not Closeable
Scanner scanner = new Scanner(new FileReader(loaderConfigOld));
StringBuffer sb = new StringBuffer();
try {
//first use a Scanner to get each line
while ( scanner.hasNextLine() ){
sb.append(JsHelper.processLoaderConfByLine(scanner.nextLine(), module.getModule_name()));
}
}
finally {
//ensure the underlying stream is always closed
//this only has any effect if the item passed to the Scanner
//constructor implements Closeable (which it does in this case).
scanner.close();
}
//use buffering
Writer output = new BufferedWriter(new FileWriter(loaderConfigOld));
try {
//FileWriter always assumes default encoding is OK!
output.write( sb.toString() );
}
finally {
output.close();
}
} else {
throw new IOException("file " + contextPath + "src/main/webapp/client/javascript/terrestris-suite.config.loader.js not found ...");
}
// INSERT IN DB
this.getDatabaseDao().createEntity("Module", module);
}
/**
* Deletes a module in the database.
*
* @param module_name
*/
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_SUPERADMIN')")
public void deleteModule(String module_name) {
this.getDatabaseDao().deleteEntityByValue(Module.class, "module_name", module_name);
}
/**
*
* @param objectType
* @return
*/
private Class<?> getHibernateModelByObjectType(String objectType) {
Class<?> mappedClazz = null;
for (String dbEntityPackage : this.dbEntitiesPackages) {
try {
mappedClazz = Class.forName(dbEntityPackage + "." + objectType);
} catch (Exception e) {
// DO NOTHING
}
}
return mappedClazz;
}
/**
* @return the dbEntitiesPackages
*/
public ArrayList<String> getDbEntitiesPackages() {
return dbEntitiesPackages;
}
/**
* @param dbEntitiesPackages the dbEntitiesPackages to set
*/
@Autowired
public void setDbEntitiesPackages(ArrayList<String> dbEntitiesPackages) {
this.dbEntitiesPackages = dbEntitiesPackages;
}
}