All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.tno.bim.nmd.services.Nmd3DataService Maven / Gradle / Ivy

package nl.tno.bim.nmd.services;

import java.io.IOException;
import java.nio.file.Path;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.apache.log4j.Logger;
import org.bimserver.shared.reflector.KeyValuePair;
import org.eclipse.jdt.core.compiler.InvalidInputException;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;

import nl.tno.bim.nmd.config.NmdConfig;
import nl.tno.bim.nmd.config.NmdConfigImpl;
import nl.tno.bim.nmd.domain.NlsfbCode;
import nl.tno.bim.nmd.domain.NmdElement;
import nl.tno.bim.nmd.domain.NmdElementImpl;
import nl.tno.bim.nmd.domain.NmdFaseProfielImpl;
import nl.tno.bim.nmd.domain.NmdMilieuCategorie;
import nl.tno.bim.nmd.domain.NmdProductCard;
import nl.tno.bim.nmd.domain.NmdProductCardImpl;
import nl.tno.bim.nmd.domain.NmdProfileSet;
import nl.tno.bim.nmd.domain.NmdProfileSetImpl;
import nl.tno.bim.nmd.domain.NmdReferenceResources;
import nl.tno.bim.nmd.scaling.NmdScaler;
import nl.tno.bim.nmd.scaling.NmdScalerFactory;

/**
 * Implementation of the NmdDataService to retrieve data from the NMD see :
 * https://www.milieudatabase.nl/ for mor information
 * 
 * @author vijj
 *
 */
public class Nmd3DataService extends AuthorizedRestDataService implements BaseNmdDataService {
// ToDo: implement class AUthorizedBAseDatabaseSession

	private final static Logger logger = Logger.getLogger(Nmd3DataService.class);
	private static final DateFormat dbDateFormat = new SimpleDateFormat("yyyyMMdd");
	
	private String apiPath = "/NMD_30_API_v1.1/api/NMD30_Web_API/";
	private Calendar requestDate;
	private NmdConfig config = null;
	private NmdReferenceResources resources;
	private Calendar tokenExpirationTime = null;
	
	NmdScalerFactory scalerFactory = new NmdScalerFactory();

	private List data;

	public Nmd3DataService(NmdConfig config) {
		setParameters(config);
	}

	private Nmd3DataService() {
		setParameters(new NmdConfigImpl());
		this.preLoadData();
	}
	
	private static Nmd3DataService instance = null;
	
    public static synchronized Nmd3DataService getInstance() {
        if(instance == null) {
            instance = new Nmd3DataService();
        }
        return instance;
    }
    
    public static synchronized Nmd3DataService getInstance(Path contextPath) {
        if(instance == null) {
            instance = new Nmd3DataService(new NmdConfigImpl(contextPath));
        }
        return instance;
    }
    
	private void setParameters(NmdConfig config) {
		this.setPort(-1);
		this.setHost("www.Milieudatabase-datainvoer.nl");
		this.setScheme("https");
		this.config = config;
		this.requestDate = Calendar.getInstance();
		requestDate.set(2019, 8, 15, 12, 00, 00);
		this.data = new ArrayList();
	}

	@Override
	public Calendar getRequestDate() {
		return this.requestDate;
	}

	@Override
	public void setRequestDate(Calendar newDate) {
		this.requestDate = newDate;
	}

	@Override
	public Boolean getIsConnected() {
		return tokenExpirationTime != null
				&& Calendar.getInstance().getTimeInMillis() + 1e4 <= this.tokenExpirationTime.getTimeInMillis();
	}

	@Override
	public List getData() {
		return data;
	}

	@Override
	public void login() {
		this.login(true);
	}

	@SuppressWarnings("null")
	public void login(boolean preloadResources) {
		// Try add a Post request to the base class.
		HttpPost httppost = new HttpPost("https://www.milieudatabase-datainvoer.nl/NMD_30_AuthenticationServer/"
				+ "NMD_30_API_Authentication/getToken");

		httppost.addHeader("refreshToken", config.getToken());
		httppost.addHeader("API_ID", "1");
		httppost.addHeader("Content-Type", "application/x-www-form-urlencoded");

		List params = new ArrayList();
		params.add(new BasicNameValuePair("grant_type", "client_credentials"));

		// Execute and get the response.
		HttpResponse response = null;
		try {
			httppost.setEntity(new UrlEncodedFormEntity(params));
			response = httpClient.execute(httppost);
			JsonNode responseNode = this.responseToJson(response);
			JsonNode tokenNode = responseNode.get("TOKEN");
			this.setToken(tokenNode.asText());
			this.tokenExpirationTime = Calendar.getInstance();
			this.tokenExpirationTime.add(Calendar.MINUTE, 30);

			httppost.releaseConnection();
		} catch (IOException e1) {
			this.tokenExpirationTime = null;
			logger.error("authentication failed: " + response.getStatusLine().toString());
		}

		if (preloadResources) {
			loadResources();
		}
	}

