/*
* Copyright 2002-2011 the original author or authors.
*
* 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 org.grails.plugins.elasticsearch.conversion.unmarshall;
import groovy.lang.GroovyObject;
import org.apache.log4j.Logger;
import org.codehaus.groovy.grails.commons.*;
import org.codehaus.groovy.grails.web.metaclass.BindDynamicMethod;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.grails.plugins.elasticsearch.ElasticSearchContextHolder;
import org.grails.plugins.elasticsearch.mapping.SearchableClassMapping;
import org.grails.plugins.elasticsearch.mapping.SearchableClassPropertyMapping;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import java.beans.PropertyEditor;
import java.util.*;
/**
* Domain class unmarshaller.
*/
public class DomainClassUnmarshaller {
private static final Logger LOG = Logger.getLogger(DomainClassUnmarshaller.class);
private TypeConverter typeConverter = new SimpleTypeConverter();
private ElasticSearchContextHolder elasticSearchContextHolder;
private BindDynamicMethod bind = new BindDynamicMethod();
private GrailsApplication grailsApplication;
private Client elasticSearchClient;
public Collection buildResults(SearchHits hits) {
DefaultUnmarshallingContext unmarshallingContext = new DefaultUnmarshallingContext();
List results = new ArrayList();
for(SearchHit hit : hits) {
String type = hit.type();
SearchableClassMapping scm = elasticSearchContextHolder.findMappingContextByElasticType(type);
if (scm == null) {
LOG.warn("Unknown SearchHit: " + hit.id() + "#" + hit.type() + ", domain class name: ");
continue;
}
String domainClassName = scm.getDomainClass().getFullName();
GrailsDomainClassProperty identifier = scm.getDomainClass().getIdentifier();
Object id = typeConverter.convertIfNecessary(hit.id(), identifier.getType());
GroovyObject instance = (GroovyObject) scm.getDomainClass().newInstance();
instance.setProperty(identifier.getName(), id);
/*def mapContext = elasticSearchContextHolder.getMappingContext(domainClass.propertyName)?.propertiesMapping*/
Map rebuiltProperties = new HashMap();
for(Map.Entry<String, Object> entry : hit.getSource().entrySet()) {
unmarshallingContext.getUnmarshallingStack().push(entry.getKey());
rebuiltProperties.put(entry.getKey(),
unmarshallProperty(scm.getDomainClass(), entry.getKey(), entry.getValue(), unmarshallingContext));
populateCyclicReference(instance, rebuiltProperties, unmarshallingContext);
unmarshallingContext.resetContext();
}
// todo manage read-only transient properties...
bind.invoke(instance, "bind", new Object[] { instance, rebuiltProperties });
results.add(instance);
}
return results;
}
private void populateCyclicReference(Object instance, Map<String, Object> rebuiltProperties, DefaultUnmarshallingContext unmarshallingContext) {
for(CycleReferenceSource cr : unmarshallingContext.getCycleRefStack()) {
populateProperty(cr.getCyclePath(), rebuiltProperties, resolvePath(cr.getSourcePath(), instance, rebuiltProperties));
}
}
private Object resolvePath(String path, Object instance, Map<String, Object> rebuiltProperties) {
if (path == null || path.equals("")) {
return instance;
} else {
StringTokenizer st = new StringTokenizer(path, "/");
Object currentProperty = rebuiltProperties;
while (st.hasMoreTokens()) {
String part = st.nextToken();
try {
int index = Integer.parseInt(part);
currentProperty = DefaultGroovyMethods.getAt(DefaultGroovyMethods.asList((Collection) currentProperty), index);
} catch (NumberFormatException e) {
currentProperty = DefaultGroovyMethods.getAt(currentProperty, part);
}
}
return currentProperty;
}
}
private void populateProperty(String path, Map<String, Object> rebuiltProperties, Object value) {
String last = null;
Object currentProperty = rebuiltProperties;
StringTokenizer st = new StringTokenizer(path, "/");
int size = st.countTokens();
int index = 0;
while (st.hasMoreTokens()) {
String part = st.nextToken();
if (index < size - 1) {
try {
if (currentProperty instanceof Collection) {
//noinspection unchecked
currentProperty = DefaultGroovyMethods.getAt(((Collection<Object>) currentProperty).iterator(), DefaultGroovyMethods.toInteger(part));
} else {
currentProperty = DefaultGroovyMethods.getAt(currentProperty, part);
}
} catch (Exception e) {
LOG.warn("/!\\ Error when trying to populate " + path);
LOG.warn("Cannot get " + part + " on " + currentProperty + " from " + rebuiltProperties);
e.printStackTrace();
}
}
if (!st.hasMoreTokens()) {
last = part;
}
index++;
}
try {
Integer.parseInt(last);
((Collection) currentProperty).add(value);
} catch (NumberFormatException e) {
DefaultGroovyMethods.putAt(currentProperty, last, value);
}
}
private Object unmarshallProperty(GrailsDomainClass domainClass, String propertyName, Object propertyValue, DefaultUnmarshallingContext unmarshallingContext) {
// TODO : adapt behavior if the mapping option "component" or "reference" are set
// below is considering the "component" behavior
SearchableClassPropertyMapping scpm = elasticSearchContextHolder.getMappingContext(domainClass).getPropertyMapping(propertyName);
Object parseResult = null;
if (null == scpm) {
// TODO: unhandled property exists in index
}
if (null != scpm && propertyValue instanceof Map) {
@SuppressWarnings({"unchecked"})
Map<String, Object> data = (Map<String, Object>) propertyValue;
// Handle cycle reference
if (data.containsKey("ref")) {
unmarshallingContext.addCycleRef(propertyValue);
return null;
}
// Searchable reference.
if (scpm.getReference() != null) {
Class<?> refClass = scpm.getBestGuessReferenceType();
GrailsDomainClass refDomainClass = null;
for(GrailsClass dClazz : grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE)) {
if (dClazz.getClazz().equals(refClass)) {
refDomainClass = (GrailsDomainClass) dClazz;
break;
}
}
if (refDomainClass == null) {
throw new IllegalStateException("Found reference to non-domain class: " + refClass);
}
return unmarshallReference(refDomainClass, data, unmarshallingContext);
}
if (data.containsKey("class") && (Boolean)grailsApplication.getFlatConfig().get("elasticSearch.unmarshallComponents")) {
// Embedded instance.
if (!scpm.isComponent()) {
// maybe ignore?
throw new IllegalStateException("Property " + domainClass.getName() + "." + propertyName +
" is not mapped as [component], but broken search hit found.");
}
GrailsDomainClass nestedDomainClass = (GrailsDomainClass)
grailsApplication.getArtefact(DomainClassArtefactHandler.TYPE, (String) data.get("class"));
if (domainClass != null) {
// Unmarshall 'component' instance.
if (!scpm.isComponent()) {
throw new IllegalStateException("Object " + data.get("class") +
" found in index, but [" + propertyName + "] is not mapped as component.");
}
parseResult = unmarshallDomain(nestedDomainClass, data.get("id"), data, unmarshallingContext);
}
}
} else if (propertyValue instanceof Collection) {
List<Object> results = new ArrayList<Object>();
int index = 0;
for(Object innerValue : (Collection) propertyValue) {
unmarshallingContext.getUnmarshallingStack().push(String.valueOf(index));
Object parseItem = unmarshallProperty(domainClass, propertyName, innerValue, unmarshallingContext);
if (parseItem != null) {
results.add(parseItem);
}
index++;
unmarshallingContext.getUnmarshallingStack().pop();
}
parseResult = results;
} else {
// consider any custom property editors here.
if (scpm.getConverter() != null) {
if (scpm.getConverter() instanceof Class) {
try {
PropertyEditor propertyEditor = (PropertyEditor) ((Class) scpm.getConverter()).newInstance();
propertyEditor.setAsText((String) propertyValue);
parseResult = propertyEditor.getValue();
} catch (Exception e) {
throw new IllegalArgumentException("Unable to unmarshall " + propertyName +
" using " + scpm.getConverter(), e);
}
}
} else if (scpm.getReference() != null) {
// This is a reference and it MUST be null because it's not a Map.
if (propertyValue != null) {
throw new IllegalStateException("Found searchable reference which is not a Map: " + domainClass + "." + propertyName +
" = " + propertyValue);
}
parseResult = null;
}
}
if (parseResult != null) {
return parseResult;
} else {
return propertyValue;
}
}
private Object unmarshallReference(GrailsDomainClass domainClass, Map<String, Object> data, DefaultUnmarshallingContext unmarshallingContext) {
// As a simplest scenario recover object directly from ElasticSearch.
// todo add first-level caching and cycle ref checking
String indexName = elasticSearchContextHolder.getMappingContext(domainClass).getIndexName();
String name = elasticSearchContextHolder.getMappingContext(domainClass).getElasticTypeName();
// A property value is expected to be a map in the form [id:ident]
Object id = data.get("id");
GetResponse response = elasticSearchClient.get(new GetRequest(indexName)
.operationThreaded(false)
.type(name)
.id(typeConverter.convertIfNecessary(id, String.class)))
.actionGet();
return unmarshallDomain(domainClass, response.getId(), response.getSourceAsMap(), unmarshallingContext);
}
private Object unmarshallDomain(GrailsDomainClass domainClass, Object providedId, Map<String, Object> data, DefaultUnmarshallingContext unmarshallingContext) {
GrailsDomainClassProperty identifier = domainClass.getIdentifier();
Object id = typeConverter.convertIfNecessary(providedId, identifier.getType());
GroovyObject instance = (GroovyObject) domainClass.newInstance();
instance.setProperty(identifier.getName(), id);
for(Map.Entry<String, Object> entry : data.entrySet()) {
if (!entry.getKey().equals("class") && !entry.getKey().equals("id")) {
unmarshallingContext.getUnmarshallingStack().push(entry.getKey());
Object propertyValue = unmarshallProperty(domainClass, entry.getKey(), entry.getValue(), unmarshallingContext);
bind.invoke(instance, "bind", new Object[] { instance, Collections.singletonMap(entry.getKey(), propertyValue) });
unmarshallingContext.getUnmarshallingStack().pop();
}
}
return instance;
}
public void setElasticSearchContextHolder(ElasticSearchContextHolder elasticSearchContextHolder) {
this.elasticSearchContextHolder = elasticSearchContextHolder;
}
public void setGrailsApplication(GrailsApplication grailsApplication) {
this.grailsApplication = grailsApplication;
}
public void setElasticSearchClient(Client elasticSearchClient) {
this.elasticSearchClient = elasticSearchClient;
}
}