blob: 404c693592d523db37205c53d45a475f66f07b6c [file] [log] [blame]
/*
*
* 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 org.apache.royale.compiler.internal.embedding;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.IFunctionDefinition;
import org.apache.royale.compiler.definitions.IFunctionDefinition.FunctionClassification;
import org.apache.royale.compiler.definitions.IGetterDefinition;
import org.apache.royale.compiler.definitions.INamespaceDefinition;
import org.apache.royale.compiler.embedding.EmbedAttribute;
import org.apache.royale.compiler.embedding.IEmbedData;
import org.apache.royale.compiler.embedding.transcoders.ITranscoder;
import org.apache.royale.compiler.internal.definitions.ClassDefinition;
import org.apache.royale.compiler.internal.embedding.transcoders.DataTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.ImageTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.JPEGTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.MovieTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.PBJTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.SoundTranscoder;
import org.apache.royale.compiler.internal.embedding.transcoders.TranscoderBase;
import org.apache.royale.compiler.internal.embedding.transcoders.XMLTranscoder;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.internal.projects.ASProject;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.projects.SourcePathManager;
import org.apache.royale.compiler.internal.scopes.ASScope;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.EmbedInvalidAttributeValueProblem;
import org.apache.royale.compiler.problems.EmbedNoSourceAttributeProblem;
import org.apache.royale.compiler.problems.EmbedQualityValueProblem;
import org.apache.royale.compiler.problems.EmbedScalingGridValueProblem;
import org.apache.royale.compiler.problems.EmbedSourceAttributeDoesNotExistProblem;
import org.apache.royale.compiler.problems.EmbedUnknownAttributeProblem;
import org.apache.royale.compiler.problems.EmbedUnknownMimeTypeProblem;
import org.apache.royale.compiler.problems.EmbedUnrecogniedFileTypeProblem;
import org.apache.royale.compiler.problems.FontEmbeddingNotSupported;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.swc.ISWCFileEntry;
import org.apache.royale.swf.ISWFConstants;
import org.apache.royale.utils.FilenameNormalization;
import org.apache.royale.utils.StringEncoder;
/**
* This is the main class which contains all information extracted from embed
* meta data.
*/
public class EmbedData implements IEmbedData
{
private static final String EMBED_SWC_SEP = "|";
public static class SkinClassInfo
{
public boolean needsIBorder;
public boolean needsIFlexDisplayObject;
public boolean royaleMovieClipOrSprite;
public boolean needsBorderMetrics;
public boolean needsMeasuredHeight;
public boolean needsMeasuredWidth;
public boolean needsMove;
public boolean needsSetActualSize;
protected SkinClassInfo(ICompilerProject project, ClassDefinition definition)
{
needsIBorder = !definition.isInstanceOf(TranscoderBase.CORE_PACKAGE + ".IBorder", project);
needsIFlexDisplayObject = !definition.isInstanceOf(TranscoderBase.CORE_PACKAGE + ".IFlexDisplayObject", project);
royaleMovieClipOrSprite = definition.isInstanceOf(TranscoderBase.CORE_PACKAGE + ".RoyaleMovieClip", project) ||
definition.isInstanceOf(TranscoderBase.CORE_PACKAGE + ".FlexSprite", project);
final INamespaceDefinition qualifier = definition.getNamespaceReference().resolveNamespaceReference(project);
needsBorderMetrics = needsGetter(project, qualifier, definition, "borderMetrics");
needsMeasuredHeight = needsGetter(project, qualifier, definition, "measuredHeight");
needsMeasuredWidth = needsGetter(project, qualifier, definition, "measuredWidth");
needsMove = needsFunction(project, qualifier, definition, "move");
needsSetActualSize = needsFunction(project, qualifier, definition, "setActualSize");
}
private static boolean needsGetter(ICompilerProject project, INamespaceDefinition qualifier, ClassDefinition classDefinition, String baseName)
{
final ASScope scope = classDefinition.getContainedScope();
IDefinition def = scope.getQualifiedPropertyFromDef(project, classDefinition, baseName, qualifier, false);
return (def instanceof IGetterDefinition) ? false : true;
}
private static boolean needsFunction(ICompilerProject project, INamespaceDefinition qualifier, ClassDefinition classDefinition, String baseName)
{
final ASScope scope = classDefinition.getContainedScope();
IDefinition def = scope.getQualifiedPropertyFromDef(project, classDefinition, baseName, qualifier, false);
if (def instanceof IFunctionDefinition)
{
FunctionClassification classification = ((IFunctionDefinition)def).getFunctionClassification();
return (classification == FunctionClassification.CLASS_MEMBER) ? false : true;
}
return false;
}
}
public EmbedData(String containingSourceFilename, String specifiedQName)
{
this.containingSourceFilename = containingSourceFilename;
this.attributes = new HashMap<EmbedAttribute, Object>();
this.specifiedName = specifiedQName;
this.swcSource = null;
this.skinClassInfo = null;
}
private final String containingSourceFilename;
private final HashMap<EmbedAttribute, Object> attributes;
private final String specifiedName;
private TranscoderBase transcoder;
private ISWCFileEntry swcSource;
@SuppressWarnings("unused")
private SkinClassInfo skinClassInfo;
/**
* Add an attribute
*
* @param project containing project
* @param location source location of the attribute
* @param key attribute key
* @param value attribute value
* @param problems any problems with the key or value
* @return true if there was an error
*/
public boolean addAttribute(ICompilerProject project, ISourceLocation location, String key, String value, Collection<ICompilerProblem> problems)
{
boolean hadError = false;
try
{
// a null key means default to source, ie [Embed="image.png"]
if (EmbedAttribute.SOURCE.equals(key) || key == null)
{
// put source resolution problems into a separate collection first
// so that if we fail because there's an octothorpe, and resolve successfully
// later on, we haven't created incorrect problems.
List<ICompilerProblem> resolveProblems = new LinkedList<ICompilerProblem>();
String source = getResolvedSourcePath(project, location, value, resolveProblems);
// could not resolve the source, so check for an octothorpe
// which indicates a file within a SWC
if (source == null)
{
int octothorpe = value.indexOf("#");
if (octothorpe != -1)
{
source = getResolvedSourcePath(project, location, value.substring(0, octothorpe), problems);
String symbol = value.substring(octothorpe + 1);
attributes.put(EmbedAttribute.SYMBOL, symbol);
}
else
{
problems.addAll(resolveProblems);
}
}
if (source != null)
{
attributes.put(EmbedAttribute.SOURCE, source);
// if we have a filename, but the mimeType hasn't been set yet,
// set if from the filename, but override it later on if there
// is an explicit mimeType, as that takes priority
if (!attributes.containsKey(EmbedAttribute.MIME_TYPE))
{
attributes.put(EmbedAttribute.MIME_TYPE, EmbedMIMEType.getMimeTypeFromFilename(source));
}
}
else
{
Collection<ICompilationUnit> referencingCUs = project.getCompilationUnits(containingSourceFilename);
for (ICompilationUnit cu : referencingCUs)
{
((RoyaleProject)project).addUnfoundReferencedSourceFileDependency(value, cu);
}
hadError = true;
}
}
else if (EmbedAttribute.MIME_TYPE.equals(key))
{
attributes.put(EmbedAttribute.MIME_TYPE, EmbedMIMEType.getMimeTypeFromMimeString(value));
}
else if (EmbedAttribute.COMPRESSION.equals(key))
{
attributes.put(EmbedAttribute.COMPRESSION, Boolean.parseBoolean(value));
}
else if (EmbedAttribute.ENCODING.equals(key))
{
attributes.put(EmbedAttribute.ENCODING, value);
}
else if (EmbedAttribute.EXPORT_SYMBOL.equals(key))
{
attributes.put(EmbedAttribute.EXPORT_SYMBOL, value);
}
else if (EmbedAttribute.FLASH_TYPE.equals(key))
{
attributes.put(EmbedAttribute.FLASH_TYPE, Boolean.parseBoolean(value));
}
else if (EmbedAttribute.ORIGINAL.equals(key))
{
attributes.put(EmbedAttribute.ORIGINAL, value);
}
else if (EmbedAttribute.QUALITY.equals(key))
{
double doubleValue = Double.parseDouble(value);
if (doubleValue < 0 || doubleValue > 100)
{
problems.add(new EmbedQualityValueProblem(location, doubleValue));
hadError = true;
}
else
{
Float floatValue = (float)(doubleValue / 100.0);
attributes.put(EmbedAttribute.QUALITY, floatValue);
}
}
else if (EmbedAttribute.SCALE_GRID_BOTTOM.equals(key))
{
Integer intValue = Integer.parseInt(value);
if (intValue.intValue() < 0)
{
problems.add(new EmbedScalingGridValueProblem(location, EmbedAttribute.SCALE_GRID_BOTTOM, intValue.intValue()));
hadError = true;
}
else
{
intValue *= ISWFConstants.TWIPS_PER_PIXEL;
attributes.put(EmbedAttribute.SCALE_GRID_BOTTOM, intValue);
}
}
else if (EmbedAttribute.SCALE_GRID_LEFT.equals(key))
{
Integer intValue = Integer.parseInt(value);
if (intValue.intValue() < 0)
{
problems.add(new EmbedScalingGridValueProblem(location, EmbedAttribute.SCALE_GRID_LEFT, intValue.intValue()));
hadError = true;
}
else
{
intValue *= ISWFConstants.TWIPS_PER_PIXEL;
attributes.put(EmbedAttribute.SCALE_GRID_LEFT, intValue);
}
}
else if (EmbedAttribute.SCALE_GRID_RIGHT.equals(key))
{
Integer intValue = Integer.parseInt(value);
if (intValue.intValue() < 0)
{
problems.add(new EmbedScalingGridValueProblem(location, EmbedAttribute.SCALE_GRID_RIGHT, intValue.intValue()));
hadError = true;
}
else
{
intValue *= ISWFConstants.TWIPS_PER_PIXEL;
attributes.put(EmbedAttribute.SCALE_GRID_RIGHT, intValue);
}
}
else if (EmbedAttribute.SCALE_GRID_TOP.equals(key))
{
Integer intValue = Integer.parseInt(value);
if (intValue.intValue() < 0)
{
problems.add(new EmbedScalingGridValueProblem(location, EmbedAttribute.SCALE_GRID_TOP, intValue.intValue()));
hadError = true;
}
else
{
intValue *= ISWFConstants.TWIPS_PER_PIXEL;
attributes.put(EmbedAttribute.SCALE_GRID_TOP, intValue);
}
}
else if (EmbedAttribute.SKIN_CLASS.equals(key))
{
attributes.put(EmbedAttribute.SKIN_CLASS, value);
if (value == null || value.length() == 0)
{
//problems.add(new EmbedNoSkinClassProblem(location));
hadError = true;
}
if (!attributes.containsKey(EmbedAttribute.MIME_TYPE))
{
attributes.put(EmbedAttribute.MIME_TYPE, EmbedMIMEType.SKIN);
}
// resolve the skin class here, as need to resolve against
// a specific project
IDefinition skinSymbol = project.resolveQNameToDefinition(value);
if (skinSymbol == null)
{
//problems.add(new EmbedNoSkinClassProblem(location));
hadError = true;
}
else
{
// set the file from which the symbol came from to the source
// so we can still detect whether EmbedDatas are equal if
// a symbol resolves to a different class depending on the project
String source = skinSymbol.getContainingFilePath();
attributes.put(EmbedAttribute.SOURCE, source);
assert (skinSymbol instanceof ClassDefinition);
skinClassInfo = new SkinClassInfo(project, (ClassDefinition)skinSymbol);
}
}
else if (EmbedAttribute.SMOOTHING.equals(key))
{
attributes.put(EmbedAttribute.SMOOTHING, Boolean.parseBoolean(value));
}
else if (EmbedAttribute.SYMBOL.equals(key))
{
attributes.put(EmbedAttribute.SYMBOL, value);
}
else if (EmbedAttribute.ADV_ANTI_ALIASING.equals(key) ||
EmbedAttribute.EMBED_AS_CFF.equals(key) ||
EmbedAttribute.FONT_FAMILY.equals(key) ||
EmbedAttribute.FONT_NAME.equals(key) ||
EmbedAttribute.FONT_STYLE.equals(key) ||
EmbedAttribute.FONT_WEIGHT.equals(key) ||
EmbedAttribute.SYSTEM_FONT.equals(key) ||
EmbedAttribute.SOURCE_LIST.equals(key))
{
// silently ignore these, as proper problem will be reported elsewhere
}
else
{
problems.add(new EmbedUnknownAttributeProblem(location, key));
hadError = true;
}
}
catch (NumberFormatException e)
{
problems.add(new EmbedInvalidAttributeValueProblem(location, key, value));
hadError = true;
}
return hadError;
}
/**
* Returns the value of an attribute.
*
* @param attribute An embed attribute.
* @return value of an attribute. null if attribute does not exist
*/
public Object getAttribute(EmbedAttribute attribute)
{
return attributes.get(attribute);
}
/**
* @return All attributes
*/
public EmbedAttribute[] getAttributes()
{
return attributes.keySet().toArray(new EmbedAttribute[attributes.size()]);
}
/**
* @param project The compiler project.
* @param location The source location.
* @param problems The colleciton of compiler projects to which this method should add problems.
* @return true if the transcoder was successfully constructed
*/
public boolean createTranscoder(ICompilerProject project, ISourceLocation location, Collection<ICompilerProblem> problems)
{
// there should always be a source, with the exception of skin embedding, so don't
// create a transcoder in this error state
String source = (String)getAttribute(EmbedAttribute.SOURCE);
if (source == null && getAttribute(EmbedAttribute.SKIN_CLASS) == null)
{
problems.add(new EmbedNoSourceAttributeProblem(location));
return false;
}
// also check that we have a mimetype set, as don't know what transcoder
// to create without it!
EmbedMIMEType mimeType = (EmbedMIMEType)getAttribute(EmbedAttribute.MIME_TYPE);
if (mimeType == null)
{
problems.add(new EmbedUnrecogniedFileTypeProblem(location, source));
return false;
}
Workspace workspace = (Workspace)project.getWorkspace();
switch (mimeType)
{
case JPEG:
case JPG:
case PNG:
case GIF:
{
Boolean compression = (Boolean)getAttribute(EmbedAttribute.COMPRESSION);
Float quality = (Float)getAttribute(EmbedAttribute.QUALITY);
if ((compression != null && compression == true) || quality != null)
{
transcoder = new JPEGTranscoder(this, workspace);
}
else
{
transcoder = new ImageTranscoder(this, workspace);
}
break;
}
case MP3:
{
transcoder = new SoundTranscoder(this, workspace);
break;
}
case FLASH:
{
transcoder = new MovieTranscoder(this, workspace);
break;
}
case PBJ:
{
transcoder = new PBJTranscoder(this, workspace);
break;
}
case OCT_STRM:
{
transcoder = new DataTranscoder(this, workspace);
break;
}
case XML:
{
transcoder = new XMLTranscoder(this, workspace);
break;
}
case SKIN:
{
//transcoder = new SkinTranscoder(this, workspace, skinClassInfo);
break;
}
case TTF:
case TTC:
case OTF:
case FONT:
case DFONT:
{
problems.add(new FontEmbeddingNotSupported(location));
transcoder = null;
break;
}
case TEXT:
case PROPERTIES:
break; // don't need transcoder for text
default:
{
problems.add(new EmbedUnknownMimeTypeProblem(location, mimeType));
transcoder = null;
}
}
if (transcoder == null)
return false;
// there were problems with the transcoder because of attribute settings
// so don't return it, and let the user deal with the errors
if (!transcoder.analyze(location, problems))
{
transcoder = null;
return false;
}
return true;
}
/**
* Returns the qname of the class generated from the EmbedData. The name
* is guaranteed to be unique and not conflict with user space names, unless
* a user defined class has been decorated with the embed metadata, in which
* case, the users class name will be returned.
* @return qname
*/
public String getQName()
{
if (specifiedName != null)
return specifiedName;
String source = (String)getAttribute(EmbedAttribute.SOURCE);
if (swcSource != null)
{
source = EMBED_SWC_SEP.concat(source);
source = swcSource.getContainingSWCPath().concat(source);
}
String filename = FilenameUtils.getName(source);
filename = filename.replace(".", "_");
String qname = filename + "$" + StringEncoder.stringToMD5String(source);
// add the transcoder hashCode to the end of the QName to ensure
// two embed data's with the same source, but different attributes
// don't clash
qname += transcoder.hashCode();
return qname;
}
/**
* Check if the generated class extends another
*
* @return true if another class is extended
*/
public boolean generatedClassExtendsAnother()
{
String baseClassQname = transcoder.getBaseClassQName();
if (baseClassQname.isEmpty())
return false;
return true;
}
/**
* Get the transcoder used by this embed. This can be null if there was
* a problem with the Embed directive
*
* @return transcoder
*/
public final ITranscoder getTranscoder()
{
return transcoder;
}
/**
*
* @return ISWCFileEntry entry to source asset contained within swc. null if not contained within SWC
*/
public final ISWCFileEntry getSWCSource()
{
return swcSource;
}
@Override
public boolean equals(Object o)
{
assert (transcoder != null) : "equals called on EmbedData with null transcoder";
if (!(o instanceof EmbedData))
return false;
// EmbedData's are considered equal if their transcoders are equal
return transcoder.equals(((EmbedData)o).getTranscoder());
}
@Override
public int hashCode()
{
assert (transcoder != null) : "hashCode called on EmbedData with null transcoder";
return transcoder.hashCode();
}
private String getResolvedSourcePath(ICompilerProject project, ISourceLocation location, String sourceValue, Collection<ICompilerProblem> problems)
{
if (sourceValue == null || sourceValue.isEmpty())
{
problems.add(new EmbedNoSourceAttributeProblem(location));
return null;
}
Map<String,String> searchedLocations = new LinkedHashMap<String,String>();
String containingSourcePath = new File(containingSourceFilename).getParent();
String sourceFile = getResolvedSourcePath(project, containingSourcePath,
sourceValue, searchedLocations);
if (sourceFile == null)
{
problems.add(new EmbedSourceAttributeDoesNotExistProblem(location,
sourceValue, searchedLocations));
}
return sourceFile;
}
/**
* Resolve the location to the requested embed asset filename based on the rules.
* 1) Absolute filename
* 2) relative to the containing source file
* 3) the source path (if flash project)
* 4) the library path (if flash project)
* @param containingSourcePath
* @param filename
* @param searchedLocations A map of the locations searched for filename. The
* key is the filename and the value is the id of the message format used to
* format the filename.
* @return The absolute path to the requested filename, or null if not found.
*/
private String getResolvedSourcePath(ICompilerProject project,
String containingSourcePath, String filename,
Map<String,String> searchedLocations)
{
// first check if absolute path
String sourceFile = null;
if (new File(filename).isAbsolute())
{
searchedLocations.put(FilenameNormalization.normalize(filename), "QuotedPath");
sourceFile = SourcePathManager.getSourceFileInPath(null, filename);
if (sourceFile != null)
return sourceFile;
}
// not an absolute path, so try relative to the containing source
if (containingSourcePath != null)
{
File file = new File(containingSourcePath, filename);
searchedLocations.put(FilenameNormalization.normalize(file).getAbsolutePath(),
"QuotedPath");
sourceFile = SourcePathManager.getSourceFileInPath(new File(containingSourcePath), filename);
}
if (sourceFile != null)
return sourceFile;
if (project instanceof ASProject)
{
sourceFile = getResolvedSourcePath((ASProject)project, filename,
searchedLocations);
}
if (project instanceof RoyaleProject)
{
RoyaleProject royaleProject = (RoyaleProject) project;
String packagePath = null;
if((containingSourcePath != null) && !royaleProject.getSourcePath().isEmpty()) {
for (File sourcePath : royaleProject.getSourcePath()) {
if (containingSourcePath.equals(sourcePath.getAbsolutePath()))
{
packagePath = "";
break;
}
if (containingSourcePath.startsWith(sourcePath.getAbsolutePath())) {
packagePath = containingSourcePath.substring(sourcePath.getAbsolutePath().length() + 1);
break;
}
}
}
sourceFile = getResolvedSourcePath((RoyaleProject)project, filename, packagePath,
searchedLocations);
}
return sourceFile;
}
private String getResolvedSourcePath(ASProject project, String filename,
Map<String,String> searchedLocations)
{
// Only files that start with a leading "/" are resolved using the
// source path.
String sourceFile = null;
boolean isAbsolute = filename.startsWith("/");
if (isAbsolute)
{
searchedLocations.put(filename.substring(1), "EmbedOnSourcePath");
sourceFile = project.getSourceFileFromSourcePath(filename.substring(1));
}
if (sourceFile != null)
return sourceFile;
// Not in the source path, so finally look for the file within the libraries.
// Absolute files are not looked up using the library path.
if (!isAbsolute)
{
searchedLocations.put(filename, "EmbedOnLibraryPath");
swcSource = project.getSourceFileFromLibraryPath(filename);
if (swcSource != null)
{
sourceFile = swcSource.getPath();
}
}
return sourceFile;
}
private String getResolvedSourcePath(RoyaleProject project, String filename, String packagePath,
Map<String,String> searchedLocations)
{
// Only files that start with a leading "/" are resolved using the
// source path.
String sourceFile = null;
boolean isAbsolute = filename.startsWith("/");
if (isAbsolute)
{
searchedLocations.put(filename.substring(1), "EmbedOnSourcePath");
sourceFile = project.getSourceFileFromSourcePath(filename.substring(1));
}
if (sourceFile != null)
return sourceFile;
// Not in the source path, so finally look for the file within the libraries.
// Absolute files are not looked up using the library path.
if (!isAbsolute)
{
for(File sourceDirectory : project.getSourcePath()) {
File potentialFile = sourceDirectory;
if(packagePath != null) {
potentialFile = new File(potentialFile, packagePath);
}
searchedLocations.put(filename, potentialFile.getAbsolutePath());
potentialFile = new File(potentialFile, filename);
if(potentialFile.exists()) {
return potentialFile.getAbsolutePath();
}
}
searchedLocations.put(filename, "EmbedOnLibraryPath");
swcSource = project.getSourceFileFromLibraryPath(filename);
if (swcSource != null)
{
sourceFile = swcSource.getPath();
}
}
return sourceFile;
}
}