001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Tiago Cardoso <tcardoso@nuxeo.com> 018 */ 019package org.nuxeo.ecm.platform.threed.convert; 020 021import org.apache.commons.io.FileUtils; 022import org.apache.commons.io.FilenameUtils; 023import org.apache.commons.io.IOUtils; 024import org.nuxeo.common.Environment; 025import org.nuxeo.common.utils.Path; 026import org.nuxeo.ecm.core.api.Blob; 027import org.nuxeo.ecm.core.api.CloseableFile; 028import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 029import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties; 030import org.nuxeo.ecm.core.api.impl.blob.FileBlob; 031import org.nuxeo.ecm.platform.commandline.executor.api.*; 032import org.nuxeo.ecm.core.convert.api.ConversionException; 033import org.nuxeo.ecm.platform.convert.plugins.CommandLineBasedConverter; 034import org.nuxeo.runtime.api.Framework; 035 036import java.io.*; 037import java.util.*; 038import java.util.stream.Collectors; 039import java.util.zip.ZipEntry; 040import java.util.zip.ZipInputStream; 041 042import static org.nuxeo.ecm.platform.threed.ThreeDConstants.SUPPORTED_EXTENSIONS; 043import static org.nuxeo.ecm.platform.threed.convert.Constants.*; 044 045/** 046 * Base converter for blender pipeline. Processes scripts for operators and input blobs 047 * 048 * @since 8.4 049 */ 050public abstract class BaseBlenderConverter extends CommandLineBasedConverter { 051 052 public static final String MIMETYPE_ZIP = "application/zip"; 053 054 protected Path tempDirectory(Map<String, Serializable> parameters, String sufix) throws ConversionException { 055 Path directory = new Path(getTmpDirectory(parameters)).append(BLENDER_PATH_PREFIX + UUID.randomUUID() + sufix); 056 boolean dirCreated = new File(directory.toString()).mkdirs(); 057 if (!dirCreated) { 058 throw new ConversionException("Unable to create tmp dir: " + directory); 059 } 060 return directory; 061 } 062 063 protected boolean isThreeDFile(File file) { 064 return SUPPORTED_EXTENSIONS.contains(FilenameUtils.getExtension(file.getName())); 065 } 066 067 private List<String> unpackZipFile(final File file, final File directory) throws IOException { 068 /* Extract ZIP contents */ 069 List<String> expandedFiles = new ArrayList<>(); 070 ZipEntry zipEntry; 071 ZipInputStream zipInputStream = null; 072 try { 073 zipInputStream = new ZipInputStream(new FileInputStream(file)); 074 while ((zipEntry = zipInputStream.getNextEntry()) != null) { 075 final File destFile = new File(directory, zipEntry.getName()); 076 if (!zipEntry.isDirectory()) { 077 try (FileOutputStream destOutputStream = new FileOutputStream(destFile)) { 078 IOUtils.copy(zipInputStream, destOutputStream); 079 } 080 zipInputStream.closeEntry(); 081 if (isThreeDFile(destFile)) { 082 expandedFiles.add(0, destFile.getAbsolutePath()); 083 } else { 084 expandedFiles.add(destFile.getAbsolutePath()); 085 } 086 } else { 087 destFile.mkdirs(); 088 } 089 } 090 } finally { 091 if (zipInputStream != null) { 092 zipInputStream.close(); 093 } 094 095 } 096 return expandedFiles; 097 } 098 099 protected List<String> blobsToTempDir(BlobHolder blobHolder, Path directory) throws IOException { 100 List<Blob> blobs = blobHolder.getBlobs(); 101 List<String> filesCreated = new ArrayList<>(); 102 if (blobs.isEmpty()) { 103 return filesCreated; 104 } 105 106 if (MIMETYPE_ZIP.equals(blobs.get(0).getMimeType())) { 107 filesCreated.addAll(unpackZipFile(blobs.get(0).getFile(), new File(directory.toString()))); 108 blobs = blobs.subList(1, blobs.size()); 109 } 110 111 // Add the main and assets as params 112 // The params are not used by the command but the blobs are extracted to files and managed! 113 filesCreated.addAll(blobs.stream().map(blob -> { 114 File file = new File(directory.append(blob.getFilename()).toString()); 115 try { 116 blob.transferTo(file); 117 } catch (IOException e) { 118 throw new UncheckedIOException(e); 119 } 120 return file.getAbsolutePath(); 121 }).collect(Collectors.toList())); 122 123 filesCreated.add(directory.toString()); 124 return filesCreated; 125 } 126 127 private void createPath(Path path) throws ConversionException { 128 File pathFile = new File(path.toString()); 129 if (!pathFile.exists()) { 130 boolean dirCreated = pathFile.mkdir(); 131 if (!dirCreated) { 132 throw new ConversionException("Unable to create tmp dir for scripts output: " + path); 133 } 134 } 135 } 136 137 private Path copyScript(Path pathDst, Path source) throws IOException { 138 String sourceFile = source.lastSegment(); 139 // xxx : find a way to check if the correct version is already there. 140 File script = new File(pathDst.append(sourceFile).toString()); 141 InputStream is = getClass().getResourceAsStream("/" + source.toString()); 142 FileUtils.copyInputStreamToFile(is, script); 143 return new Path(script.getAbsolutePath()); 144 } 145 146 /** 147 * Returns the absolute path to the main script (main and pipeline scripts). Copies the script to the filesystem if 148 * needed. Copies needed {@code operators} script to the filesystem if missing. 149 */ 150 protected Path getScriptWith(List<String> operators) throws IOException { 151 Path dataPath = new Path(Environment.getDefault().getData().getAbsolutePath()); 152 String sourceDir = initParameters.get(SCRIPTS_DIR_PARAMETER); 153 String sourceFile = initParameters.get(SCRIPT_FILE_PARAMETER); 154 155 // create scripts directory 156 Path scriptsPath = dataPath.append(SCRIPTS_DIRECTORY); 157 createPath(scriptsPath); 158 159 copyScript(scriptsPath, new Path(sourceDir).append(sourceFile)); 160 161 if (operators.isEmpty()) { 162 return scriptsPath; 163 } 164 165 // create pipeline scripts directory 166 Path pipelinePath = scriptsPath.append(SCRIPTS_PIPELINE_DIRECTORY); 167 createPath(pipelinePath); 168 169 Path pipelineSourcePath = new Path(sourceDir).append("pipeline"); 170 // copy operators scripts resources 171 operators.forEach(operator -> { 172 try { 173 copyScript(pipelinePath, pipelineSourcePath.append(operator + ".py")); 174 } catch (IOException e) { 175 throw new UncheckedIOException(e); 176 } 177 }); 178 179 return scriptsPath; 180 } 181 182 private List<String> getParams(Map<String, Serializable> inParams, Map<String, String> initParams, String key) { 183 String values = ""; 184 if (inParams.containsKey(key)) { 185 values = (String) inParams.get(key); 186 } else if (initParams.containsKey(key)) { 187 values = initParams.get(key); 188 } 189 return Arrays.asList(values.split(" ")); 190 } 191 192 @Override 193 public BlobHolder convert(BlobHolder blobHolder, Map<String, Serializable> parameters) throws ConversionException { 194 String dataContainer = "data" + String.valueOf(Calendar.getInstance().getTime().getTime()); 195 String convertContainer = "convert" + String.valueOf(Calendar.getInstance().getTime().getTime()); 196 String commandName = getCommandName(blobHolder, parameters); 197 if (commandName == null) { 198 throw new ConversionException("Unable to determine target CommandLine name"); 199 } 200 201 List<Closeable> toClose = new ArrayList<>(); 202 Path inDirectory = tempDirectory(null, "_in"); 203 try { 204 CmdParameters params = new CmdParameters(); 205 206 // Deal with operators and script files (blender and pipeline) 207 List<String> operatorsList = getParams(parameters, initParameters, OPERATORS_PARAMETER); 208 params.addNamedParameter(OPERATORS_PARAMETER, operatorsList); 209 210 operatorsList = operatorsList.stream().distinct().collect(Collectors.toList()); 211 Path mainScriptDir = getScriptWith(operatorsList); 212 // params.addNamedParameter(SCRIPTS_DIR_PARAMETER, mainScriptDir.toString()); 213 params.addNamedParameter(SCRIPT_FILE_PARAMETER, initParameters.get(SCRIPT_FILE_PARAMETER)); 214 215 // Initialize render id params 216 params.addNamedParameter(RENDER_IDS_PARAMETER, getParams(parameters, initParameters, RENDER_IDS_PARAMETER)); 217 218 // Initialize LOD id params 219 params.addNamedParameter(LOD_IDS_PARAMETER, getParams(parameters, initParameters, LOD_IDS_PARAMETER)); 220 221 // Initialize percentage polygon params 222 params.addNamedParameter(PERC_POLY_PARAMETER, getParams(parameters, initParameters, PERC_POLY_PARAMETER)); 223 224 // Initialize max polygon params 225 params.addNamedParameter(MAX_POLY_PARAMETER, getParams(parameters, initParameters, MAX_POLY_PARAMETER)); 226 227 // Initialize spherical coordinates params 228 params.addNamedParameter(COORDS_PARAMETER, getParams(parameters, initParameters, COORDS_PARAMETER)); 229 230 // Initialize dimension params 231 params.addNamedParameter(DIMENSIONS_PARAMETER, getParams(parameters, initParameters, DIMENSIONS_PARAMETER)); 232 233 // Deal with input blobs (main and assets) 234 List<String> inputFiles = blobsToTempDir(blobHolder, inDirectory); 235 Path mainFile = new Path(inputFiles.get(0)); 236 // params.addNamedParameter(INPUT_DIR_PARAMETER, mainFile.removeLastSegments(1).toString() ); 237 params.addNamedParameter(INPUT_FILE_PARAMETER, mainFile.lastSegment()); 238 239 // Extra blob parameters 240 Map<String, Blob> blobParams = getCmdBlobParameters(blobHolder, parameters); 241 242 ExecResult createRes = DockerHelper.CreateContainer(dataContainer, "nuxeo/blender"); 243 if (createRes == null || !createRes.isSuccessful()) { 244 throw new ConversionException("Unable to create data volume : " + dataContainer, 245 (createRes != null) ? createRes.getError() : null); 246 } 247 ExecResult copyRes = DockerHelper.CopyData( 248 mainFile.removeLastSegments(1).toString() + File.separatorChar + ".", dataContainer + ":/in/"); 249 if (copyRes == null || !copyRes.isSuccessful()) { 250 throw new ConversionException("Unable to copy content to data volume : " + dataContainer, 251 (copyRes != null) ? copyRes.getError() : null); 252 } 253 copyRes = DockerHelper.CopyData(mainScriptDir.toString() + File.separatorChar + ".", 254 dataContainer + ":/scripts/"); 255 if (copyRes == null || !copyRes.isSuccessful()) { 256 throw new ConversionException("Unable to copy to scripts data volume : " + dataContainer, 257 (copyRes != null) ? copyRes.getError() : null); 258 } 259 params.addNamedParameter(NAME_PARAM, convertContainer); 260 params.addNamedParameter(DATA_PARAM, dataContainer); 261 262 if (blobParams != null) { 263 for (String blobParamName : blobParams.keySet()) { 264 Blob blob = blobParams.get(blobParamName); 265 // closed in finally block 266 CloseableFile closeable = blob.getCloseableFile( 267 "." + FilenameUtils.getExtension(blob.getFilename())); 268 params.addNamedParameter(blobParamName, closeable.getFile()); 269 toClose.add(closeable); 270 } 271 } 272 273 // Extra string parameters 274 Map<String, String> strParams = getCmdStringParameters(blobHolder, parameters); 275 276 if (strParams != null) { 277 for (String paramName : strParams.keySet()) { 278 if (RENDER_IDS_PARAMETER.equals(paramName) || LOD_IDS_PARAMETER.equals(paramName) 279 || PERC_POLY_PARAMETER.equals(paramName) || MAX_POLY_PARAMETER.equals(paramName) 280 || COORDS_PARAMETER.equals(paramName) || DIMENSIONS_PARAMETER.equals(paramName)) { 281 if (strParams.get(paramName) != null) { 282 params.addNamedParameter(paramName, Arrays.asList(strParams.get(paramName).split(" "))); 283 } 284 } else { 285 params.addNamedParameter(paramName, strParams.get(paramName)); 286 } 287 } 288 } 289 290 // Deal with output directory 291 Path outDir = tempDirectory(null, "_out"); 292 params.addNamedParameter(OUT_DIR_PARAMETER, outDir.toString()); 293 294 ExecResult result = Framework.getService(CommandLineExecutorService.class).execCommand(commandName, params); 295 if (!result.isSuccessful()) { 296 throw result.getError(); 297 } 298 299 copyRes = DockerHelper.CopyData(dataContainer + ":/out/.", outDir.toString()); 300 if (copyRes == null || !copyRes.isSuccessful()) { 301 throw new ConversionException("Unable to copy from data volume : " + dataContainer, 302 (copyRes != null) ? copyRes.getError() : null); 303 } 304 return buildResult(result.getOutput(), params); 305 } catch (CommandNotAvailable e) { 306 // XXX bubble installation instructions 307 throw new ConversionException("Unable to find targetCommand", e); 308 } catch (IOException | CommandException e) { 309 throw new ConversionException("Error while converting via CommandLineService", e); 310 } finally { 311 FileUtils.deleteQuietly(new File(inDirectory.toString())); 312 for (Closeable closeable : toClose) { 313 IOUtils.closeQuietly(closeable); 314 } 315 DockerHelper.RemoveContainer(dataContainer); 316 DockerHelper.RemoveContainer(convertContainer); 317 } 318 319 } 320 321 public List<String> getConversionLOD(String outDir) { 322 File directory = new File(outDir); 323 String[] files = directory.list((dir, name) -> true); 324 return (files == null) ? Collections.emptyList() : Arrays.asList(files); 325 } 326 327 public List<String> getRenders(String outDir) { 328 File directory = new File(outDir); 329 String[] files = directory.list((dir, name) -> name.startsWith("render") && name.endsWith(".png")); 330 return (files == null) ? Collections.emptyList() : Arrays.asList(files); 331 } 332 333 public List<String> getInfos(String outDir) { 334 File directory = new File(outDir); 335 String[] files = directory.list((dir, name) -> name.endsWith(".info")); 336 return (files == null) ? Collections.emptyList() : Arrays.asList(files); 337 } 338 339 @Override 340 protected BlobHolder buildResult(List<String> cmdOutput, CmdParameters cmdParams) throws ConversionException { 341 String outDir = cmdParams.getParameter(OUT_DIR_PARAMETER); 342 Map<String, Integer> lodBlobIndexes = new HashMap<>(); 343 List<Integer> resourceIndexes = new ArrayList<>(); 344 Map<String, Integer> infoIndexes = new HashMap<>(); 345 List<Blob> blobs = new ArrayList<>(); 346 347 String lodDir = outDir + File.separatorChar + "convert"; 348 List<String> conversions = getConversionLOD(lodDir); 349 conversions.forEach(filename -> { 350 File file = new File(lodDir + File.separatorChar + filename); 351 Blob blob = new FileBlob(file); 352 blob.setFilename(file.getName()); 353 if (FilenameUtils.getExtension(filename).toLowerCase().equals("dae")) { 354 String[] filenameArray = filename.split("-"); 355 if (filenameArray.length != 4) { 356 throw new ConversionException(filenameArray + " incompatible with conversion file name schema."); 357 } 358 lodBlobIndexes.put(filenameArray[1], blobs.size()); 359 } else { 360 resourceIndexes.add(blobs.size()); 361 } 362 blobs.add(blob); 363 }); 364 365 String infoDir = outDir + File.separatorChar + "info"; 366 List<String> infos = getInfos(infoDir); 367 infos.forEach(filename -> { 368 File file = new File(infoDir + File.separatorChar + filename); 369 Blob blob = new FileBlob(file); 370 blob.setFilename(file.getName()); 371 if (FilenameUtils.getExtension(filename).toLowerCase().equals("info")) { 372 String lodId = FilenameUtils.getBaseName(filename); 373 infoIndexes.put(lodId, blobs.size()); 374 blobs.add(blob); 375 } 376 }); 377 378 String renderDir = outDir + File.separatorChar + "render"; 379 List<String> renders = getRenders(renderDir); 380 381 Map<String, Serializable> properties = new HashMap<>(); 382 properties.put("cmdOutput", (Serializable) cmdOutput); 383 properties.put("resourceIndexes", (Serializable) resourceIndexes); 384 properties.put("infoIndexes", (Serializable) infoIndexes); 385 properties.put("lodIdIndexes", (Serializable) lodBlobIndexes); 386 properties.put("renderStartIndex", blobs.size()); 387 388 blobs.addAll(renders.stream().map(result -> { 389 File file = new File(renderDir + File.separatorChar + result); 390 Blob blob = new FileBlob(file); 391 blob.setFilename(file.getName()); 392 return blob; 393 }).collect(Collectors.toList())); 394 395 return new SimpleBlobHolderWithProperties(blobs, properties); 396 } 397}