Java tutorial
/** * Axelor Business Solutions * * Copyright (C) 2005-2016 Axelor (<http://axelor.com>). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.axelor.rpc; import static com.axelor.common.StringUtils.isBlank; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; import javax.persistence.EntityTransaction; import javax.persistence.OptimisticLockException; import org.hibernate.StaleObjectStateException; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.axelor.auth.AuthUtils; import com.axelor.auth.db.User; import com.axelor.common.Inflector; import com.axelor.db.EntityHelper; import com.axelor.db.JPA; import com.axelor.db.JpaRepository; import com.axelor.db.JpaSecurity; import com.axelor.db.Model; import com.axelor.db.Query; import com.axelor.db.QueryBinder; import com.axelor.db.Repository; import com.axelor.db.mapper.Mapper; import com.axelor.db.mapper.Property; import com.axelor.db.mapper.PropertyType; import com.axelor.i18n.I18n; import com.axelor.i18n.I18nBundle; import com.axelor.i18n.L10n; import com.axelor.inject.Beans; import com.axelor.meta.MetaPermissions; import com.axelor.meta.MetaStore; import com.axelor.meta.db.MetaAction; import com.axelor.meta.db.MetaTranslation; import com.axelor.meta.schema.views.Selection; import com.axelor.rpc.filter.Filter; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Splitter; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import com.google.inject.TypeLiteral; import com.google.inject.persist.Transactional; /** * This class defines CRUD like interface. * */ public class Resource<T extends Model> { private Class<T> model; private Provider<JpaSecurity> security; private Logger LOG = LoggerFactory.getLogger(Resource.class); private Resource(Class<T> model, Provider<JpaSecurity> security) { this.model = model; this.security = security; } @Inject @SuppressWarnings("unchecked") public Resource(TypeLiteral<T> typeLiteral, Provider<JpaSecurity> security) { this((Class<T>) typeLiteral.getRawType(), security); } /** * Returns the resource class. * */ public Class<?> getModel() { return model; } private Long findId(Map<String, Object> values) { try { return Long.parseLong(values.get("id").toString()); } catch (Exception e) { } return null; } public Response fields() { final Response response = new Response(); final Repository<?> repository = JpaRepository.of(model); final Map<String, Object> meta = Maps.newHashMap(); final List<Object> fields = Lists.newArrayList(); if (repository == null) { for (Property p : JPA.fields(model)) { fields.add(p.toMap()); } } else { for (Property p : repository.fields()) { fields.add(p.toMap()); } } meta.put("model", model.getName()); meta.put("fields", fields); response.setData(meta); response.setStatus(Response.STATUS_SUCCESS); return response; } public static Response models(Request request) { Response response = new Response(); List<String> data = Lists.newArrayList(); for (Class<?> type : JPA.models()) { data.add(type.getName()); } Collections.sort(data); response.setData(ImmutableList.copyOf(data)); response.setStatus(Response.STATUS_SUCCESS); return response; } public Response perms() { Set<JpaSecurity.AccessType> perms = security.get().getAccessTypes(model, null); Response response = new Response(); response.setData(perms); response.setStatus(Response.STATUS_SUCCESS); return response; } public Response perms(Long id) { Set<JpaSecurity.AccessType> perms = security.get().getAccessTypes(model, id); Response response = new Response(); response.setData(perms); response.setStatus(Response.STATUS_SUCCESS); return response; } public Response perms(Long id, String perm) { Response response = new Response(); JpaSecurity sec = security.get(); JpaSecurity.AccessType type = JpaSecurity.CAN_READ; try { type = JpaSecurity.AccessType.valueOf(perm.toUpperCase()); } catch (Exception e) { } try { sec.check(type, model, id); response.setStatus(Response.STATUS_SUCCESS); } catch (Exception e) { response.addError(perm, e.getMessage()); response.setStatus(Response.STATUS_VALIDATION_ERROR); } return response; } private List<String> getSortBy(Request request) { final List<String> sortBy = Lists.newArrayList(); final List<String> sortOn = Lists.newArrayList(); final Mapper mapper = Mapper.of(model); boolean unique = true; boolean desc = true; if (request.getSortBy() != null) { sortOn.addAll(request.getSortBy()); } if (sortOn.isEmpty()) { Property nameField = mapper.getNameField(); if (nameField == null) { nameField = mapper.getProperty("name"); } if (nameField == null) { nameField = mapper.getProperty("code"); } if (nameField != null) { sortOn.add(nameField.getName()); } } for (String spec : sortOn) { String name = spec; if (name.startsWith("-")) { name = name.substring(1); } else { desc = false; } Property property = mapper.getProperty(name); if (property == null || property.isPrimary()) { // dotted field or primary key sortBy.add(spec); continue; } if (property.isReference()) { // use name field to sort many-to-one column Mapper m = Mapper.of(property.getTarget()); Property p = m.getNameField(); if (p != null) { spec = spec + "." + p.getName(); } } if (!property.isUnique()) { unique = false; } sortBy.add(spec); } if (!unique && (!sortBy.contains("id") || !sortBy.contains("-id"))) { sortBy.add(desc ? "-id" : "id"); } return sortBy; } private Criteria getCriteria(Request request) { if (request.getData() != null) { Object domain = request.getData().get("_domain"); if (domain != null) { try { String qs = request.getCriteria().createQuery(model).toString(); JPA.em().createQuery(qs); } catch (Exception e) { throw new IllegalArgumentException("Invalid domain: " + domain); } } } return request.getCriteria(); } private Query<?> getQuery(Request request) { Criteria criteria = getCriteria(request); Filter filter = security.get().getFilter(JpaSecurity.CAN_READ, model); Query<?> query = JPA.all(model); if (criteria != null) { query = criteria.createQuery(model, filter); } else if (filter != null) { query = filter.build(model); } for (String spec : getSortBy(request)) { query = query.order(spec); } return query; } @SuppressWarnings("all") public Response search(Request request) { security.get().check(JpaSecurity.CAN_READ, model); LOG.debug("Searching '{}' with {}", model.getCanonicalName(), request.getData()); Response response = new Response(); int offset = request.getOffset(); int limit = request.getLimit(); Query<?> query = getQuery(request); List<?> data = null; try { if (request.getFields() != null) { Query<?>.Selector selector = query.cacheable().select(request.getFields().toArray(new String[] {})); LOG.debug("JPQL: {}", selector); data = selector.fetch(limit, offset); } else { LOG.debug("JPQL: {}", query); data = query.cacheable().fetch(limit, offset); } response.setTotal(query.count()); } catch (Exception e) { EntityTransaction txn = JPA.em().getTransaction(); if (txn.isActive()) { txn.rollback(); } data = Lists.newArrayList(); LOG.error("Error: {}", e, e); } LOG.debug("Records found: {}", data.size()); final Repository repo = JpaRepository.of(model); final List<Object> jsonData = new ArrayList<>(); for (Object item : data) { if (item instanceof Model) { item = toMap(item); } if (item instanceof Map) { item = repo.populate((Map) item, request.getContext()); Translator.applyTranslatables((Map) item, model); } jsonData.add(item); } try { // check for children (used by tree view) doChildCount(request, jsonData); } catch (NullPointerException | ClassCastException e) { } ; response.setData(jsonData); response.setOffset(offset); response.setStatus(Response.STATUS_SUCCESS); return response; } @SuppressWarnings("all") private void doChildCount(Request request, List<?> result) throws NullPointerException, ClassCastException { if (result == null || result.isEmpty()) { return; } final Map context = (Map) request.getData().get("_domainContext"); final Map childOn = (Map) context.get("_childOn"); final String countOn = (String) context.get("_countOn"); if (countOn == null && childOn == null) { return; } final StringBuilder builder = new StringBuilder(); final List ids = Lists.newArrayList(); for (Object item : result) { ids.add(((Map) item).get("id")); } String modelName = model.getName(); String parentName = countOn; if (childOn != null) { modelName = (String) childOn.get("model"); parentName = (String) childOn.get("parent"); } builder.append("SELECT new map(_parent.id as id, count(self.id) as count) FROM ").append(modelName) .append(" self ").append("LEFT JOIN self.").append(parentName).append(" AS _parent ") .append("WHERE _parent.id IN (:ids) GROUP BY _parent"); javax.persistence.Query q = JPA.em().createQuery(builder.toString()); q.setParameter("ids", ids); Map counts = Maps.newHashMap(); for (Object item : q.getResultList()) { counts.put(((Map) item).get("id"), ((Map) item).get("count")); } for (Object item : result) { ((Map) item).put("_children", counts.get(((Map) item).get("id"))); } } public void export(Request request, Writer writer) throws IOException { security.get().check(JpaSecurity.CAN_READ, model); LOG.debug("Exporting '{}' with {}", model.getName(), request.getData()); List<String> fields = request.getFields(); List<String> header = new ArrayList<>(); List<String> names = new ArrayList<>(); Map<Integer, Map<String, String>> selection = new HashMap<>(); Mapper mapper = Mapper.of(model); MetaPermissions perms = Beans.get(MetaPermissions.class); if (fields == null) { fields = new ArrayList<>(); } if (fields.isEmpty()) { fields.add("id"); try { fields.add(mapper.getNameField().getName()); } catch (Exception e) { } for (Property property : mapper.getProperties()) { if (property.isPrimary() || property.isTransient() || property.isVersion() || property.isCollection() || property.isPassword() || property.getType() == PropertyType.BINARY) { continue; } String name = property.getName(); if (fields.contains(name) || name.matches("^(created|updated)(On|By)$")) { continue; } fields.add(name); } } for (String field : fields) { Iterator<String> iter = Splitter.on(".").split(field).iterator(); Property prop = mapper.getProperty(iter.next()); while (iter.hasNext() && prop != null) { prop = Mapper.of(prop.getTarget()).getProperty(iter.next()); } if (prop == null || prop.isCollection() || prop.isTransient() || prop.getType() == PropertyType.BINARY) { continue; } String name = prop.getName(); String title = prop.getTitle(); String model = getModel().getName(); if (prop.isReference()) { model = prop.getTarget().getName(); } if (!perms.canExport(AuthUtils.getUser(), model, name)) { continue; } if (iter != null) { name = field; } if (isBlank(title)) { title = Inflector.getInstance().humanize(prop.getName()); } if (prop.isReference()) { prop = Mapper.of(prop.getTarget()).getNameField(); if (prop == null) { continue; } name = name + '.' + prop.getName(); } else if (!isBlank(prop.getSelection())) { List<Selection.Option> options = MetaStore.getSelectionList(prop.getSelection()); if (options == null || options.isEmpty()) { continue; } Map<String, String> map = new HashMap<>(); for (Selection.Option option : options) { map.put(option.getValue(), option.getLocalizedTitle()); } selection.put(header.size(), map); } title = I18n.get(title); names.add(name); header.add(escapeCsv(title)); } writer.write(Joiner.on(";").join(header)); int limit = 100; int offset = 0; Query<?> query = getQuery(request); Query<?>.Selector selector = query.select(names.toArray(new String[0])); List<?> data = selector.values(limit, offset); final L10n formatter = L10n.getInstance(); while (!data.isEmpty()) { for (Object item : data) { List<?> row = (List<?>) item; List<String> line = Lists.newArrayList(); int index = 0; for (Object value : row) { if (index++ < 2) continue; // ignore first two items (id, version) Object objValue = value == null ? "" : value; if (selection.containsKey(index - 3)) { objValue = selection.get(index - 3).get(objValue.toString()); } if (objValue instanceof Number) { objValue = formatter.format((Number) objValue); } if (objValue instanceof LocalDate) { objValue = formatter.format((LocalDate) objValue); } if (objValue instanceof LocalDateTime) { objValue = formatter.format((LocalDateTime) objValue); } if (objValue instanceof DateTime) { objValue = formatter.format((DateTime) objValue); } String strValue = objValue == null ? "" : escapeCsv(objValue.toString()); line.add(strValue); } writer.write("\n"); writer.write(Joiner.on(";").join(line)); } offset += limit; data = selector.values(limit, offset); } } private String escapeCsv(String value) { if (value == null) return ""; if (value.indexOf('"') > -1) value = value.replaceAll("\"", "\"\""); return '"' + value + '"'; } public Response read(long id) { security.get().check(JpaSecurity.CAN_READ, model, id); Response response = new Response(); List<Object> data = Lists.newArrayList(); Model entity = JPA.find(model, id); if (entity != null) data.add(entity); response.setData(data); response.setStatus(Response.STATUS_SUCCESS); return response; } public Response fetch(long id, Request request) { security.get().check(JpaSecurity.CAN_READ, model, id); final Response response = new Response(); final Repository<?> repository = JpaRepository.of(model); final Model entity = repository.find(id); response.setStatus(Response.STATUS_SUCCESS); if (entity == null) { return response; } final List<Object> data = Lists.newArrayList(); final String[] fields = request.getFields().toArray(new String[] {}); final Map<String, Object> values = mergeRelated(request, entity, toMap(entity, fields)); // special case for User/Group objects if (values.get("homeAction") != null) { MetaAction act = JpaRepository.of(MetaAction.class).all() .filter("self.name = ?", values.get("homeAction")).fetchOne(); if (act != null) { values.put("__actionSelect", toMapCompact(act)); } } // don't include password if not requested if (entity instanceof User && !request.getFields().contains("password")) { values.remove("password"); } data.add(repository.populate(values, request.getContext())); response.setData(data); return response; } @SuppressWarnings("all") private Map<String, Object> mergeRelated(Request request, Model entity, Map<String, Object> values) { final Map<String, List<String>> related = request.getRelated(); if (related == null) { return values; } final Mapper mapper = Mapper.of(model); for (final String name : related.keySet()) { final String[] names = related.get(name).toArray(new String[] {}); Object old = values.get(name); Object value = mapper.get(entity, name); if (value instanceof Collection<?>) { value = Collections2.transform((Collection<?>) value, new Function<Object, Object>() { @Override public Object apply(Object input) { return toMap(input, names); } }); } else if (value instanceof Model) { value = toMap(value, names); if (old instanceof Map) { value = mergeMaps((Map) value, (Map) old); } } values.put(name, value); } return values; } @SuppressWarnings("all") private Map<String, Object> mergeMaps(Map<String, Object> target, Map<String, Object> source) { if (target == null || source == null || source.isEmpty()) { return target; } for (String key : source.keySet()) { Object old = source.get(key); Object val = target.get(key); if (val instanceof Map && old instanceof Map) { mergeMaps((Map) val, (Map) old); } else if (val == null) { target.put(key, old); } } return target; } public Response verify(Request request) { Response response = new Response(); try { JPA.verify(model, request.getData()); response.setStatus(Response.STATUS_SUCCESS); } catch (OptimisticLockException e) { response.setStatus(Response.STATUS_VALIDATION_ERROR); } return response; } @Transactional @SuppressWarnings("all") public Response save(final Request request) { final Response response = new Response(); final Repository repository = JpaRepository.of(model); List<Object> records = request.getRecords(); List<Object> data = Lists.newArrayList(); if (records == null) { records = Lists.newArrayList(); records.add(request.getData()); } for (Object record : records) { record = (Map) repository.validate((Map) record, request.getContext()); Long id = findId((Map) record); if (id == null || id <= 0L) { security.get().check(JpaSecurity.CAN_CREATE, model); } Map<String, Object> orig = (Map) ((Map) record).get("_original"); JPA.verify(model, orig); // save translatable values and remove them from record Translator.saveTranslatables((Map) record, model); Model bean = JPA.edit(model, (Map) record); id = bean.getId(); if (bean != null && id != null && id > 0L) { security.get().check(JpaSecurity.CAN_WRITE, model, id); } bean = JPA.manage(bean); if (repository != null) { bean = repository.save(bean); } // if it's a translation object, invalidate cache if (bean instanceof MetaTranslation) { I18nBundle.invalidate(); } data.add(repository.populate(toMap(bean), request.getContext())); } response.setData(data); response.setStatus(Response.STATUS_SUCCESS); return response; } @Transactional public Response updateMass(Request request) { security.get().check(JpaSecurity.CAN_WRITE, model); LOG.debug("Mass update '{}' with {}", model.getCanonicalName(), request.getData()); Response response = new Response(); Query<?> query = getQuery(request); List<?> data = request.getRecords(); LOG.debug("JPQL: {}", query); @SuppressWarnings("all") Map<String, Object> values = (Map) data.get(0); response.setTotal(query.update(values)); LOG.debug("Records updated: {}", response.getTotal()); response.setStatus(Response.STATUS_SUCCESS); return response; } @Transactional @SuppressWarnings("all") public Response remove(long id, Request request) { security.get().check(JpaSecurity.CAN_REMOVE, model, id); final Response response = new Response(); final Repository repository = JpaRepository.of(model); final Map<String, Object> data = Maps.newHashMap(); data.put("id", id); data.put("version", request.getData().get("version")); Model bean = JPA.edit(model, data); if (bean.getId() != null) { if (repository == null) { JPA.remove(bean); } else { repository.remove(bean); } } response.setData(ImmutableList.of(toMapCompact(bean))); response.setStatus(Response.STATUS_SUCCESS); return response; } @Transactional @SuppressWarnings("all") public Response remove(Request request) { final Response response = new Response(); final Repository repository = JpaRepository.of(model); final List<Object> records = request.getRecords(); if (records == null || records.isEmpty()) { response.setException(new IllegalArgumentException("No records provides.")); return response; } final List<Model> entities = Lists.newArrayList(); for (Object record : records) { Map map = (Map) record; Long id = Longs.tryParse(map.get("id").toString()); Integer version = null; try { version = Ints.tryParse(map.get("version").toString()); } catch (Exception e) { } security.get().check(JpaSecurity.CAN_REMOVE, model, id); Model bean = JPA.find(model, id); if (version != null && !Objects.equal(version, bean.getVersion())) { throw new OptimisticLockException(new StaleObjectStateException(model.getName(), id)); } entities.add(bean); } for (Model entity : entities) { if (JPA.em().contains(entity)) { if (repository == null) { JPA.remove(entity); } else { repository.remove(entity); } } } response.setData(records); response.setStatus(Response.STATUS_SUCCESS); return response; } @SuppressWarnings("all") public Response copy(long id) { security.get().check(JpaSecurity.CAN_CREATE, model, id); final Response response = new Response(); final Repository repository = JpaRepository.of(model); Model bean = JPA.find(model, id); if (repository == null) { bean = JPA.copy(bean, true); } else { bean = repository.copy(bean, true); } response.setData(ImmutableList.of(bean)); response.setStatus(Response.STATUS_SUCCESS); return response; } public ActionResponse action(ActionRequest request) { ActionResponse response = new ActionResponse(); String[] parts = request.getAction().split("\\:"); if (parts.length != 2) { response.setStatus(Response.STATUS_FAILURE); return response; } String controller = parts[0]; String method = parts[1]; try { Class<?> klass = Class.forName(controller); Method m = klass.getDeclaredMethod(method, ActionRequest.class, ActionResponse.class); Object obj = Beans.get(klass); m.setAccessible(true); m.invoke(obj, new Object[] { request, response }); response.setStatus(Response.STATUS_SUCCESS); } catch (Exception e) { LOG.debug(e.toString(), e); response.setException(e); } return response; } /** * Get the name of the record. This method should be used to get the value * of name field if it's a function field. * * @param request * the request containing the current values of the record * @return response with the updated values with record name */ public Response getRecordName(Request request) { Response response = new Response(); Mapper mapper = Mapper.of(model); Map<String, Object> data = request.getData(); Property property = null; try { property = mapper.getProperty(request.getFields().get(0)); } catch (Exception e) { } if (property == null) { property = mapper.getNameField(); } if (property != null) { String qs = String.format("SELECT self.%s FROM %s self WHERE self.id = :id", property.getName(), model.getSimpleName()); javax.persistence.Query query = JPA.em().createQuery(qs); QueryBinder.of(query).bind(data); Object name = query.getSingleResult(); data.put(property.getName(), name); } response.setData(ImmutableList.of(data)); response.setStatus(Response.STATUS_SUCCESS); return response; } public static Map<String, Object> toMap(Object bean, String... names) { return _toMap(bean, unflatten(null, names), false, 0); } public static Map<String, Object> toMapCompact(Object bean) { return _toMap(bean, null, true, 1); } @SuppressWarnings("all") private static Map<String, Object> _toMap(Object bean, Map<String, Object> fields, boolean compact, int level) { if (bean == null) { return null; } bean = EntityHelper.getEntity(bean); if (fields == null) { fields = Maps.newHashMap(); } Map<String, Object> result = new HashMap<String, Object>(); Mapper mapper = Mapper.of(bean.getClass()); boolean isSaved = ((Model) bean).getId() != null; boolean isCompact = compact || fields.containsKey("$version"); if ((isCompact && isSaved) || (isSaved && level >= 1) || (level > 1)) { Property pn = mapper.getNameField(); Property pc = mapper.getProperty("code"); result.put("id", mapper.get(bean, "id")); result.put("$version", mapper.get(bean, "version")); if (pn != null) result.put(pn.getName(), mapper.get(bean, pn.getName())); if (pc != null) result.put(pc.getName(), mapper.get(bean, pc.getName())); for (String name : fields.keySet()) { Object child = mapper.get(bean, name); if (child instanceof Model) { child = _toMap(child, (Map) fields.get(name), true, level + 1); } if (child != null) { result.put(name, child); } } return result; } for (final Property prop : mapper.getProperties()) { String name = prop.getName(); PropertyType type = prop.getType(); if (type == PropertyType.BINARY) { continue; } if (isSaved && prop.isCollection() && !fields.isEmpty() && !fields.containsKey(name)) { continue; } Object value = mapper.get(bean, name); if (prop.isImage() && byte[].class.isInstance(value)) { value = new String((byte[]) value); } // decimal values should be rounded accordingly otherwise the // json mapper may use wrong scale. if (value instanceof BigDecimal) { BigDecimal decimal = (BigDecimal) value; int scale = prop.getScale(); if (decimal.scale() == 0 && scale > 0 && scale != decimal.scale()) { value = decimal.setScale(scale, RoundingMode.HALF_UP); } } if (value instanceof Model) { // m2o Map<String, Object> _fields = (Map) fields.get(prop.getName()); value = _toMap(value, _fields, true, level + 1); } if (value instanceof Collection) { // o2m | m2m List<Object> items = Lists.newArrayList(); for (Model input : (Collection<Model>) value) { Map<String, Object> item; if (input.getId() != null) { item = _toMap(input, null, true, level + 1); } else { item = _toMap(input, null, false, 1); } if (item != null) { items.add(item); } } value = items; } if (prop.isTranslatable() && value instanceof String) { value = Translator.getTranslation(prop, (String) value); } result.put(name, value); } return result; } @SuppressWarnings("all") private static Map<String, Object> unflatten(Map<String, Object> map, String... names) { if (map == null) map = Maps.newHashMap(); for (String name : names) { if (map.containsKey(name)) continue; if (name.contains(".")) { String[] parts = name.split("\\.", 2); Map<String, Object> child = (Map) map.get(parts[0]); if (child == null) { child = Maps.newHashMap(); } map.put(parts[0], unflatten(child, parts[1])); } else { map.put(name, Maps.newHashMap()); } } return map; } }