| /******************************************************************************** |
| * Copyright (c) 2015-2020 Contributors to the Eclipse Foundation |
| * |
| * See the NOTICE file(s) distributed with this work for additional |
| * information regarding copyright ownership. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| ********************************************************************************/ |
| |
| package org.eclipse.mdm.businessobjects.control; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.URLConnection; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import javax.ejb.EJB; |
| import javax.ejb.Stateless; |
| import javax.inject.Inject; |
| import javax.ws.rs.WebApplicationException; |
| import javax.ws.rs.core.StreamingOutput; |
| |
| import org.eclipse.mdm.api.base.Transaction; |
| import org.eclipse.mdm.api.base.file.FileService; |
| import org.eclipse.mdm.api.base.model.ContextComponent; |
| import org.eclipse.mdm.api.base.model.ContextDescribable; |
| import org.eclipse.mdm.api.base.model.ContextRoot; |
| import org.eclipse.mdm.api.base.model.ContextType; |
| import org.eclipse.mdm.api.base.model.Entity; |
| import org.eclipse.mdm.api.base.model.Environment; |
| import org.eclipse.mdm.api.base.model.FileLink; |
| import org.eclipse.mdm.api.base.model.FileLink.Format; |
| import org.eclipse.mdm.api.base.model.FilesAttachable; |
| import org.eclipse.mdm.api.base.model.Measurement; |
| import org.eclipse.mdm.api.base.model.MimeType; |
| import org.eclipse.mdm.api.base.model.TestStep; |
| import org.eclipse.mdm.api.base.model.Value; |
| import org.eclipse.mdm.api.base.model.ValueType; |
| import org.eclipse.mdm.api.dflt.ApplicationContext; |
| import org.eclipse.mdm.api.dflt.EntityManager; |
| import org.eclipse.mdm.businessobjects.entity.FileSize; |
| import org.eclipse.mdm.businessobjects.service.ContextService; |
| import org.eclipse.mdm.businessobjects.service.DescribableContexts; |
| import org.eclipse.mdm.connector.boundary.ConnectorService; |
| |
| import com.google.common.io.ByteStreams; |
| |
| import io.vavr.Tuple; |
| import io.vavr.Tuple2; |
| |
| @Stateless |
| public class FileLinkActivity { |
| |
| @Inject |
| private ConnectorService connectorService; |
| |
| @EJB |
| private ContextService contextService; |
| |
| @EJB |
| private ContextActivity contextActivity; |
| |
| /** |
| * Loads size of File with given remotePath. |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the file is |
| * attached to |
| * @param remotePath the remotePath |
| * @return the {@link FileSize} |
| */ |
| public FileSize loadFileSize(String sourceName, Entity entity, FileLink fileLink) { |
| FileService fileService = getFileService(sourceName); |
| return loadSize(entity, fileLink, fileService); |
| } |
| |
| /** |
| * Loads sizes for all files attached to the given entity. |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the files |
| * are attached to |
| * @return the {@link FileSize}s as {@link List} |
| */ |
| public List<FileSize> loadFileSizes(String sourceName, FilesAttachable fileAttachable) { |
| FileService fileService = getFileService(sourceName); |
| return Arrays.asList(fileAttachable.getFileLinks()).stream() |
| .map(fileLink -> loadSize(fileAttachable, fileLink, fileService)).collect(Collectors.toList()); |
| } |
| |
| /** |
| * Helping function to trigger the actual file size loading process via given |
| * {@link FileService} |
| * |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the file is |
| * attached to |
| * @param fileLink the {@link FileLink} |
| * @param fileService the {@link FileService} |
| * @return the {@link FileSize} |
| */ |
| private FileSize loadSize(Entity entity, FileLink fileLink, FileService fileService) { |
| FileSize fileSize = new FileSize(); |
| try { |
| fileService.loadSize(entity, fileLink); |
| fileSize.setRemotePath(fileLink.getRemotePath()); |
| fileSize.setSize(fileLink.getSize(Format.BINARY)); |
| } catch (IOException e) { |
| throw new MDMFileAccessException(e.getMessage(), e); |
| } |
| return fileSize; |
| } |
| |
| /** |
| * Persists file at file server. Creates a new {@link FileLink} (pointing at the |
| * new file) and adds it to the {@link FilesAttachable} |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the file is |
| * attached to |
| * @param fileName the name of the file |
| * @param fis the {@code InputStream} with the file data |
| * @param description the file description. Default value is the file name. |
| * @param mimeType the mimeType of the file. |
| * @return the {@link FileLink} to the created File |
| */ |
| public FileLink createFile(String sourceName, FilesAttachable filesAttachable, String fileName, InputStream fis, |
| String description, MimeType mimeType) { |
| |
| FileLink fileLink = null; |
| try { |
| // Create new local FileLink |
| fileLink = newLocalFileLink(fileName, fis, description, mimeType); |
| |
| // Upload fileLink. Upload will create remote path. |
| uploadFile(sourceName, filesAttachable, fileLink); |
| |
| // Add file link to fileAttachable |
| filesAttachable.addFileLink(fileLink); |
| |
| // Persist changes |
| persistEntity(sourceName, filesAttachable); |
| } catch (IOException e) { |
| throw new MDMFileAccessException( |
| String.format("File (%s, %s, %s) could not be created.", fileName, description, mimeType), e); |
| } |
| return fileLink; |
| } |
| |
| /** |
| * Persists file at file server. Creates a new {@link FileLink} (pointing at the |
| * new file) and adds it to the specified context attribute |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param contextDescribable the {@link ContextDescribable} |
| * @param fileName the name of the file |
| * @param fis the {@code InputStream} with the file data |
| * @param description the file description. Default value is the file |
| * name. |
| * @param mimeType the mimeType of the file. |
| * @param contextType the {@link ContextType} holding the component |
| * @param contextComponentName the name of the component holding the file link |
| * attribute |
| * @param attributeName the name of the attribute holding the file link |
| * @return |
| */ |
| public FileLink createFile(String sourceName, ContextDescribable contextDescribable, String fileName, |
| InputStream fis, String description, MimeType mimeType, ContextType contextType, |
| String contextComponentName, String attributeName) { |
| |
| Map<ContextType, ContextRoot> contextMap = getContextMap(sourceName, contextDescribable, contextType); |
| Value value = findValueInContext(contextType, contextComponentName, contextMap, attributeName); |
| |
| FileLink fileLink = null; |
| try { |
| // Create new local FileLink |
| fileLink = newLocalFileLink(fileName, fis, description, mimeType); |
| // Upload file to file server. Upload creates and sets remote path in fileLink. |
| uploadFile(sourceName, contextDescribable, fileLink); |
| |
| // Since attributes of type FILE_LINK can hold a maximum of one file, the old |
| // file has to be deleted from fileServer |
| if (ValueType.FILE_LINK.equals(value.getValueType())) { |
| deleteFile(sourceName, contextDescribable, value.extract(ValueType.FILE_LINK)); |
| } |
| |
| // Adds file link to the context attribute |
| setOrAddFileLinkToValue(value, fileLink); |
| |
| // Persist changes |
| DescribableContexts dc = createDescribableContext(contextDescribable, contextMap); |
| contextService.persist(dc); |
| |
| } catch (IOException e) { |
| throw new MDMFileAccessException( |
| String.format("File (%s, %s, %s) could not be created.", fileName, description, mimeType), e); |
| } |
| return fileLink; |
| } |
| |
| /** |
| * Deletes file from file server and removes the related {@link FileLink} from |
| * the {@link FilesAttachable} |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the file is |
| * attached to |
| * @param remotePath the remotePath |
| * @return the {@link FileLink} to the deleted File |
| */ |
| public FileLink deleteFileLink(String sourceName, FilesAttachable filesAttachable, String remotePath) { |
| |
| // Find file link |
| FileLink fileLink = findFileLinkAtFileAttachable(remotePath, filesAttachable); |
| |
| // Delete file from FileServer |
| deleteFile(sourceName, filesAttachable, fileLink); |
| |
| // Remove FileLink from FileAttachable |
| filesAttachable.removeFileLink(fileLink); |
| |
| // Persist changes |
| persistEntity(sourceName, filesAttachable); |
| |
| return fileLink; |
| } |
| |
| /** |
| * Deletes file from file server and removes the related {@link FileLink} from |
| * the specified context attribute. |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param contextDescribable the context describable |
| * @param contextType the {@link ContextType} holding the component |
| * @param contextComponentName the name of the component holding the file link |
| * attribute |
| * @param attributeName the name of the attribute holding the file link |
| * @param remotePath the remotePath |
| * @return |
| */ |
| public FileLink deleteFileLink(String sourceName, ContextDescribable contextDescribable, ContextType contextType, |
| String contextComponentName, String attributeName, String remotePath) { |
| |
| // Find link attribute in context |
| Map<ContextType, ContextRoot> contextMap = getContextMap(sourceName, contextDescribable, contextType); |
| Value value = findValueInContext(contextType, contextComponentName, contextMap, attributeName); |
| FileLink fileLink = extractFileLink(remotePath, value); |
| |
| // Delete the file from file Server |
| deleteFile(sourceName, contextDescribable, fileLink); |
| |
| // Removes file link from the context attribute |
| removeFileLinkfromValue(value, fileLink); |
| |
| // Persist changes |
| DescribableContexts dc = createDescribableContext(contextDescribable, contextMap); |
| contextService.persist(dc); |
| return fileLink; |
| } |
| |
| /** |
| * Helping function to find the {@link FileLink} with the specified remotePath |
| * attached to the given entity. |
| * |
| * @param remotePath the remotePath |
| * @param filesAttachable the {@link ContextDescribable} the file is attached to |
| * @return the specified {@link FileLink} |
| */ |
| public FileLink findFileLinkInContext(String remotePath, String sourceName, ContextDescribable contextDescribable, |
| ContextType contextType, String contextComponentName, String attributeName) { |
| Map<ContextType, ContextRoot> contextMap = getContextMap(sourceName, contextDescribable, contextType); |
| Value value = findValueInContext(contextType, contextComponentName, contextMap, attributeName); |
| return extractFileLink(remotePath, value); |
| } |
| |
| /** |
| * Helping function to find the {@link FileLink} with the specified remotePath |
| * attached to the given entity. |
| * |
| * @param remotePath the remotePath |
| * @param filesAttachable the {@link FilesAttachable} the file is attached to |
| * @return the specified {@link FileLink} |
| */ |
| public FileLink findFileLinkAtFileAttachable(String remotePath, FilesAttachable entity) { |
| for (FileLink l : ((FilesAttachable) entity).getFileLinks()) { |
| if (l.isRemote() && l.getRemotePath().equals(remotePath)) { |
| return l; |
| } |
| } |
| throw new MDMEntityAccessException("FileLink with remotePath " + remotePath + " not found!"); |
| } |
| |
| public StreamingOutput toStreamingOutput(String sourceName, Entity entity, FileLink fileLink) { |
| return new StreamingOutput() { |
| @Override |
| public void write(OutputStream output) throws IOException, WebApplicationException { |
| streamFileLink(sourceName, entity, fileLink, output); |
| } |
| }; |
| } |
| |
| /** |
| * Creates {@link StreamingOutput} for file. Guesses mime-type to ensure proper |
| * display/download functionality at client side. |
| * |
| * @TODO Mime-type guess works only for limited types, but CorbaFileServer does |
| * not offer methods to properly load mime-type. Find a solution! |
| * |
| * @param sourceName |
| * @param entity |
| * @param fileLink |
| * @return |
| */ |
| public Tuple2<StreamingOutput, String> toStreamingOutputGuessMimeType(String sourceName, Entity entity, |
| FileLink fileLink) { |
| String m = null; |
| StreamingOutput o = null; |
| try { |
| InputStream in = getFileService(sourceName).openStream(entity, fileLink); |
| m = URLConnection.guessContentTypeFromStream(in); |
| o = new StreamingOutput() { |
| public void write(OutputStream output) throws IOException, WebApplicationException { |
| ByteStreams.copy(in, output); |
| } |
| }; |
| } catch (IOException e) { |
| throw new MDMFileAccessException(e.getMessage(), e); |
| } |
| return Tuple.of(o, m); |
| } |
| |
| /** |
| * Helping function to physically delete file from file server |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param entity the {@link Entity} the file is attached to or holding the |
| * context with the file attribute |
| * @param fileLink the {@link FileLink} related to the file |
| */ |
| private void deleteFile(String sourceName, Entity entity, FileLink fileLink) { |
| FileService fileService = getFileService(sourceName); |
| fileService.delete(entity, fileLink); |
| } |
| |
| /** |
| * Helping function to physically upload file to file server |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param entity the {@link Entity} the file is attached to or holding the |
| * context with the file attribute |
| * @param fileLink the {@link FileLink} related to the file |
| */ |
| private void uploadFile(String sourceName, Entity entity, FileLink fileLink) throws IOException { |
| FileService fileService = getFileService(sourceName); |
| List<FileLink> fileLinks = new ArrayList<>(); |
| fileLinks.add(fileLink); |
| fileService.uploadSequential(entity, fileLinks, null); |
| } |
| |
| /** |
| * Helping function to persist an {@link Entity} |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param entity the {@link Entity} the file is attached to or holding the |
| * context with the file attribute |
| */ |
| private void persistEntity(String sourceName, Entity entity) { |
| List<Entity> enteties = new ArrayList<>(); |
| enteties.add(entity); |
| |
| EntityManager em = getEntityManager(sourceName); |
| Transaction t = em.startTransaction(); |
| t.update(enteties); |
| t.commit(); |
| } |
| |
| /** |
| * Helping function to create new local {@FileLink}. Sets filename as default |
| * description, if no description is provided. |
| * |
| * @param fileName the file name |
| * @param fis the file {@link InputStream} |
| * @param description the file description |
| * @param mimeType the files {@link MimeType} |
| * @return the new {@FileLink} |
| * @throws IOException |
| */ |
| private FileLink newLocalFileLink(String fileName, InputStream fis, String description, MimeType mimeType) |
| throws IOException { |
| String desc = (description == null || description.isEmpty()) ? fileName : description; |
| return FileLink.newLocal(fis, fileName, -1, mimeType, desc); |
| } |
| |
| /** |
| * Helping function to load context for {@link ContextDescribable}s. |
| * |
| * Notice: For {@link TestStep}s only the ordered context is loaded. For |
| * {@link Measurement}s only the measured context is loaded. |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param contextDescribable the {@link ContextDescribable} |
| * @param contextType the {@link ContextType} holding the component |
| * @return |
| */ |
| private Map<ContextType, ContextRoot> getContextMap(String sourceName, ContextDescribable contextDescribable, |
| ContextType contextType) { |
| if (contextDescribable instanceof TestStep) { |
| return contextActivity.getTestStepContext(sourceName, contextDescribable.getID(), contextType) |
| .get(ContextActivity.CONTEXT_GROUP_ORDERED); |
| } else if (contextDescribable instanceof Measurement) { |
| return contextActivity.getMeasurementContext(sourceName, contextDescribable.getID(), contextType) |
| .get(ContextActivity.CONTEXT_GROUP_MEASURED); |
| } |
| throw new MDMEntityAccessException( |
| "No context of type " + contextType + " not found for " + contextDescribable.toString()); |
| } |
| |
| /** |
| * Helping function to add {@link FileLink} to a {@link Value} |
| * |
| * @param value the {@link Value} |
| * @param fileLink the {@link FileLink} |
| */ |
| private void setOrAddFileLinkToValue(Value value, FileLink fileLink) { |
| if (ValueType.FILE_LINK.equals(value.getValueType())) { |
| value.set(fileLink); |
| } else if (ValueType.FILE_LINK_SEQUENCE.equals(value.getValueType())) { |
| // delete if exists or create if not exists. |
| FileLink[] old = value.extract(ValueType.FILE_LINK_SEQUENCE); |
| FileLink[] current = Arrays.copyOf(old, old.length + 1); |
| current[old.length] = fileLink; |
| value.set(current); |
| } |
| } |
| |
| /** |
| * Helping function to remove {@link FileLink} from a {@link Value} |
| * |
| * @param value the {@link Value} |
| * @param fileLink the {@link FileLink} |
| */ |
| private void removeFileLinkfromValue(Value value, FileLink fileLink) { |
| if (ValueType.FILE_LINK.equals(value.getValueType())) { |
| value.set(null); |
| } else if (ValueType.FILE_LINK_SEQUENCE.equals(value.getValueType())) { |
| FileLink[] links = Stream.of(value.extract(ValueType.FILE_LINK_SEQUENCE)) |
| .filter(link -> !link.equals(fileLink)).toArray(FileLink[]::new); |
| value.set(links); |
| } |
| } |
| |
| /** |
| * Helping function to create {@link DescribableContexts} |
| * |
| * @param contextDescribable the {@link ContextDescribable} |
| * @param contextMap the context |
| * @return |
| */ |
| private DescribableContexts createDescribableContext(ContextDescribable contextDescribable, |
| Map<ContextType, ContextRoot> contextMap) { |
| DescribableContexts dc = new DescribableContexts(); |
| dc.setTestStep(contextService.getTestStep(contextDescribable)); |
| if (contextDescribable instanceof TestStep) { |
| dc.setOrdered(contextMap); |
| } |
| if (contextDescribable instanceof Measurement) { |
| List<Measurement> list = new ArrayList<>(); |
| list.add((Measurement) contextDescribable); |
| dc.setMeasurements(list); |
| dc.setMeasured(contextMap); |
| } |
| return dc; |
| } |
| |
| /** |
| * |
| * Helping function to extract {@link Value} from context |
| * |
| * @param contextType |
| * @param componentName |
| * @param typeRootMap |
| * @param attributeName |
| * @return |
| */ |
| private Value findValueInContext(ContextType contextType, String componentName, |
| Map<ContextType, ContextRoot> typeRootMap, String attributeName) { |
| |
| ContextRoot contextRoot = typeRootMap.get(contextType); |
| if (contextRoot != null) { |
| List<ContextComponent> components = contextRoot.getContextComponents().stream() |
| .filter(cc -> cc.getName().equals(componentName)).collect(Collectors.toList()); |
| if (components != null && !components.isEmpty()) { |
| return components.get(0).getValue(attributeName); |
| } |
| } |
| throw new MDMEntityAccessException( |
| "ContextComponent with name " + componentName + " not found in context of type " + contextType); |
| } |
| |
| /** |
| * |
| * @param remotePath |
| * @param value |
| * @return |
| */ |
| private FileLink extractFileLink(String remotePath, Value value) { |
| FileLink fileLink = null; |
| if (ValueType.FILE_LINK.equals(value.getValueType())) { |
| fileLink = value.extract(ValueType.FILE_LINK); |
| } else if (ValueType.FILE_LINK_SEQUENCE.equals(value.getValueType())) { |
| List<FileLink> fileLinks = Stream.of(value.extract(ValueType.FILE_LINK_SEQUENCE)) |
| .filter(link -> link.getRemotePath().equals(remotePath)).collect(Collectors.toList()); |
| fileLink = fileLinks.get(0); |
| } |
| return fileLink; |
| } |
| |
| /** |
| * Opens an {@code InputStream} for given {@link FileLink} and copies it to |
| * {@param outputStream}. |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @param filesAttachable the {@link FilesAttachable} {@link Entity} the file is |
| * attached to |
| * @param fileLink the {@link FileLink} |
| * @param outputStream the {link OutputStream} |
| * @throws IOException Thrown if unable to provide as stream |
| */ |
| private void streamFileLink(String sourceName, Entity entity, FileLink fileLink, OutputStream outputStream) |
| throws IOException { |
| |
| try (InputStream in = getFileService(sourceName).openStream(entity, fileLink)) { |
| ByteStreams.copy(in, outputStream); |
| } |
| } |
| |
| /** |
| * Helping function to load an {@link FileService} |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @return the {@link FileService} |
| */ |
| private FileService getFileService(String sourceName) { |
| ApplicationContext context = this.connectorService.getContextByName(sourceName); |
| return context.getFileService() |
| .orElseThrow(() -> new MDMFileAccessException("FileService not present in '" + sourceName + "'.")); |
| } |
| |
| /** |
| * Helping function to load an {@link EntityManager} |
| * |
| * @param sourceName name of the source (MDM {@link Environment}) |
| * @return the {@link EntityManager} |
| */ |
| private EntityManager getEntityManager(String sourceName) { |
| ApplicationContext context = this.connectorService.getContextByName(sourceName); |
| return context.getEntityManager() |
| .orElseThrow(() -> new MDMEntityAccessException("Entity manager not present in '" + sourceName + "'.")); |
| } |
| } |