/*
* #%L
* ACS AEM Tools Bundle
* %%
* Copyright (C) 2015 Adobe
* %%
* 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.
* #L%
*/
package com.adobe.acs.tools.csv_resource_type_updater.impl;
import com.adobe.acs.tools.csv.impl.CsvUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@SlingServlet(
label = "ACS AEM Tools - CSV Resource Type Updater Servlet",
methods = { "POST" },
resourceTypes = { "acs-tools/components/csv-resource-type-updater" },
selectors = { "update" },
extensions = { "json" }
)
public class CsvResourceTypeUpdateServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(CsvResourceTypeUpdateServlet.class);
private static final int DEFAULT_BATCH_SIZE = 1000;
// 3 to account for Line Termination
private static final int VALID_ROW_LENGTH = 3;
@Override
protected final void doPost(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
final JSONObject jsonResponse = new JSONObject();
final Parameters params = new Parameters(request);
if (params.getFile() != null) {
final long start = System.currentTimeMillis();
final Iterator<String[]> rows = CsvUtil.getRowsFromCsv(params);
try {
request.getResourceResolver().adaptTo(Session.class).getWorkspace().getObservationManager().setUserData("acs-aem-tools.csv-resource-type-updater");
final Result result = this.update(request.getResourceResolver(), params, rows);
if (log.isInfoEnabled()) {
log.info("Updated as TOTAL of [ {} ] resources in {} ms", result.getSuccess().size(),
System.currentTimeMillis() - start);
}
try {
jsonResponse.put("success", result.getSuccess());
jsonResponse.put("failure", result.getFailure());
} catch (JSONException e) {
log.error("Could not serialized results into JSON", e);
this.addMessage(jsonResponse, "Could not serialized results into JSON");
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} catch (Exception e) {
log.error("Could not process CSV type update replacement", e);
this.addMessage(jsonResponse, "Could not process CSV type update. " + e.getMessage());
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} else {
log.error("Could not find CSV file in request.");
this.addMessage(jsonResponse, "CSV file is missing");
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
response.getWriter().print(jsonResponse.toString());
}
/**
* Update all resources that have matching property values with the new values in the CSV.
*
* @param resourceResolver the resource resolver object
* @param params the request params
* @param rows the CSV rows
* @return a list of the resource paths updated
* @throws PersistenceException
*/
private Result update(final ResourceResolver resourceResolver,
final Parameters params,
final Iterator<String[]> rows) throws PersistenceException {
final Result result = new Result();
final Map<String, String> map = new HashMap<String, String>();
while (rows.hasNext()) {
String[] row = rows.next();
if (row.length == VALID_ROW_LENGTH) {
map.put(row[0], row[1]);
log.debug("Adding type translation [ {} ] ~> [ {} ]", row[0], row[1]);
} else {
log.warn("Row {} is malformed", Arrays.asList(row));
}
}
String query = "SELECT * FROM [nt:base] WHERE ";
query += "ISDESCENDANTNODE([" + params.getPath() + "]) AND (";
final List<String> conditions = new ArrayList<String>();
for (String key : map.keySet()) {
conditions.add("[" + params.getPropertyName() + "] = '" + key + "'");
}
query += StringUtils.join(conditions, " OR ");
query += ")";
log.debug("Query: {}", query);
final Iterator<Resource> resources = resourceResolver.findResources(query, "JCR-SQL2");
int count = 0;
while (resources.hasNext()) {
final Resource resource = resources.next();
final ModifiableValueMap properties = resource.adaptTo(ModifiableValueMap.class);
String newValue = map.get(properties.get(params.getPropertyName(), String.class));
if (newValue != null) {
try {
properties.put(params.getPropertyName(), newValue);
result.addSuccess(resource.getPath());
count++;
} catch (Exception e) {
result.addFailure(resource.getPath());
log.warn("Could not update [ {}@" + params.getPropertyName() + " ]", resource.getPath(), e);
}
if (count == DEFAULT_BATCH_SIZE) {
this.save(resourceResolver, count);
count = 0;
}
}
}
this.save(resourceResolver, count);
return result;
}
/**
* Helper for saving changes to the JCR; contains timing logging.
*
* @param resourceResolver the resource resolver
* @param size the number of changes to save
* @throws PersistenceException
*/
private void save(final ResourceResolver resourceResolver, final int size) throws PersistenceException {
if (resourceResolver.hasChanges()) {
final long start = System.currentTimeMillis();
resourceResolver.commit();
if (log.isInfoEnabled()) {
log.info("Imported a BATCH of [ {} ] assets in {} ms", size, System.currentTimeMillis() - start);
}
} else {
log.debug("Nothing to save");
}
}
/**
* Helper method; adds a message to the JSON Response object.
*
* @param jsonObject the JSON object to add the message to
* @param message the message to add.
*/
private void addMessage(final JSONObject jsonObject, final String message) {
try {
jsonObject.put("message", message);
} catch (JSONException e) {
log.error("Could not formulate JSON Response", e);
}
}
/**
* Result to expose success and failure paths to the JSON response.
*/
private class Result {
private List<String> success;
private List<String> failure;
public Result() {
success = new ArrayList<String>();
failure = new ArrayList<String>();
}
public List<String> getSuccess() {
return success;
}
public void addSuccess(String path) {
this.success.add(path);
}
public List<String> getFailure() {
return failure;
}
public void addFailure(String path) {
this.failure.add(path);
}
}
}