// Copyright (c) 2006 by Leif Frenzel <himself@leiffrenzel.de> // All rights reserved. package net.sf.eclipsefp.haskell.core.cabalmodel; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.io.Writer; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import net.sf.eclipsefp.haskell.util.FileUtil; import net.sf.eclipsefp.haskell.util.PlatformUtil; /** <p>represents a stanza in a package description.</p> * * @author Leif Frenzel */ public class PackageDescriptionStanza { private static class CabalSyntaxMap<V> extends LinkedHashMap<String, V>{ /** * */ private static final long serialVersionUID = -630958988149146289L; @Override public V get(final Object key) { if (key instanceof String){ return super.get(((String)key).toLowerCase()); } else if (key instanceof CabalSyntax){ return super.get(((CabalSyntax)key).getCabalName().toLowerCase()); } return null; } } private final String name; private int startLine; private int endLine; private int indent; /** * field names to values */ private final Map<String, String> properties=new CabalSyntaxMap<>(); /** * field names to real names (case may be different) */ private final Map<String, String> realNames=new CabalSyntaxMap<>(); /** * field names to positions */ private final Map<String, ValuePosition> positions=new CabalSyntaxMap<>(); private final CabalSyntax type; private final List<PackageDescriptionStanza> stanzas=new LinkedList<>(); /** * this field is kind of a hack, this is used to append properly to the end of a Cabal file * if it doesn't end with a NL */ boolean needNL=false; private final PackageDescription pd; private String realTypeName; PackageDescriptionStanza(final PackageDescription pd,final CabalSyntax type,final String name, final int startLine){ this.type=type; this.name = name; this.startLine = startLine; this.pd=pd; } public PackageDescription getPackageDescription(){ return this.pd; } /** * clone and add the stanza to the given description * @param desc * @return */ public PackageDescriptionStanza move(final PackageDescription desc){ int startLine=desc.getStanzas().get(desc.getStanzas().size()-1).getEndLine()+1; int diff=startLine-getStartLine(); PackageDescriptionStanza npds= move(diff,this,desc); desc.getStanzas().add( npds ); return npds; } private static PackageDescriptionStanza move(final int diff,final PackageDescriptionStanza me,final PackageDescription desc){ PackageDescriptionStanza pds=new PackageDescriptionStanza(desc,me.getType(),me.getName(),me.getStartLine()+diff); pds.endLine=me.endLine+diff; pds.indent=me.indent; pds.realTypeName=me.realTypeName; pds.needNL=me.needNL; pds.properties.putAll( me.properties ); pds.realNames.putAll( me.realNames ); for (String entry : me.positions.keySet()) { ValuePosition vp = me.positions.get( entry ).clone(); vp.diffLine( diff ); pds.positions.put( entry, vp); } for (PackageDescriptionStanza cpds:me.stanzas){ PackageDescriptionStanza ncpds=move(diff,cpds,desc); pds.stanzas.add(ncpds); } return pds; } public void diffLine ( final int diff ) { this.startLine += diff; this.endLine += diff; for (String entry : positions.keySet()) { ValuePosition vp = positions.get( entry ); vp.diffLine( diff ); } for (PackageDescriptionStanza pds:stanzas){ pds.diffLine( diff ); } } public void diffLineIf ( final int diff, final int linePos ) { boolean needPos=false; if (this.startLine>=linePos){ this.startLine += diff; needPos=true; } if (this.endLine>=linePos){ this.endLine += diff; needPos=true; } if ( needPos){ for (String entry : positions.keySet()) { ValuePosition vp = positions.get( entry ); if (vp.getStartLine()>=linePos){ vp.diffLine( diff ); } } } for (PackageDescriptionStanza st:stanzas){ st.diffLineIf( diff, linePos ); } } public void setStartLine ( final int startLine ) { this.startLine = startLine; } public void setEndLine( final int endLine ) { this.endLine = endLine; } public List<PackageDescriptionStanza> getStanzas() { return stanzas; } public CabalSyntax getType() { return type; } public int getIndent() { return indent; } public void setIndent( final int indent ) { this.indent = indent; } public Map<String, String> getProperties() { return properties; } public Map<String, ValuePosition> getPositions() { return positions; } /*public ValuePosition update(final CabalSyntax field,final String value){ return update(field.getCabalName(),value); } public ValuePosition update(final String field,final String value){ getProperties().put(field.toLowerCase(),value); ValuePosition vp=getPositions().get( field.toLowerCase() ); if (vp==null){ return new ValuePosition(endLine,endLine,0); } return vp; }*/ public RealValuePosition update(final CabalSyntax field,final String value){ String key=field.getCabalName().toLowerCase( Locale.ENGLISH ); String realValue = value; if (value!=null && value.trim().length()==0){ realValue=null; } Map.Entry<String,String> eLast=null; if (realValue!=null){ if (needNL){ for (Map.Entry<String,String> e:getProperties().entrySet()){ eLast=e; } } getProperties().put(key,realValue); } else { getProperties().remove(key); } ValuePosition oldVP=getPositions().get( field ); int indent=0; int subIndent=0; int insertSub=0; StringBuilder sb=new StringBuilder(); if (oldVP==null){ oldVP=new ValuePosition(getEndLine(),getEndLine(),0); Collection<ValuePosition> vps=getPositions().values(); for (int a=0;a<getIndent();a++){ sb.append( ' '); } String name=getRealNames().get( key ); sb.append(name!=null?name: field.getCabalName() ); sb.append( ":" ); //$NON-NLS-1$ int spaces=4; if (vps.isEmpty()){ subIndent=getIndent()+key.length()+5; } else { ValuePosition vp=vps.iterator().next(); indent=subIndent=vp.getSubsequentIndent(); spaces=indent-(key.length() +1+getIndent()); if (needNL && eLast!=null){ addLeadingNL(sb,oldVP,eLast); //insertSub+=PlatformUtil.NL.length(); } } spaces=Math.max( spaces, 1 ); for (int a=0;a<spaces;a++){ sb.append( ' '); } insertSub=sb.length(); } else { subIndent=oldVP.getSubsequentIndent(); indent=oldVP.getInitialIndent(); insertSub=0; } if (realValue==null){ getPositions().remove( key ); // remove field name too oldVP.setInitialIndent( 0 ); for (PackageDescriptionStanza st:getPackageDescription().getStanzas()){ st.diffLineIf( oldVP.getStartLine()-oldVP.getEndLine(), oldVP.getEndLine() ); } //oldVP.setInitialIndent( 0 ); return new RealValuePosition( oldVP,""); //$NON-NLS-1$ } BufferedReader br=new BufferedReader( new StringReader( realValue ) ); try { String line=br.readLine(); int count=0; while (line!=null){ for (int a=0;count>0 && a<subIndent;a++){ sb.append( ' '); } count++; if (line.trim().length()==0){ line= "."+line ; //$NON-NLS-1$ } sb.append( line ); sb.append( PlatformUtil.NL ); line=br.readLine(); } if (count>1){ // when several line, start at next one for (int a=0;a<subIndent;a++){ sb.insert( insertSub, ' '); } sb.insert( insertSub, PlatformUtil.NL ); count++; } ValuePosition newVP=new ValuePosition(oldVP.getStartLine(),oldVP.getStartLine()+count,indent); newVP.setSubsequentIndent( subIndent ); getPositions().put( key, newVP ); int diff=newVP.getEndLine()-oldVP.getEndLine(); if (oldVP.getEndLine()==getEndLine()){ needNL=false; } else { for (PackageDescriptionStanza st:getPackageDescription().getStanzas()){ st.diffLineIf( diff, oldVP.getEndLine() ); } } setEndLine( getEndLine()+diff ); return new RealValuePosition( oldVP,sb.toString()); } catch (IOException ioe){ return null; } } /** * if the cabal file didn't end with a NL, we need to add it when we modify the document * @param sb the builder for the final value * @param oldVP the current position we're inserting * @param eLast the entry for the last property in the stanza */ private void addLeadingNL(final StringBuilder sb,final ValuePosition oldVP,final Map.Entry<String,String> eLast){ needNL=false; sb.insert( 0, PlatformUtil.NL ); oldVP.setStartLine(oldVP.getStartLine()-1); //oldVP.setEndLine(oldVP.getEndLine()-1); for (CabalSyntax cs:CabalSyntax.values()){ if (cs.getCabalName().toLowerCase().equals( eLast.getKey() )){ RealValuePosition rvpLast=update(cs,eLast.getValue()); if (rvpLast.getStartLine()+1==rvpLast.getEndLine()){ oldVP.setInitialIndent(rvpLast.getInitialIndent()+rvpLast.getRealValue().length()-PlatformUtil.NL.length()); } else { BufferedReader br=new BufferedReader( new StringReader( rvpLast.getRealValue() ) ); try { String line=br.readLine(); while (line!=null){ String newLine=br.readLine(); if (newLine==null){ oldVP.setInitialIndent(line.length()); } line=newLine; } } catch (IOException ioe){ // cannot happen } } break; } } } public RealValuePosition addToPropertyList(final CabalSyntax field,final String value){ String s=getProperties().get( field ); List<String> ls=PackageDescriptionLoader.parseList( s ); // short lists we hope if (!ls.contains( value )){ StringBuilder newValue=new StringBuilder(); if (s!=null){ newValue.append( s ); if (s.trim().length()>0){ if (!s.trim().endsWith( "," )){ //$NON-NLS-1$ newValue.append(","); //$NON-NLS-1$ } if (!s.endsWith( PlatformUtil.NL )){ newValue.append(PlatformUtil.NL); } /*if (!s.endsWith( " " )){ //$NON-NLS-1$ newValue.append(" "); //$NON-NLS-1$ }*/ } } newValue.append(value); return update(field, newValue.toString() ); } return null; } public RealValuePosition removeFromPropertyList(final CabalSyntax field,final String value){ String s=getProperties().get( field ); if (s!=null){ List<String> ls=PackageDescriptionLoader.parseList( s ); StringBuilder newValue=new StringBuilder(s.length()); boolean changed=false; for (String token:ls){ if (!value.equals( token )){ if(newValue.length()>0){ newValue.append( ","+PlatformUtil.NL ); //$NON-NLS-1$ } newValue.append( token); } else { changed=true; } } if (changed){ return update(field, newValue.toString() ); } } return null; } public RealValuePosition removePrefixFromPropertyList(final CabalSyntax field,final String prefix,final String seps){ String s=getProperties().get( field ); if (s!=null){ List<String> ls=PackageDescriptionLoader.parseList( s,seps ); StringBuilder newValue=new StringBuilder(s.length()); boolean changed=false; String prefixSp=prefix+" "; //$NON-NLS-1$ for (String token:ls){ if ((!prefix.equals( token.trim() )) && (!token.trim().startsWith( prefixSp ))){ if(newValue.length()>0){ newValue.append( ", " ); //$NON-NLS-1$ } else if (token.startsWith( PlatformUtil.NL )){ token=token.substring( PlatformUtil.NL.length() ); } newValue.append( token); } else { changed=true; } } if (changed){ return update(field, newValue.toString() ); } } return update(field, s); } public String getName() { return name; } public int getStartLine() { return startLine; } public int getEndLine() { return endLine; } public String toTypeName(){ // this is equivalent to Component toString() String tn=realTypeName!=null?realTypeName:String.valueOf( getType() ) ; return tn + (getName()!=null?" "+getName():""); //$NON-NLS-1$//$NON-NLS-2$ } // interface methods of Object ////////////////////////////// @Override public String toString() { return (getName()!=null?getName():String.valueOf( getType() )) + " (line " //$NON-NLS-1$ + startLine + "-" //$NON-NLS-1$ + endLine + ")"; //$NON-NLS-1$ } public Collection<String> getDependentPackages(){ Collection<String> ret=new HashSet<>(); String val=getProperties().get( CabalSyntax.FIELD_BUILD_DEPENDS); if (val!=null && val.length()>0){ List<String> ls=PackageDescriptionLoader.parseList( val,"," ); //$NON-NLS-1$ for (String s:ls){ int a=0; for (;a<s.length();a++){ char c=s.charAt( a ); if (c == '=' || c== '>' || c== '<'){ break; } } s=s.substring(0,a).trim(); ret.add( s); } } for (PackageDescriptionStanza st:getStanzas()){ ret.addAll(st.getDependentPackages()); } return ret; } public Collection<String> getSourceDirs(){ Collection<String> ret=new HashSet<>(); String val=getProperties().get( CabalSyntax.FIELD_HS_SOURCE_DIRS); if (val!=null && val.length()>0){ ret.addAll(PackageDescriptionLoader.parseList( val)); } // backward compatibility val=getProperties().get( "hs-source-dir"); //$NON-NLS-1$ if (val!=null && val.length()>0){ ret.addAll(PackageDescriptionLoader.parseList( val)); } for (PackageDescriptionStanza st:getStanzas()){ ret.addAll(st.getSourceDirs()); } if (ret.isEmpty() && (getType()!=null && isSourceStanza())){ ret.add("."); //$NON-NLS-1$ } return ret; } /** * Are we a stanza that contains sources? * @return */ public boolean isSourceStanza(){ return getType()!=null && ( getType().equals( CabalSyntax.SECTION_EXECUTABLE) || getType().equals( CabalSyntax.SECTION_LIBRARY) || getType().equals( CabalSyntax.SECTION_TESTSUITE) || getType().equals( CabalSyntax.SECTION_BENCHMARK)); } /** * are we a stanza that can contain a Main module? * @return */ public boolean isMainStanza(){ return getType()!=null && ( getType().equals( CabalSyntax.SECTION_EXECUTABLE) || getType().equals( CabalSyntax.SECTION_TESTSUITE) || getType().equals( CabalSyntax.SECTION_BENCHMARK)); } public Collection<String> getNonHaskellFiles(){ Collection<String> ret=new HashSet<>(); String val=getProperties().get( CabalSyntax.FIELD_INCLUDES); if (val!=null && val.length()>0){ ret.addAll(PackageDescriptionLoader.parseList( val)); } val=getProperties().get( CabalSyntax.FIELD_C_SOURCES); if (val!=null && val.length()>0){ ret.addAll(PackageDescriptionLoader.parseList( val)); } for (PackageDescriptionStanza st:getStanzas()){ ret.addAll(st.getNonHaskellFiles()); } return ret; } public void dump(final Writer w,final int indent) throws IOException { int indent2=indent; if (getType()!=null){ for (int a=0;a<indent;a++){ w.write(" "); //$NON-NLS-1$ } w.write( toTypeName() ); w.write(PlatformUtil.NL); indent2=indent+2; } int max=0; for (String p:positions.keySet()){ max=Math.max( p.length(), max ); } for (String p:positions.keySet()){ for (int a=0;a<indent2;a++){ w.write(" "); //$NON-NLS-1$ } String name=getRealNames().get( p ); w.write( name!=null?name:p ); ValuePosition vp=positions.get(p); w.write( ":" ); //$NON-NLS-1$ for (int a=0;a<max+2-p.length();a++){ w.write(" "); //$NON-NLS-1$ } boolean first=true; String value=properties.get(p); BufferedReader br=new BufferedReader( new StringReader( value ) ); try { String line=br.readLine(); while (line!=null){ String line2=br.readLine(); if (!first){ for (int a=0;a<vp.getSubsequentIndent();a++){ w.write(" "); //$NON-NLS-1$ } } else { first=false; // multiple lines: start on next line if (line2!=null){ w.write(PlatformUtil.NL); for (int a=0;a<vp.getSubsequentIndent();a++){ w.write(" "); //$NON-NLS-1$ } } } if (line.trim().length()==0){ line= "."+line ; //$NON-NLS-1$ } w.write(line); w.write(PlatformUtil.NL); line=line2; } } catch (IOException ignore){ // cannot happen } } w.write(PlatformUtil.NL); for (PackageDescriptionStanza pds:stanzas){ pds.dump( w, indent2 ); } } public ModuleInclusionType getModuleInclusionType(final String module){ String s=getProperties().get( CabalSyntax.FIELD_OTHER_MODULES ); List<String> ls=PackageDescriptionLoader.parseList( s ); if (ls.contains( module )){ return ModuleInclusionType.INCLUDED; } s=getProperties().get( CabalSyntax.FIELD_EXPOSED_MODULES ); ls=PackageDescriptionLoader.parseList( s ); if (ls.contains( module )){ return ModuleInclusionType.EXPOSED; } s=getProperties().get( CabalSyntax.FIELD_MAIN_IS ); String f=module.replace( '.', '/' ); if ((f+"."+FileUtil.EXTENSION_HS).equals(s) || (f+"."+FileUtil.EXTENSION_LHS).equals(s)){ //$NON-NLS-1$ //$NON-NLS-2$ return ModuleInclusionType.MAIN; } for (PackageDescriptionStanza pds:getStanzas()){ ModuleInclusionType inSubSection=pds.getModuleInclusionType( module ); if (!inSubSection.equals( ModuleInclusionType.MISSING )){ return inSubSection; } } return ModuleInclusionType.MISSING; } public Set<String> listAllModules(){ Set<String> ret=new HashSet<>(); String s=getProperties().get( CabalSyntax.FIELD_OTHER_MODULES ); ret.addAll(PackageDescriptionLoader.parseList( s )); s=getProperties().get( CabalSyntax.FIELD_EXPOSED_MODULES ); ret.addAll(PackageDescriptionLoader.parseList( s )); s=getProperties().get( CabalSyntax.FIELD_MAIN_IS ); if (s!=null){ ret.add("Main"); //$NON-NLS-1$ } for (PackageDescriptionStanza pds:getStanzas()){ ret.addAll( pds.listAllModules() ); } return ret; } public Set<String> listExposedModules(){ Set<String> ret=new HashSet<>(); String s=getProperties().get( CabalSyntax.FIELD_EXPOSED_MODULES ); ret.addAll(PackageDescriptionLoader.parseList( s )); for (PackageDescriptionStanza pds:getStanzas()){ ret.addAll( pds.listExposedModules() ); } return ret; } public Map<String, String> getRealNames() { return realNames; } public String getRealTypeName() { return realTypeName; } public void setRealTypeName( final String realTypeName ) { this.realTypeName = realTypeName; } }