	@Override
	public void preLoadData() {
		this.data = this.getAllElements();
	}

	@Override
	public void logout() {
		this.setToken("");
		this.tokenExpirationTime = null;
		this.resources = null;
	}

	private void loadResources() {
		if (this.resources == null) {
			this.loadReferenceResources();
		}
	}

	public NmdReferenceResources getResources() {
		loadResources();
		return this.resources;
	}

	public void setResources(NmdReferenceResources resources) {
		this.resources = resources;
	}

	/*
	 * Get the reference resources to map database ids to meaningful fieldnames
	 */
	public void loadReferenceResources() {
		List params = new ArrayList();
		params.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));
		resources = new NmdReferenceResources();

		try {
			performResourceRequest("Fasen", params, this::convertJsonToLifeCycleFasen);
			performResourceRequest("MilieuCategorien", params, this::convertJsonToMilieuCategorieen);
			performResourceRequest("Eenheden", params, this::convertJsonToUnits);
			performResourceRequest("CUAScategorien", params, this::convertJsonToCUASCodes);
			performResourceRequest("SchalingsFormules", params, this::convertJsonToScaling);
		} catch (Exception e) {
			logger.error("failed to retrieve reference resources");
		}
	}

	/**
	 * Generic request to add resource data to the overall resources
	 * 
	 * @param                   type of the resource to be stored
	 * @param resourceName         API name of the resource
	 * @param params               params for the api request
	 * @param resourceInsertMethod Consumer method for specific implementation how
	 *                             to convert Json to resource object
	 */
	private  void performResourceRequest(String resourceName, List params,
			BiConsumer> resourceInsertMethod) {
		// rekenregels reference data
		HttpResponse response = this.performGetRequestWithParams(apiPath + resourceName, params);
		JsonNode resp = this.responseToJson(response);

		if (resp.hasNonNull("results")) {
			JsonNode node = resp.get("results");
			Map resource = new HashMap();
			resourceInsertMethod.accept(node, resource);
		}
	}

	private void convertJsonToScaling(JsonNode node, Map resource) {
		node.forEach(formula -> {
			resource.putIfAbsent(TryParseJsonNode(formula.get("SchalingsFormuleID"), -1),
					TryParseJsonNode(formula.get("SoortFormule"), "") + " : "
							+ TryParseJsonNode(formula.get("Formule"), ""));
		});
		resources.setScalingFormula(resource);
	}

	private void convertJsonToCUASCodes(JsonNode node, Map resource) {
		node.forEach(cuas_code -> {
			resource.putIfAbsent(TryParseJsonNode(cuas_code.get("ID"), -1),
					TryParseJsonNode(cuas_code.get("CUAS_code"), ""));
		});
		resources.setCuasCategorieMapping(resource);
	}

	private void convertJsonToUnits(JsonNode node, Map resource) {
		node.forEach(eenheid -> {

			resource.putIfAbsent(TryParseJsonNode(eenheid.get("EenheidID"), -1),
					TryParseJsonNode(eenheid.get("Code"), ""));
		});
		resources.setUnitMapping(resource);
	}

	private void convertJsonToMilieuCategorieen(JsonNode node, Map resource) {
		node.forEach(categorie -> {
			NmdMilieuCategorie factor = new NmdMilieuCategorie(categorie.get("Milieueffect").asText(),
					categorie.get("Eenheid").asText(),
					categorie.get("Wegingsfactor") != null ? categorie.get("Wegingsfactor").asDouble() : 0.0);
			resource.putIfAbsent(Integer.parseInt(categorie.get("MilieuCategorieID").asText()), factor);
		});
		resources.setMilieuCategorieMapping(resource);
	}

	private void convertJsonToLifeCycleFasen(JsonNode node, Map resource) {
		node.forEach(fase -> {
			resource.putIfAbsent(Integer.parseInt(fase.get("FaseID").asText()), fase.get("FaseNaam").asText());
		});
		resources.setFaseMapping(resource);
	}

	@Override
	public List getAllElements() {
		if (this.data.size() > 0) { return data;}

		if (!this.getIsConnected()) {
			login();
		}

		List params = new ArrayList();
		params.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));

		HttpResponse response = this.performGetRequestWithParams(this.apiPath + "NLsfB_RAW_Elementen", params);
		// do something with the entity to get the token
		JsonNode respNode = this.responseToJson(response);

		// convert reponseNode to NmdProductCard info.
		List results = new ArrayList();

		// parse the results and get the product cards
		if (respNode.hasNonNull("results")) {
			respNode.get("results").forEach(f -> results.add(this.getElementDataFromJson(f)));
			for (NmdElement el : results) {
				el.addProductCards(this.getProductsForElement(el));
			}
		}

		return results;
	}

	/**
	 * get child elements (deelelementen) for a list of parent elements
	 * 
	 * @param parentIds any possible parent element ids
	 * @return a list of child elements to these parent elements and any recurrent
	 *         children.
	 */
	private List getChildElements(List parentIds) {

		List results = new ArrayList();

		parentIds.forEach(id -> {
			results.addAll(this.getChildElement(id));
		});

		return results;
	}

	/**
	 * Call the MD REST api to call a single Element Onderdeel
	 * 
	 * @param id: unique identifier of a (possible) parent element
	 * @return a list of the possible child elements (and its children)
	 */
	private List getChildElement(Integer id) {

		List results = new ArrayList();
		try {
			List params_el = new ArrayList();
			params_el.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));
			params_el.add(new KeyValuePair("ElementID", id));

			// load the json
			HttpResponse responseEl = this.performGetRequestWithParams(this.apiPath + "ElementOnderdelen", params_el);
			JsonNode respNodeEl = this.responseToJson(responseEl);
			if (respNodeEl != null) {
				// parse json to objects
				List newResults = new ArrayList();
				respNodeEl.get("results").forEach(f -> newResults.add(this.getElementDataFromJson(f)));
				if (newResults.size() > 0) {
					results.addAll(newResults);

					// repeat above process for new finds.
					List newResultIds = newResults.stream().map(r -> r.getElementId())
							.collect(Collectors.toList());
					results.addAll(getChildElements(newResultIds));
				}
			}

		} catch (NullPointerException e) {
			logger.error("error encountered while querying ElementOnderdelen with id: " + id.toString());
		}
		return results;
	}

	@Override
	public List getProductsForElement(NmdElement element) {
		if (!this.getIsConnected()) {
			login();
		}

		List params = new ArrayList();
		params.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));
		params.add(new KeyValuePair("ElementID", element.getElementId()));

		HttpResponse response = this.performGetRequestWithParams(this.apiPath + "ProductenBijElement", params);

		JsonNode resp_node = this.responseToJson(response);
		if (resp_node == null) {
			return new ArrayList();
		}

		JsonNode producten = resp_node.get("results");
		List products = new ArrayList<>();
		if (producten != null) {
			producten.forEach(p -> {

				// only get items that are relevant for us.
				NmdProductCardImpl product = this.getProductCardDataFromJson(p);
				if (TryParseJsonNode(p.get("ProfielSetGekoppeld"), false) && product.getUnit() != null
						&& product.getLifetime() > 0) {
					if (this.getAdditionalProfileDataForCard(product)) {
						products.add(product);
						product.setNlsfbCode(element.getNlsfbCode());
					}
				}
			});
		}

		return products;
	}

	public HashMap> getQuantitiesOfProfileSetsForProduct(Integer productId) {
		if (!this.getIsConnected()) {
			login();
		}

		List params = new ArrayList();
		params.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));
		params.add(new KeyValuePair("ProductID", productId.toString()));
		params.add(new KeyValuePair("includeNULLs", true));

		HttpResponse response = this.performGetRequestWithParams(this.apiPath + "ProfielsetsEnSchalingBijProduct",
				params);

		HashMap> res = new HashMap<>();
		// do something with the entity to get the token
		JsonNode resp_node = this.responseToJson(response);

		JsonNode psNodes = resp_node.get("results");
		if (psNodes != null) {
			psNodes.forEach(ps -> {
				NmdScaler scaler = getScalerFromJson(ps);
				Integer id = ps.get("ProfielSetID").asInt(-1);
				Double q = ps.get("Hoeveelheid").asDouble(1.0);
				q = q > 0.0 ? q : 1.0;
				res.put(id, new MutablePair(q, scaler));
			});
		}

		return res;
	}

	/*
	 * Add missing data to the NmdProfileSet
	 */
	@Override
	public Boolean getAdditionalProfileDataForCard(NmdProductCard c) {
		// check if data has already been loaded
		if (c.getProfileSets().size() > 0) {
			return true;
		}

		try {
			HashMap setData = getProfileSetsByIds(Arrays.asList(c.getProductId()));
			if (setData.size() > 0) {
				c.addProfileSets(setData.values());
				return true;
			}
		} catch (Exception e) {
			logger.warn("Could not find profile data for product with ID: " + c.getProductId().toString());
		}
		return false;
	}

	@Override
	public HashMap getProfileSetsByIds(List ids) {
		if (!this.getIsConnected()) {
			login();
		}
		HashMap profileSets = new HashMap<>();
		
		// First get the profileset data that is independent of the product card
		String idsString = String.join(",", ids.stream().map(id -> id.toString()).collect(Collectors.toList()));
		List params = new ArrayList();
		params.add(new KeyValuePair("ZoekDatum", dbDateFormat.format(this.getRequestDate().getTime())));
		params.add(new KeyValuePair("ProductIDs", idsString));
		params.add(new KeyValuePair("includeNULLs", true));

		HttpResponse response = this.performGetRequestWithParams(this.apiPath + "ProductenProfielWaarden", params);

		if (response == null) return profileSets;
		// do something with the entity to get the token
		JsonNode resp_node = this.responseToJson(response);

		if (resp_node.get("results") != null) {
			if (!(resp_node.get("results") instanceof TextNode)) {
				JsonNode profielSetNodes = resp_node.get("results").get(0).get("ProfielSet");
				profielSetNodes.forEach(profielSetNode -> {

					Integer profielSetId = TryParseJsonNode(profielSetNode.get("ProfielSetID"), -1);

					// load set specific data
					NmdProfileSetImpl set = this.getDetailedProfielSetData(profielSetNode);

					profileSets.put(profielSetId, set);
				});
			}
		}

		// per product card it also needs the quantity and scaler information
		if (profileSets.size() > 0) {
			HashMap> ratios = this.getQuantitiesOfProfileSetsForProduct(ids.get(0));
			profileSets.values().forEach(ps -> {
				Integer id = ps.getProfielId();
				if (ratios.containsKey(id)) {
					((NmdProfileSetImpl) ps).setQuantity(ratios.get(id).getKey());
					((NmdProfileSetImpl) ps).setScaler(ratios.get(id).getValue());
					((NmdProfileSetImpl) ps).setIsScalable(true);
				}
			});
		}
		return profileSets;
	}

	private void loadFaseProfielDataForSet(JsonNode profielSetNode, NmdProfileSetImpl set) {
		// laad faseprofiel specifieke data
		JsonNode profielen = profielSetNode.get("Profiel");
		if (profielen != null) {
			profielen.forEach(p -> {
				Integer fase = TryParseJsonNode(p.get("FaseID"), -1);
				String faseName = this.getResources().getFaseMapping().get(fase);

				NmdFaseProfielImpl profiel = new NmdFaseProfielImpl(faseName, this.getResources());

				p.get("ProfielMilieuEffecten").forEach(val -> {
					Integer catId = TryParseJsonNode(val.get("MilieuCategorieID"), -1);
					Double catVal = TryParseJsonNode(val.get("MilieuWaarde"), Double.NaN);

					profiel.setProfielCoefficient(
							this.getResources().getMilieuCategorieMapping().get(catId).getDescription(), catVal);
				});

				set.addFaseProfiel(faseName, profiel);
			});
		}
	}

	private NmdProfileSetImpl getDetailedProfielSetData(JsonNode psNode) {
		NmdProfileSetImpl set = new NmdProfileSetImpl();
		set.setProfileLifetime(TryParseJsonNode(psNode.get("Levensduur"), -1));
		set.setUnit(this.getResources().getUnitMapping().get(TryParseJsonNode(psNode.get("ProfielSetEenheidID"), -1)));

		set.setProfielId(TryParseJsonNode(psNode.get("ProfielSetID"), -1));
		set.setName(TryParseJsonNode(psNode.get("ProfielSetNaam"), ""));

		this.loadFaseProfielDataForSet(psNode, set);

		return set;
	}

	/*
	 * Try to get a set of profiel set data from the json node to populate the
	 * NmdProfielSetobject
	 */
	private NmdProductCardImpl getProductCardDataFromJson(JsonNode prodNode) {
		NmdProductCardImpl prod = new NmdProductCardImpl();

		prod.setLifetime(TryParseJsonNode(prodNode.get("Levensduur"), -1));
		prod.setUnit(
				this.getResources().getUnitMapping().get(TryParseJsonNode(prodNode.get("FunctioneleEenheidID"), -1)));
		prod.setProductId(TryParseJsonNode(prodNode.get("ProductID"), -1));
		prod.setParentProductId(TryParseJsonNode(prodNode.get("OuderProductID"), -1));

		prod.setIsTotaalProduct(TryParseJsonNode(prodNode.get("IsElementDekkend"), false));
		prod.setDescription(TryParseJsonNode(prodNode.get("ProductNaam"), ""));
		prod.setCategory(TryParseJsonNode(prodNode.get("CategorieID"), 3));

		return prod;
	}

	/*
	 * Get the essential product card info from an element JsonNode. With this info
	 * profielSet info can be retrieved
	 */
	private NmdElement getElementDataFromJson(JsonNode elementInfo) {
		NmdElementImpl newElement = new NmdElementImpl();
		newElement.setElementId(TryParseJsonNode(elementInfo.get("ElementID"), -1));

		// TODO: Omit any GWW elements?
		//	if (!TryParseJsonNode(elementInfo.get("ElementCode"), "").startsWith("gww")) { return null}
		if (elementInfo.has("PureElementCode")) {
			newElement.setNlsfbCode(new NlsfbCode(TryParseJsonNode(elementInfo.get("PureElementCode"), "")));
		} else {
			String elementCode = TryParseJsonNode(elementInfo.get("ElementCode"), "");
			String cleanedCode = elementCode.substring(0, elementCode.lastIndexOf("."));
			newElement.setNlsfbCode(new NlsfbCode(cleanedCode));
		}

		newElement.setElementName(TryParseJsonNode(elementInfo.get("ElementNaam"), ""));
		newElement.setParentId(TryParseJsonNode(elementInfo.get("OuderID"), -1));
		newElement.setIsMandatory(TryParseJsonNode(elementInfo.get("Verplicht"), false));

		return newElement;
	}
	
	private NmdScaler getScalerFromJson(JsonNode psNode) {
		int scalerType = TryParseJsonNode(psNode.get("SchalingsFormuleID"), -1);
		if (scalerType > 0) {
			String scalerDescription = TryParseJsonNode(psNode.get("OmschrijvingSchalingsmaat"), "");
			double scalerDim1 = TryParseJsonNode(psNode.get("SchalingsMaat_X1"), Double.NaN);
			double scalerDim2 = TryParseJsonNode(psNode.get("SchalingsMaat_X2"), Double.NaN);
			int scalerUnit = TryParseJsonNode(psNode.get("EenheidID_SchalingsMaat"), -1);

			double scalerMinDim1 = TryParseJsonNode(psNode.get("SchalingMinX1"), Double.NaN);
			double scalerMinDim2 = TryParseJsonNode(psNode.get("SchalingMinX2"), Double.NaN);
			double scalerMaxDim1 = TryParseJsonNode(psNode.get("SchalingMaxX1"), Double.NaN);
			double scalerMaxDim2 = TryParseJsonNode(psNode.get("SchalingMaxX2"), Double.NaN);

			JsonNode profNode = psNode.get("ProfielSetInfo").get(0);
			double scalerCoeffA = TryParseJsonNode(profNode.get("SchalingsFormuleA"), Double.NaN);
			double scalerCoeffB = TryParseJsonNode(profNode.get("SchalingsFormuleB"), Double.NaN);
			double scalerCoeffC = TryParseJsonNode(profNode.get("SchalingsFormuleC"), Double.NaN);

			String scalerTypeName = this.getResources().getScalingFormula().get(scalerType);
			String scalerUnitName = this.getResources().getUnitMapping().get(scalerUnit);
			NmdScaler scaler = null;
			try {
				scaler = scalerFactory.create(scalerTypeName, scalerDescription, scalerUnitName,
						new Double[] { scalerCoeffA, scalerCoeffB, scalerCoeffC },
						new Double[] { scalerMinDim1, scalerMaxDim1, scalerMinDim2, scalerMaxDim2 },
						new Double[] { scalerDim1, scalerDim2 });

			} catch (InvalidInputException e) {
				logger.error("encountered invalid input combinations in scaler creation");
			}
			return scaler;
		}
		return null;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy