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

io.milton.http.json.PutJsonResource Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 io.milton.http.json;

import io.milton.resource.ReplaceableResource;
import io.milton.common.FileUtils;
import io.milton.common.Utils;
import io.milton.event.EventManager;
import io.milton.event.PutEvent;
import io.milton.http.*;
import io.milton.http.Request.Method;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.ConflictException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.resource.DeletableResource;
import io.milton.resource.PostableResource;
import io.milton.resource.PutableResource;
import io.milton.resource.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.sf.json.JSON;
import net.sf.json.JSONSerializer;
import net.sf.json.JsonConfig;
import net.sf.json.util.CycleDetectionStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Will use milton's PUT framework to support file uploads using POST and
 * multipart encoding
 *
 * This will save the uploaded files with their given names into the parent
 * collection resource.
 *
 * If a file already exists with the same name a ConflictException is thrown,
 * unless you set the _autoname request parameter. If this parameter is present
 * (ie with any value) the file will be saved with a non-conflicting file name
 *
 * Save file information is returned as JSON in the response content
 *
 * @author brad
 */
public class PutJsonResource extends JsonResource implements PostableResource {

	private static final Logger log = LoggerFactory.getLogger(PutJsonResource.class);
	public static final String PARAM_AUTONAME = "_autoname";
	public static final String PARAM_NAME = "name";
	public static final String PARAM_OVERWRITE = "overwrite";
	private final EventManager eventManager;
	private final PutableResource wrapped;
	private final String href;
	private List newFiles;
	private String errorMessage;

	public PutJsonResource(PutableResource putableResource, String href, EventManager eventManager) {
		super(putableResource, Request.Method.PUT.code, null);
		this.eventManager = eventManager;
		this.wrapped = putableResource;
		this.href = href;
	}

	@Override
	public String getContentType(String accepts) {
		//String s = "application/x-javascript; charset=utf-8";
		return "text/plain";
		//return "application/json";
	}

	@Override
	public String processForm(Map parameters, Map files) throws ConflictException, NotAuthorizedException, BadRequestException {
		log.info("processForm: " + wrapped.getClass());

		newFiles = new ArrayList<>();

		try {
			if (parameters.containsKey("content")) {
				String name = parameters.get("name");
				String content = parameters.get("content");
				String contentType = parameters.get("Content-Type");
				byte[] arr = toArray(content);
				ByteArrayInputStream in = new ByteArrayInputStream(arr);
				long length = arr.length;
				NewFile nf = new NewFile();
				nf.setOriginalName(name);
				nf.setContentType(contentType);
				nf.setLength(length);
				newFiles.add(nf);

				processFile(name, in, length, contentType, nf);
			}

			for (FileItem file : files.values()) {
				NewFile nf = new NewFile();
				String ua = HttpManager.request().getUserAgentHeader();
				String f = Utils.truncateFileName(ua, file.getName());
				nf.setOriginalName(f);
				nf.setContentType(file.getContentType());
				nf.setLength(file.getSize());
				newFiles.add(nf);
				String newName = getName(f, parameters);

				log.info("creating resource: " + newName + " size: " + file.getSize());
				InputStream in = null;
				try {
					in = file.getInputStream();
					processFile(newName, in, file.getSize(), file.getContentType(), nf);
				} finally {
					FileUtils.close(in);
				}
			}
			log.trace("completed all POST processing");
		} catch (BadRequestException e) {
			log.warn("BadRequestException", e);
			errorMessage = "Bad Request: " + e.getReason();
		} catch (NotAuthorizedException e) {
			log.warn("NotAuthorizedException", e);
			errorMessage = "Not authorised: " + e.getMessage();
		} catch (ConflictException e) {
			log.warn("ConflictException", e);
			errorMessage = "Conflict: " + e.getMessage();
		}
		return null;
	}

	private byte[] toArray(String s) {
		try {
			return s.getBytes("UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

	private void processFile(String newName, InputStream in, Long length, String contentType, NewFile nf) throws BadRequestException, NotAuthorizedException, ConflictException {
		Resource newResource;

		try {
			Resource existing = wrapped.child(newName);
			if (existing != null) {
				if (existing instanceof ReplaceableResource) {
					log.trace("existing resource is replaceable, so replace content");
					ReplaceableResource rr = (ReplaceableResource) existing;
					rr.replaceContent(in, null);
					log.trace("completed POST processing for file. Updated: " + existing.getName());
					eventManager.fireEvent(new PutEvent(rr));
					newResource = rr;
				} else {
					log.trace("existing resource is not replaceable, will be deleted");
					if (existing instanceof DeletableResource) {
						DeletableResource dr = (DeletableResource) existing;
						dr.delete();
						newResource = wrapped.createNew(newName, in, length, contentType);
						log.trace("completed POST processing for file. Deleted, then created: " + newResource.getName());
						eventManager.fireEvent(new PutEvent(newResource));
					} else {
						throw new BadRequestException(existing, "existing resource could not be deleted, is not deletable");
					}
				}
			} else {
				newResource = wrapped.createNew(newName, in, length, contentType);
				if( newResource != null ) {
					log.info("completed POST processing for file. Created: " + newResource.getName());
				} else {
					log.info("completed POST processing for file. null resource returned");
				}
				eventManager.fireEvent(new PutEvent(newResource));
			}
			String newHref = buildNewHref(href, newResource.getName());
			nf.setHref(newHref);
		} catch (IOException ex) {
			throw new RuntimeException("Exception creating resource", ex);
		} finally {
		}
	}

	/**
	 * Returns a JSON representation of the newly created hrefs
	 *
	 * @param out
	 * @param range
	 * @param params
	 * @param contentType
	 * @throws IOException
	 * @throws NotAuthorizedException
	 */
	@Override
	public void sendContent(OutputStream out, Range range, Map params, String contentType) throws IOException, NotAuthorizedException {
		JsonConfig cfg = new JsonConfig();
		cfg.setIgnoreTransientFields(true);
		cfg.setCycleDetectionStrategy(CycleDetectionStrategy.LENIENT);
		Writer writer = new PrintWriter(out);
		if (errorMessage != null) {
			Map map = new HashMap();
			map.put("error", errorMessage);
			JSON json = JSONSerializer.toJSON(map, cfg);
			json.write(writer);

		} else {
			NewFile[] arr;
			if (newFiles != null) {
				arr = new NewFile[newFiles.size()];
				newFiles.toArray(arr);
			} else {
				arr = new NewFile[0];
			}
			JSON json = JSONSerializer.toJSON(arr, cfg);
			json.write(writer);
		}
		writer.flush();
	}

	@Override
	public Method applicableMethod() {
		return Method.PUT;
	}

	/**
	 * We dont return anything, so best not use json
	 *
	 * @param accepts
	 * @return
	 */
//    @Override
//    public String getContentType(String accepts) {
//        return "text/html";
//    }
	private String getName(String filename, Map parameters) throws ConflictException, NotAuthorizedException, BadRequestException {
		String initialName = filename;
		if (parameters.containsKey(PARAM_NAME)) {
			initialName = parameters.get(PARAM_NAME);
		}
		boolean nonBlankName = initialName != null && initialName.trim().length() > 0;
		boolean autoname = (parameters.get(PARAM_AUTONAME) != null);
		boolean overwrite = (parameters.get(PARAM_OVERWRITE) != null);
		if (nonBlankName) {
			Resource child = wrapped.child(initialName);
			if (child == null) {
				log.trace("no existing file with that name");
				return initialName;
			} else {
				if (overwrite) {
					log.trace("file exists, and overwrite parameters is set, so allow overwrite: " + initialName);
					return initialName;
				} else {
					if (!autoname) {
						log.warn("Conflict: Can't create resource with name " + initialName + " because it already exists. To rename automatically use request parameter: " + PARAM_AUTONAME + ", or to overwrite use " + PARAM_OVERWRITE);
						throw new ConflictException(this);
					} else {
						log.trace("file exists and autoname is set, so will find acceptable name");
					}
				}
			}
		} else {
			initialName = getDateAsName("upload");
			log.trace("no name given in request");
		}
		return findAcceptableName(initialName);
	}

	private String getDateAsName(String base) {
		Calendar cal = Calendar.getInstance();
		return base + "_" + cal.get(Calendar.YEAR) + "-" + cal.get(Calendar.MONTH) + "-" + cal.get(Calendar.DAY_OF_MONTH);
	}

	private String findAcceptableName(String initialName) throws ConflictException, NotAuthorizedException, BadRequestException {
		String baseName = FileUtils.stripExtension(initialName);
		String ext = FileUtils.getExtension(initialName);
		return findAcceptableName(baseName, ext, 1);
	}

	private String findAcceptableName(String baseName, String ext, int i) throws ConflictException, NotAuthorizedException, BadRequestException {
		String candidateName = baseName + "_" + i;
		if (ext != null && ext.length() > 0) {
			candidateName += "." + ext;
		}
		if (wrapped.child(candidateName) == null) {
			return candidateName;
		} else {
			if (i < 100) {
				return findAcceptableName(baseName, ext, i + 1);
			} else {
				log.warn("Too many files with similar names: " + candidateName);
				throw new ConflictException(this);
			}
		}
	}

	private String buildNewHref(String href, String newName) {
		String s = href;
		int pos = href.lastIndexOf("_DAV");
		s = s.substring(0, pos - 1);
		if (!s.endsWith("/")) {
			s += "/";
		}
		s += newName;
		return s;
	}

	public static class NewFile {

		private String href;
		private String originalName;
		private long length;
		private String contentType;

		public String getHref() {
			return href;
		}

		public void setHref(String href) {
			this.href = href;
		}

		public String getOriginalName() {
			return originalName;
		}

		public void setOriginalName(String originalName) {
			this.originalName = originalName;
		}

		public long getLength() {
			return length;
		}

		public void setLength(long length) {
			this.length = length;
		}

		public String getContentType() {
			return contentType;
		}

		public void setContentType(String contentType) {
			this.contentType = contentType;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy